mirror of
https://github.com/dunglas/frankenphp.git
synced 2025-12-24 13:38:11 +08:00
Compare commits
310 Commits
fix/env-te
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e6f665d82 | ||
|
|
57c58faf1c | ||
|
|
25d9cb9600 | ||
|
|
4092ecb5b5 | ||
|
|
75ccccf1b2 | ||
|
|
6231bf4a1c | ||
|
|
e01e40fd97 | ||
|
|
175e644d10 | ||
|
|
a8f75d0eef | ||
|
|
91c553f3d9 | ||
|
|
7fca07ed67 | ||
|
|
3599299cde | ||
|
|
bb1c3678dc | ||
|
|
58a63703b4 | ||
|
|
694ab86cef | ||
|
|
e23e0c571e | ||
|
|
f02e6f2f85 | ||
|
|
11213fd1de | ||
|
|
41da660088 | ||
|
|
599c92b15d | ||
|
|
225ca409d3 | ||
|
|
d2007620a4 | ||
|
|
4ac024a1d0 | ||
|
|
e0dcf42852 | ||
|
|
7b8cf6b127 | ||
|
|
98573ed7c0 | ||
|
|
16e2bbb969 | ||
|
|
816bcc2ad6 | ||
|
|
1fbabf91c9 | ||
|
|
2fa7663d3b | ||
|
|
b1bdce359b | ||
|
|
c9ad9fc55a | ||
|
|
12d4c3d09b | ||
|
|
7fceb32f7b | ||
|
|
1b30905c26 | ||
|
|
dadeb5a628 | ||
|
|
abaf03c7f7 | ||
|
|
fc5f6ef092 | ||
|
|
65111334a1 | ||
|
|
6747aaae2d | ||
|
|
6c764ad9c5 | ||
|
|
e6b3f70d91 | ||
|
|
911e6d156b | ||
|
|
c6cadf3bf6 | ||
|
|
f28f6e8d03 | ||
|
|
01beb66573 | ||
|
|
41e0713a1b | ||
|
|
bbfb1b0a0e | ||
|
|
49e98cc8d6 | ||
|
|
c93729e136 | ||
|
|
ea042637e6 | ||
|
|
0b74507945 | ||
|
|
aa1bd23004 | ||
|
|
02f900bb97 | ||
|
|
56df2666e1 | ||
|
|
1de9073e49 | ||
|
|
36062a0dce | ||
|
|
10cf2c4a2e | ||
|
|
f224f8e391 | ||
|
|
0b2d3c913f | ||
|
|
75a48e81a7 | ||
|
|
bd943f49de | ||
|
|
8f298ab060 | ||
|
|
41cb2bbeaa | ||
|
|
853cb67e95 | ||
|
|
eeb7d1a0c4 | ||
|
|
8341cc98c6 | ||
|
|
40cb42aace | ||
|
|
1e48fbb801 | ||
|
|
4e6d67e0b4 | ||
|
|
18946308fd | ||
|
|
f7298557aa | ||
|
|
861b345b05 | ||
|
|
724c0b11ca | ||
|
|
63168e087e | ||
|
|
6225da9c18 | ||
|
|
407ef09ac3 | ||
|
|
bf4c9fe986 | ||
|
|
b22bdd987b | ||
|
|
28d17b39dc | ||
|
|
264f92835d | ||
|
|
b49aed1934 | ||
|
|
4d0fb7d0f8 | ||
|
|
5447a7a6c8 | ||
|
|
1270784cd3 | ||
|
|
9b8d215727 | ||
|
|
94e58eb215 | ||
|
|
bf6e6534f6 | ||
|
|
fb1f46808e | ||
|
|
7f64673495 | ||
|
|
f7756717b5 | ||
|
|
9aee496b96 | ||
|
|
45823c51b2 | ||
|
|
f8ea48c3b1 | ||
|
|
1fbd619597 | ||
|
|
d52ce94341 | ||
|
|
b749f52ae5 | ||
|
|
e917ab7974 | ||
|
|
5514491a18 | ||
|
|
c42d287138 | ||
|
|
1f6f768c97 | ||
|
|
a4596b7767 | ||
|
|
fa3a7032a4 | ||
|
|
ab1ec71d24 | ||
|
|
219a5407ff | ||
|
|
76511c2e19 | ||
|
|
7668a27d4b | ||
|
|
e4c1801c25 | ||
|
|
52df300f86 | ||
|
|
fe7aa2cae4 | ||
|
|
7754abcbd0 | ||
|
|
52a0be5728 | ||
|
|
960dd209f7 | ||
|
|
984f0a0211 | ||
|
|
86a2d27c01 | ||
|
|
0f942c8601 | ||
|
|
8c501a8d98 | ||
|
|
c564c3ffb9 | ||
|
|
6ab8350561 | ||
|
|
ad86bf49c2 | ||
|
|
1030e4ceb4 | ||
|
|
460d63e436 | ||
|
|
78bc5c87d8 | ||
|
|
99bb87167e | ||
|
|
c14d771fdf | ||
|
|
952754db27 | ||
|
|
9b851bf53e | ||
|
|
d540727369 | ||
|
|
c10e85b905 | ||
|
|
4dd6b5ea16 | ||
|
|
2b78ffe15c | ||
|
|
53e6d1897d | ||
|
|
1eb16f1434 | ||
|
|
34005da9f8 | ||
|
|
b3fb8dfaba | ||
|
|
d7aebedd2d | ||
|
|
5f153e06d6 | ||
|
|
555c613669 | ||
|
|
e34b82b425 | ||
|
|
af057a93a9 | ||
|
|
a1ae2692e1 | ||
|
|
6ad34b1cb3 | ||
|
|
f2e9217bfc | ||
|
|
1f6d6bde92 | ||
|
|
c7bc5a3778 | ||
|
|
9e4a6b789b | ||
|
|
8d148a16e2 | ||
|
|
1d0169d321 | ||
|
|
365eae1a99 | ||
|
|
2a41fc183a | ||
|
|
8175ae7e8c | ||
|
|
cd16da248a | ||
|
|
f224ffaece | ||
|
|
50b438f978 | ||
|
|
f7ea33d328 | ||
|
|
ce9620b5be | ||
|
|
6e120283e9 | ||
|
|
1da2ba1f28 | ||
|
|
0c25b2488c | ||
|
|
3e542576f6 | ||
|
|
34fbfd467b | ||
|
|
8df41236d9 | ||
|
|
1804e36b93 | ||
|
|
63c742648d | ||
|
|
a19fcdb38d | ||
|
|
a161af26ae | ||
|
|
23073b6626 | ||
|
|
d5544bbca4 | ||
|
|
6ce99f251a | ||
|
|
1ba19ae09e | ||
|
|
b80cb6cdea | ||
|
|
23c493dfcf | ||
|
|
6be261169a | ||
|
|
292e98cd3d | ||
|
|
e9d8923c6a | ||
|
|
ac900e0df4 | ||
|
|
40ee7929a1 | ||
|
|
fb10b1e8f0 | ||
|
|
94c3fac556 | ||
|
|
fcc5299a20 | ||
|
|
92abb16bc0 | ||
|
|
94ac4b4935 | ||
|
|
29c88c0fec | ||
|
|
80de1f8bc7 | ||
|
|
93f2384749 | ||
|
|
db9c8446ef | ||
|
|
995c829247 | ||
|
|
96400a85d0 | ||
|
|
58fde42654 | ||
|
|
291dd4eed9 | ||
|
|
30ef5f6657 | ||
|
|
8d88c13795 | ||
|
|
d2a1b619a5 | ||
|
|
9e3b47c52f | ||
|
|
abfd893d88 | ||
|
|
bbc3e49d6f | ||
|
|
2712876e95 | ||
|
|
b2435183f4 | ||
|
|
cfb9d9f895 | ||
|
|
5c69109011 | ||
|
|
12f469e701 | ||
|
|
71aebbe0e7 | ||
|
|
34a0255c15 | ||
|
|
9bd314d2fb | ||
|
|
3afb709f02 | ||
|
|
82aeb128bc | ||
|
|
6749ddbde5 | ||
|
|
82ba882a4e | ||
|
|
4b1679e70f | ||
|
|
75ce2e22c2 | ||
|
|
5a43e9f4de | ||
|
|
5542044376 | ||
|
|
52b65311c2 | ||
|
|
2dc8048ad2 | ||
|
|
a59b649dac | ||
|
|
68a4548bf4 | ||
|
|
6f049f9a9c | ||
|
|
340b1fd1c2 | ||
|
|
c9329bd717 | ||
|
|
f54a1fa85e | ||
|
|
b4115ca9a2 | ||
|
|
14469d4a0a | ||
|
|
ee394756b1 | ||
|
|
5a260c430a | ||
|
|
b6fcab5a95 | ||
|
|
1e49586b0e | ||
|
|
b27cd1c986 | ||
|
|
c6483088c5 | ||
|
|
5a9785d0d9 | ||
|
|
c522b52804 | ||
|
|
9a8ad979e0 | ||
|
|
663aff7cc4 | ||
|
|
79f2b2347b | ||
|
|
bf5c98410b | ||
|
|
cf7541fde6 | ||
|
|
25491068df | ||
|
|
d72751b9fd | ||
|
|
8820e53819 | ||
|
|
d2b6f9e723 | ||
|
|
13fbe126ea | ||
|
|
afa7dafe1c | ||
|
|
39e22bd5e0 | ||
|
|
bbbfdb31b5 | ||
|
|
0b83602575 | ||
|
|
ecca9dc01d | ||
|
|
eb40c03a21 | ||
|
|
c2390e7c3b | ||
|
|
0d12a5162d | ||
|
|
a48db9422d | ||
|
|
ab0fcd80de | ||
|
|
2f7b987198 | ||
|
|
1d74b2caa8 | ||
|
|
92e92335e3 | ||
|
|
8f5f9e4c8b | ||
|
|
5b7fc5ec52 | ||
|
|
05220de0e3 | ||
|
|
3741782330 | ||
|
|
a6e1d3554d | ||
|
|
6f1b4f3bae | ||
|
|
cd540bda11 | ||
|
|
8125993001 | ||
|
|
8583afd83e | ||
|
|
d10a243f86 | ||
|
|
1ec37f6cc9 | ||
|
|
4ad5e870ec | ||
|
|
49d2e62996 | ||
|
|
8febee71af | ||
|
|
16814581f9 | ||
|
|
ffa52f7c8d | ||
|
|
4550027de4 | ||
|
|
7f8e43fd62 | ||
|
|
254c0a8a55 | ||
|
|
22cf94d556 | ||
|
|
a4dc93f831 | ||
|
|
c276a3f434 | ||
|
|
02a1c92b88 | ||
|
|
8092f4a35c | ||
|
|
b250bd9a07 | ||
|
|
99064ee3e1 | ||
|
|
58a728b790 | ||
|
|
66aa975d47 | ||
|
|
5e1ad5d797 | ||
|
|
96dd739064 | ||
|
|
729cf9bba1 | ||
|
|
c5752f9e3b | ||
|
|
ba36f92a35 | ||
|
|
d3589f9770 | ||
|
|
8e6a183bda | ||
|
|
855b3f93b1 | ||
|
|
f85ca1c2d2 | ||
|
|
a30ed2e9d3 | ||
|
|
565b3a9629 | ||
|
|
3701516e5e | ||
|
|
f36bd51163 | ||
|
|
45bba2101f | ||
|
|
87315a19ae | ||
|
|
3bc426482a | ||
|
|
341b0240c9 | ||
|
|
432824edf1 | ||
|
|
9cca12858b | ||
|
|
cc473ee03e | ||
|
|
93266dfcad | ||
|
|
6203d207fa | ||
|
|
424ca426cb | ||
|
|
a9cf944b62 | ||
|
|
8d9ce15849 | ||
|
|
409c0fdf5f | ||
|
|
f50248a7d2 | ||
|
|
09b8219ad4 | ||
|
|
f2bae25a78 |
1
.clang-format-ignore
Normal file
1
.clang-format-ignore
Normal file
@@ -0,0 +1 @@
|
||||
frankenphp_arginfo.h
|
||||
@@ -1,14 +1,11 @@
|
||||
# ignored
|
||||
**/*
|
||||
|
||||
# authorized
|
||||
!**/Caddyfile
|
||||
!**/*.go
|
||||
!**/go.*
|
||||
!**/*.c
|
||||
!**/*.h
|
||||
!testdata/*.php
|
||||
!testdata/*.txt
|
||||
!build-static.sh
|
||||
!app.tar
|
||||
!app_checksum.txt
|
||||
/caddy/frankenphp/frankenphp
|
||||
/internal/testserver/testserver
|
||||
/internal/testcli/testcli
|
||||
/dist
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
__debug_bin
|
||||
frankenphp.test
|
||||
caddy/frankenphp/Build
|
||||
*.log
|
||||
|
||||
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.sh]
|
||||
indent_style = tab
|
||||
tab_width = 4
|
||||
|
||||
[*.Dockerfile]
|
||||
indent_style = tab
|
||||
tab_width = 4
|
||||
8
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -10,7 +10,7 @@ body:
|
||||
Before submitting a bug, please double-check that your problem [is not
|
||||
a known issue](https://frankenphp.dev/docs/known-issues/)
|
||||
(especially if you use XDebug or Tideways), and that is has not
|
||||
[already been reported](https://github.com/dunglas/frankenphp/issues).
|
||||
[already been reported](https://github.com/php/frankenphp/issues).
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
@@ -26,10 +26,12 @@ body:
|
||||
label: Build Type
|
||||
description: What build of FrankenPHP do you use?
|
||||
options:
|
||||
- Docker (Debian Trixie)
|
||||
- Docker (Debian Bookworm)
|
||||
- Docker (Alpine)
|
||||
- Official static build
|
||||
- Standalone binary
|
||||
- deb packages
|
||||
- rpm packages
|
||||
- Static binary
|
||||
- Custom (tell us more in the description)
|
||||
default: 0
|
||||
validations:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -6,7 +6,7 @@ body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe you feature request
|
||||
label: Describe your feature request
|
||||
value: |
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is.
|
||||
|
||||
2
.github/actions/watcher/action.yaml
vendored
2
.github/actions/watcher/action.yaml
vendored
@@ -19,7 +19,7 @@ runs:
|
||||
name: Compile e-dant/watcher
|
||||
run: |
|
||||
mkdir watcher
|
||||
gh release download 0.13.2 --repo e-dant/watcher -A tar.gz -O - | tar -xz -C watcher --strip-components 1
|
||||
gh release download --repo e-dant/watcher -A tar.gz -O - | tar -xz -C watcher --strip-components 1
|
||||
cd watcher
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build
|
||||
|
||||
23
.github/dependabot.yaml
vendored
23
.github/dependabot.yaml
vendored
@@ -7,20 +7,33 @@ updates:
|
||||
interval: weekly
|
||||
commit-message:
|
||||
prefix: chore
|
||||
groups:
|
||||
go-modules:
|
||||
patterns:
|
||||
- "*"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: gomod
|
||||
directory: /caddy
|
||||
schedule:
|
||||
interval: weekly
|
||||
commit-message:
|
||||
prefix: chore(caddy)
|
||||
# These packages must be in sync with versions
|
||||
# used by github.com/caddyserver/caddy/v2
|
||||
ignore:
|
||||
- dependency-name: github.com/google/cel-go
|
||||
- dependency-name: github.com/quic-go/*
|
||||
groups:
|
||||
go-modules:
|
||||
patterns:
|
||||
- "*"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
commit-message:
|
||||
prefix: ci
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
87
.github/workflows/docker.yaml
vendored
87
.github/workflows/docker.yaml
vendored
@@ -7,8 +7,15 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
paths:
|
||||
- "docker-bake.hcl"
|
||||
- ".github/workflows/docker.yaml"
|
||||
- "**cgo.go"
|
||||
- "**Dockerfile"
|
||||
- "**.c"
|
||||
- "**.h"
|
||||
- "**.sh"
|
||||
- "**.stub.php"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -39,6 +46,8 @@ jobs:
|
||||
php_version: ${{ steps.check.outputs.php_version }}
|
||||
php82_version: ${{ steps.check.outputs.php82_version }}
|
||||
php83_version: ${{ steps.check.outputs.php83_version }}
|
||||
php84_version: ${{ steps.check.outputs.php84_version }}
|
||||
php85_version: ${{ steps.check.outputs.php85_version }}
|
||||
skip: ${{ steps.check.outputs.skip }}
|
||||
ref: ${{ steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}
|
||||
steps:
|
||||
@@ -50,11 +59,13 @@ jobs:
|
||||
PHP_82_LATEST=$(skopeo inspect docker://docker.io/library/php:8.2 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
|
||||
PHP_83_LATEST=$(skopeo inspect docker://docker.io/library/php:8.3 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
|
||||
PHP_84_LATEST=$(skopeo inspect docker://docker.io/library/php:8.4 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
|
||||
PHP_85_LATEST=$(skopeo inspect docker://docker.io/library/php:8.5 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
|
||||
{
|
||||
echo php_version="${PHP_82_LATEST},${PHP_83_LATEST},${PHP_84_LATEST}"
|
||||
echo php_version="${PHP_82_LATEST},${PHP_83_LATEST},${PHP_84_LATEST},${PHP_85_LATEST}"
|
||||
echo php82_version="${PHP_82_LATEST//./-}"
|
||||
echo php83_version="${PHP_83_LATEST//./-}"
|
||||
echo php84_version="${PHP_84_LATEST//./-}"
|
||||
echo php85_version="${PHP_85_LATEST//./-}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
# Check if the Docker images must be rebuilt
|
||||
@@ -63,23 +74,27 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FRANKENPHP_82_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:php8.2 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
|
||||
FRANKENPHP_83_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:php8.3 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
|
||||
FRANKENPHP_84_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:php8.4 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
|
||||
FRANKENPHP_LATEST_TAG=$(gh release view --repo php/frankenphp --json tagName --jq '.tagName')
|
||||
FRANKENPHP_LATEST_TAG_NO_PREFIX="${FRANKENPHP_LATEST_TAG#v}"
|
||||
FRANKENPHP_82_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:"${FRANKENPHP_LATEST_TAG_NO_PREFIX}"-php8.2 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
|
||||
FRANKENPHP_83_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:"${FRANKENPHP_LATEST_TAG_NO_PREFIX}"-php8.3 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
|
||||
FRANKENPHP_84_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:"${FRANKENPHP_LATEST_TAG_NO_PREFIX}"-php8.4 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
|
||||
FRANKENPHP_85_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:"${FRANKENPHP_LATEST_TAG_NO_PREFIX}"-php8.5 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
|
||||
|
||||
if [[ "${FRANKENPHP_82_LATEST}" == "${PHP_82_LATEST}" ]] && [[ "${FRANKENPHP_83_LATEST}" == "${PHP_83_LATEST}" ]] && [[ "${FRANKENPHP_84_LATEST}" == "${PHP_84_LATEST}" ]]; then
|
||||
if [[ "${FRANKENPHP_82_LATEST}" == "${PHP_82_LATEST}" ]] && [[ "${FRANKENPHP_83_LATEST}" == "${PHP_83_LATEST}" ]] && [[ "${FRANKENPHP_84_LATEST}" == "${PHP_84_LATEST}" ]] && [[ "${FRANKENPHP_85_LATEST}" == "${PHP_85_LATEST}" ]]; then
|
||||
echo skip=true >> "${GITHUB_OUTPUT}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
{
|
||||
echo ref="$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')"
|
||||
echo ref="${FRANKENPHP_LATEST_TAG}"
|
||||
echo skip=false
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
if: ${{ !fromJson(steps.check.outputs.skip) }}
|
||||
with:
|
||||
ref: ${{ steps.check.outputs.ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{ !fromJson(steps.check.outputs.skip) }}
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -115,26 +130,39 @@ jobs:
|
||||
race: "-race" # The Go race detector is only supported on amd64
|
||||
exclude:
|
||||
# arm/v6 is only available for Alpine: https://github.com/docker-library/golang/issues/502
|
||||
- variant: php-${{ needs.prepare.outputs.php82_version }}-trixie
|
||||
platform: linux/arm/v6
|
||||
- variant: php-${{ needs.prepare.outputs.php83_version }}-trixie
|
||||
platform: linux/arm/v6
|
||||
- variant: php-${{ needs.prepare.outputs.php84_version }}-trixie
|
||||
platform: linux/arm/v6
|
||||
- variant: php-${{ needs.prepare.outputs.php85_version }}-trixie
|
||||
platform: linux/arm/v6
|
||||
- variant: php-${{ needs.prepare.outputs.php82_version }}-bookworm
|
||||
platform: linux/arm/v6
|
||||
- variant: php-${{ needs.prepare.outputs.php83_version }}-bookworm
|
||||
platform: linux/arm/v6
|
||||
- variant: php-${{ needs.prepare.outputs.php84_version }}-bookworm
|
||||
platform: linux/arm/v6
|
||||
- variant: php-${{ needs.prepare.outputs.php85_version }}-bookworm
|
||||
platform: linux/arm/v6
|
||||
steps:
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "sanitized_platform=${platform//\//-}" >> "${GITHUB_OUTPUT}"
|
||||
- uses: actions/checkout@v4
|
||||
run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
- name: Login to DockerHub
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
with:
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
@@ -144,6 +172,7 @@ jobs:
|
||||
with:
|
||||
pull: true
|
||||
load: ${{ !fromJson(needs.prepare.outputs.push) }}
|
||||
source: .
|
||||
targets: |
|
||||
builder-${{ matrix.variant }}
|
||||
runner-${{ matrix.variant }}
|
||||
@@ -169,16 +198,17 @@ jobs:
|
||||
run: |
|
||||
mkdir -p /tmp/metadata/builder /tmp/metadata/runner
|
||||
|
||||
builderDigest=$(jq -r '."builder-${{ matrix.variant }}"."containerimage.digest"' <<< "${METADATA}")
|
||||
builderDigest=$(jq -r ".\"builder-${VARIANT}\".\"containerimage.digest\"" <<< "${METADATA}")
|
||||
touch "/tmp/metadata/builder/${builderDigest#sha256:}"
|
||||
|
||||
runnerDigest=$(jq -r '."runner-${{ matrix.variant }}"."containerimage.digest"' <<< "${METADATA}")
|
||||
runnerDigest=$(jq -r ".\"runner-${VARIANT}\".\"containerimage.digest\"" <<< "${METADATA}")
|
||||
touch "/tmp/metadata/runner/${runnerDigest#sha256:}"
|
||||
env:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
VARIANT: ${{ matrix.variant }}
|
||||
- name: Upload builder metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: metadata-builder-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}
|
||||
path: /tmp/metadata/builder/*
|
||||
@@ -186,7 +216,7 @@ jobs:
|
||||
retention-days: 1
|
||||
- name: Upload runner metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: metadata-runner-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}
|
||||
path: /tmp/metadata/runner/*
|
||||
@@ -195,11 +225,15 @@ jobs:
|
||||
- name: Run tests
|
||||
if: ${{ !fromJson(needs.prepare.outputs.push) }}
|
||||
run: |
|
||||
docker run --platform=${{ matrix.platform }} --rm \
|
||||
"$(jq -r '."builder-${{ matrix.variant }}"."containerimage.config.digest"' <<< "${METADATA}")" \
|
||||
sh -c 'go test -tags ${{ matrix.race }} -v ./... && cd caddy && go test -tags nobadger,nomysql,nopgx ${{ matrix.race }} -v ./...'
|
||||
docker run --platform="${PLATFORM}" --rm \
|
||||
"$(jq -r ".\"builder-${VARIANT}\".\"containerimage.config.digest\"" <<< "${METADATA}")" \
|
||||
sh -c "./go.sh test ${RACE} -v $(./go.sh list ./... | grep -v github.com/dunglas/frankenphp/internal/testext | grep -v github.com/dunglas/frankenphp/internal/extgen | tr '\n' ' ') && cd caddy && ../go.sh test ${RACE} -v ./..."
|
||||
env:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
VARIANT: ${{ matrix.variant }}
|
||||
RACE: ${{ matrix.race }}
|
||||
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
push:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -214,7 +248,7 @@ jobs:
|
||||
target: ["builder", "runner"]
|
||||
steps:
|
||||
- name: Download metadata
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: metadata-${{ matrix.target }}-${{ matrix.variant }}-*
|
||||
path: /tmp/metadata
|
||||
@@ -223,6 +257,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
with:
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
@@ -231,13 +266,17 @@ jobs:
|
||||
run: |
|
||||
set -x
|
||||
# shellcheck disable=SC2046,SC2086
|
||||
docker buildx imagetools create $(jq -cr '.target."${{ matrix.target }}-${{ matrix.variant }}".tags | map("-t " + .) | join(" ")' <<< ${METADATA}) \
|
||||
docker buildx imagetools create $(jq -cr ".target.\"${TARGET}-${VARIANT}\".tags | map(\"-t \" + .) | join(\" \")" <<< ${METADATA}) \
|
||||
$(printf "${IMAGE_NAME}@sha256:%s " *)
|
||||
env:
|
||||
METADATA: ${{ needs.prepare.outputs.metadata }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
VARIANT: ${{ matrix.variant }}
|
||||
- name: Inspect image
|
||||
run: |
|
||||
# shellcheck disable=SC2046,SC2086
|
||||
docker buildx imagetools inspect $(jq -cr '.target."${{ matrix.target }}-${{ matrix.variant }}".tags | first' <<< ${METADATA})
|
||||
docker buildx imagetools inspect $(jq -cr ".target.\"${TARGET}-${VARIANT}\".tags | first" <<< ${METADATA})
|
||||
env:
|
||||
METADATA: ${{ needs.prepare.outputs.metadata }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
VARIANT: ${{ matrix.variant }}
|
||||
|
||||
17
.github/workflows/lint.yaml
vendored
17
.github/workflows/lint.yaml
vendored
@@ -1,5 +1,8 @@
|
||||
---
|
||||
name: Lint Code Base
|
||||
concurrency:
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -17,14 +20,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Lint Code Base
|
||||
uses: super-linter/super-linter/slim@v7.3.0
|
||||
uses: super-linter/super-linter/slim@v8
|
||||
env:
|
||||
VALIDATE_ALL_CODEBASE: true
|
||||
DEFAULT_BRANCH: main
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
LINTER_RULES_PATH: /
|
||||
MARKDOWN_CONFIG_FILE: .markdown-lint.yaml
|
||||
@@ -37,8 +39,13 @@ jobs:
|
||||
VALIDATE_PHP_PSALM: false
|
||||
VALIDATE_TERRAGRUNT: false
|
||||
VALIDATE_DOCKERFILE_HADOLINT: false
|
||||
# Prettier and StandardJS are incompatible
|
||||
VALIDATE_TRIVY: false
|
||||
# Prettier, Biome and StandardJS are incompatible
|
||||
VALIDATE_JAVASCRIPT_PRETTIER: false
|
||||
VALIDATE_TYPESCRIPT_PRETTIER: false
|
||||
VALIDATE_BIOME_FORMAT: false
|
||||
VALIDATE_BIOME_LINT: false
|
||||
# Conflicts with MARKDOWN
|
||||
VALIDATE_MARKDOWN_PRETTIER: false
|
||||
# To re-enable when https://github.com/super-linter/super-linter/issues/7244 will be closed
|
||||
VALIDATE_EDITORCONFIG: false
|
||||
|
||||
25
.github/workflows/sanitizers.yaml
vendored
25
.github/workflows/sanitizers.yaml
vendored
@@ -1,5 +1,8 @@
|
||||
---
|
||||
name: Sanitizers
|
||||
concurrency:
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -13,6 +16,8 @@ on:
|
||||
- "docs/**"
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
GOTOOLCHAIN: local
|
||||
jobs:
|
||||
# Adapted from https://github.com/beberlei/hdrhistogram-php
|
||||
sanitizers:
|
||||
@@ -30,25 +35,29 @@ jobs:
|
||||
USE_ZEND_ALLOC: 0
|
||||
LIBRARY_PATH: ${{ github.workspace }}/php/target/lib:${{ github.workspace }}/watcher/target/lib
|
||||
LD_LIBRARY_PATH: ${{ github.workspace }}/php/target/lib
|
||||
# PHP doesn't free some memory on purpose, we have to disable leaks detection: https://go.dev/doc/go1.25#go-command
|
||||
ASAN_OPTIONS: detect_leaks=0
|
||||
steps:
|
||||
- name: Remove local PHP
|
||||
run: sudo apt-get remove --purge --autoremove 'php*' 'libmemcached*'
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
go-version: "1.24"
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
caddy/go.sum
|
||||
- name: Determine PHP version
|
||||
id: determine-php-version
|
||||
run: |
|
||||
curl -fsSL 'https://www.php.net/releases/index.php?json&max=1&version=8.4' -o version.json
|
||||
curl -fsSL 'https://www.php.net/releases/index.php?json&max=1&version=8.5' -o version.json
|
||||
echo version="$(jq -r 'keys[0]' version.json)" >> "$GITHUB_OUTPUT"
|
||||
echo archive="$(jq -r '.[] .source[] | select(.filename |endswith(".xz")) | "https://www.php.net/distributions/" + .filename' version.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache PHP
|
||||
id: cache-php
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: php/target
|
||||
key: php-sanitizers-${{ matrix.sanitizer }}-${{ runner.arch }}-${{ steps.determine-php-version.outputs.version }}
|
||||
@@ -56,7 +65,7 @@ jobs:
|
||||
name: Compile PHP
|
||||
run: |
|
||||
mkdir php/
|
||||
curl -fsSL "${{ steps.determine-php-version.outputs.archive }}" | tar -Jx -C php --strip-components=1
|
||||
curl -fsSL "${URL}" | tar -Jx -C php --strip-components=1
|
||||
cd php/
|
||||
./configure \
|
||||
CFLAGS="$CFLAGS" \
|
||||
@@ -86,6 +95,8 @@ jobs:
|
||||
--prefix="$(pwd)/target/"
|
||||
make -j"$(getconf _NPROCESSORS_ONLN)"
|
||||
make install
|
||||
env:
|
||||
URL: ${{ steps.determine-php-version.outputs.archive }}
|
||||
- name: Add PHP to the PATH
|
||||
run: echo "$(pwd)/php/target/bin" >> "$GITHUB_PATH"
|
||||
- name: Install e-dant/watcher
|
||||
@@ -97,6 +108,6 @@ jobs:
|
||||
echo "CGO_LDFLAGS=$LDFLAGS $(php-config --ldflags) $(php-config --libs)"
|
||||
} >> "$GITHUB_ENV"
|
||||
- name: Compile tests
|
||||
run: go test ${{ matrix.sanitizer == 'msan' && '-tags=nowatcher' || '' }} -${{ matrix.sanitizer }} -v -x -c
|
||||
run: go test ${{ matrix.sanitizer == 'msan' && '-tags=nowatcher' || '' }} -${{ matrix.sanitizer }} -v -x -c
|
||||
- name: Run tests
|
||||
run: ./frankenphp.test -test.v
|
||||
|
||||
324
.github/workflows/static.yaml
vendored
324
.github/workflows/static.yaml
vendored
@@ -3,12 +3,20 @@ name: Build binary releases
|
||||
concurrency:
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
paths:
|
||||
- "docker-bake.hcl"
|
||||
- ".github/workflows/static.yaml"
|
||||
- "**cgo.go"
|
||||
- "**Dockerfile"
|
||||
- "**.c"
|
||||
- "**.h"
|
||||
- "**.sh"
|
||||
- "**.stub.php"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -23,12 +31,15 @@ on:
|
||||
type: string
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
|
||||
SPC_OPT_BUILD_ARGS: --debug
|
||||
GOTOOLCHAIN: local
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -36,13 +47,14 @@ jobs:
|
||||
push: ${{ toJson((steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false) }}
|
||||
platforms: ${{ steps.matrix.outputs.platforms }}
|
||||
metadata: ${{ steps.matrix.outputs.metadata }}
|
||||
gnu_metadata: ${{ steps.matrix.outputs.gnu_metadata }}
|
||||
ref: ${{ steps.check.outputs.ref }}
|
||||
steps:
|
||||
- name: Get version
|
||||
id: check
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
ref="${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}"
|
||||
ref="${REF}"
|
||||
if [[ -z "${ref}" ]]; then
|
||||
ref="$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')"
|
||||
fi
|
||||
@@ -50,23 +62,32 @@ jobs:
|
||||
echo "ref=${ref}" >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
REF: ${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ steps.check.outputs.ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Create platforms matrix
|
||||
id: matrix
|
||||
run: |
|
||||
METADATA="$(docker buildx bake --print static-builder | jq -c)"
|
||||
METADATA="$(docker buildx bake --print static-builder-musl | jq -c)"
|
||||
GNU_METADATA="$(docker buildx bake --print static-builder-gnu | jq -c)"
|
||||
{
|
||||
echo metadata="${METADATA}"
|
||||
echo platforms="$(jq -c 'first(.target[]) | .platforms' <<< "${METADATA}")"
|
||||
echo gnu_metadata="${GNU_METADATA}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
SHA: ${{ github.sha }}
|
||||
VERSION: ${{ steps.check.outputs.ref || 'dev' }}
|
||||
build-linux:
|
||||
|
||||
build-linux-musl:
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -79,48 +100,62 @@ jobs:
|
||||
debug: true
|
||||
- platform: linux/amd64
|
||||
mimalloc: true
|
||||
name: Build ${{ matrix.platform }} static binary${{ matrix.debug && ' (debug)' || '' }}${{ matrix.mimalloc && ' (mimalloc)' || '' }}
|
||||
name: Build ${{ matrix.platform }} static musl binary${{ matrix.debug && ' (debug)' || '' }}${{ matrix.mimalloc && ' (mimalloc)' || '' }}
|
||||
runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
|
||||
needs: [prepare]
|
||||
steps:
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "sanitized_platform=${platform//\//-}" >> "${GITHUB_OUTPUT}"
|
||||
- uses: actions/checkout@v4
|
||||
run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
- name: Login to DockerHub
|
||||
if: ${{ fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc }}
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
with:
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- name: Set VERSION
|
||||
run: |
|
||||
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
|
||||
export VERSION=${GITHUB_REF_NAME:1}
|
||||
elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
|
||||
export VERSION="${REF}"
|
||||
else
|
||||
export VERSION=${GITHUB_SHA}
|
||||
fi
|
||||
|
||||
echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
|
||||
env:
|
||||
REF: ${{ needs.prepare.outputs.ref }}
|
||||
- name: Build
|
||||
id: build
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
pull: true
|
||||
load: ${{ !fromJson(needs.prepare.outputs.push) || matrix.debug || matrix.mimalloc }}
|
||||
targets: static-builder
|
||||
source: .
|
||||
targets: static-builder-musl
|
||||
set: |
|
||||
${{ matrix.debug && 'static-builder.args.DEBUG_SYMBOLS=1' || '' }}
|
||||
${{ matrix.mimalloc && 'static-builder.args.MIMALLOC=1' || '' }}
|
||||
${{ (github.event_name == 'pull_request' || matrix.platform == 'linux/arm64') && 'static-builder.args.NO_COMPRESS=1' || '' }}
|
||||
${{ matrix.debug && 'static-builder-musl.args.DEBUG_SYMBOLS=1' || '' }}
|
||||
${{ matrix.mimalloc && 'static-builder-musl.args.MIMALLOC=1' || '' }}
|
||||
${{ (github.event_name == 'pull_request' || matrix.platform == 'linux/arm64') && 'static-builder-musl.args.NO_COMPRESS=1' || '' }}
|
||||
*.tags=
|
||||
*.platform=${{ matrix.platform }}
|
||||
*.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
|
||||
*.cache-from=type=gha,scope=refs/heads/main-static-builder${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
|
||||
*.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }},ignore-error=true
|
||||
*.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder-musl${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
|
||||
*.cache-from=type=gha,scope=refs/heads/main-static-builder-musl${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
|
||||
*.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder-musl${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }},ignore-error=true
|
||||
${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
|
||||
env:
|
||||
SHA: ${{ github.sha }}
|
||||
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || 'dev' }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
|
||||
name: Export metadata
|
||||
@@ -129,42 +164,200 @@ jobs:
|
||||
mkdir -p /tmp/metadata
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
digest=$(jq -r '."static-builder"."containerimage.digest"' <<< ${METADATA})
|
||||
digest=$(jq -r '."static-builder-musl"."containerimage.digest"' <<< ${METADATA})
|
||||
touch "/tmp/metadata/${digest#sha256:}"
|
||||
env:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
- name: Upload metadata
|
||||
if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: metadata-static-builder-${{ steps.prepare.outputs.sanitized_platform }}
|
||||
name: metadata-static-builder-musl-${{ steps.prepare.outputs.sanitized_platform }}
|
||||
path: /tmp/metadata/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
- name: Copy binary
|
||||
run: |
|
||||
# shellcheck disable=SC2034
|
||||
digest=$(jq -r '."static-builder"."${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && 'containerimage.digest' || 'containerimage.config.digest' }}"' <<< "${METADATA}")
|
||||
docker create --platform=${{ matrix.platform }} --name static-builder "${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && '${IMAGE_NAME}@${digest}' || '${digest}' }}"
|
||||
docker cp "static-builder:/go/src/app/dist/${BINARY}" "${BINARY}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}"
|
||||
digest=$(jq -r '."static-builder-musl"."${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && 'containerimage.digest' || 'containerimage.config.digest' }}"' <<< "${METADATA}")
|
||||
docker create --platform="${PLATFORM}" --name static-builder-musl "${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && '${IMAGE_NAME}@${digest}' || '${digest}' }}"
|
||||
docker cp "static-builder-musl:/go/src/app/dist/${BINARY}" "${BINARY}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}"
|
||||
env:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
- name: Upload artifact
|
||||
if: ${{ !fromJson(needs.prepare.outputs.push) }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
|
||||
path: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
|
||||
- name: Upload assets
|
||||
if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
|
||||
run: gh release upload "${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}" frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }} --repo dunglas/frankenphp --clobber
|
||||
run: gh release upload "${REF}" frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }} --repo dunglas/frankenphp --clobber
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REF: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}
|
||||
- if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
|
||||
uses: actions/attest-build-provenance@v2
|
||||
uses: actions/attest-build-provenance@v3
|
||||
with:
|
||||
subject-path: ${{ github.workspace }}/frankenphp-linux-*
|
||||
- name: Run sanity checks
|
||||
run: |
|
||||
"${BINARY}" version
|
||||
"${BINARY}" build-info
|
||||
"${BINARY}" list-modules | grep frankenphp
|
||||
"${BINARY}" list-modules | grep http.encoders.br
|
||||
"${BINARY}" list-modules | grep http.handlers.mercure
|
||||
"${BINARY}" list-modules | grep http.handlers.mercure
|
||||
"${BINARY}" list-modules | grep http.handlers.vulcain
|
||||
"${BINARY}" php-cli -r "echo 'Sanity check passed';"
|
||||
env:
|
||||
BINARY: ./frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
|
||||
|
||||
build-linux-gnu:
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
|
||||
name: Build ${{ matrix.platform }} static GNU binary
|
||||
runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
|
||||
needs: [prepare]
|
||||
steps:
|
||||
# Inspired by https://gist.github.com/antiphishfish/1e3fbc3f64ef6f1ab2f47457d2da5d9d and https://github.com/apache/flink/blob/master/tools/azure-pipelines/free_disk_space.sh
|
||||
- name: Free disk space
|
||||
run: |
|
||||
set -xe
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/share/swift
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo rm -rf /opt/hostedtoolcache/
|
||||
sudo rm -rf /usr/local/graalvm/
|
||||
sudo rm -rf /usr/local/share/powershell
|
||||
sudo rm -rf /usr/local/share/chromium
|
||||
sudo rm -rf /usr/local/lib/node_modules
|
||||
sudo docker image prune --all --force
|
||||
|
||||
APT_PARAMS='sudo apt -y -qq -o=Dpkg::Use-Pty=0'
|
||||
$APT_PARAMS remove -y '^dotnet-.*'
|
||||
$APT_PARAMS remove -y '^llvm-.*'
|
||||
$APT_PARAMS remove -y '^php.*'
|
||||
$APT_PARAMS remove -y '^mongodb-.*'
|
||||
$APT_PARAMS remove -y '^mysql-.*'
|
||||
$APT_PARAMS remove -y azure-cli firefox powershell mono-devel libgl1-mesa-dri
|
||||
$APT_PARAMS autoremove --purge -y
|
||||
$APT_PARAMS autoclean
|
||||
$APT_PARAMS clean
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.ref }}
|
||||
persist-credentials: false
|
||||
- name: Set VERSION
|
||||
run: |
|
||||
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
|
||||
export VERSION=${GITHUB_REF_NAME:1}
|
||||
elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
|
||||
export VERSION="${REF}"
|
||||
else
|
||||
export VERSION=${GITHUB_SHA}
|
||||
fi
|
||||
|
||||
echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
|
||||
env:
|
||||
REF: ${{ needs.prepare.outputs.ref }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
with:
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
- name: Build
|
||||
id: build
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
pull: true
|
||||
load: ${{ !fromJson(needs.prepare.outputs.push) }}
|
||||
source: .
|
||||
targets: static-builder-gnu
|
||||
set: |
|
||||
${{ (github.event_name == 'pull_request' || matrix.platform == 'linux/arm64') && 'static-builder-gnu.args.NO_COMPRESS=1' || '' }}
|
||||
*.tags=
|
||||
*.platform=${{ matrix.platform }}
|
||||
*.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder-gnu
|
||||
*.cache-from=type=gha,scope=refs/heads/main-static-builder-gnu
|
||||
*.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder-gnu,ignore-error=true
|
||||
${{ fromJson(needs.prepare.outputs.push) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
|
||||
env:
|
||||
SHA: ${{ github.sha }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
|
||||
name: Export metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
run: |
|
||||
mkdir -p /tmp/metadata-gnu
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
digest=$(jq -r '."static-builder-gnu"."containerimage.digest"' <<< ${METADATA})
|
||||
touch "/tmp/metadata-gnu/${digest#sha256:}"
|
||||
env:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
- name: Upload metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: metadata-static-builder-gnu-${{ steps.prepare.outputs.sanitized_platform }}
|
||||
path: /tmp/metadata-gnu/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
- name: Copy all frankenphp* files
|
||||
run: |
|
||||
# shellcheck disable=SC2034
|
||||
digest=$(jq -r '."static-builder-gnu"."${{ fromJson(needs.prepare.outputs.push) && 'containerimage.digest' || 'containerimage.config.digest' }}"' <<< "${METADATA}")
|
||||
container_id=$(docker create --platform="${PLATFORM}" "${{ fromJson(needs.prepare.outputs.push) && '${IMAGE_NAME}@${digest}' || '${digest}' }}")
|
||||
mkdir -p gh-output
|
||||
cd gh-output
|
||||
for file in $(docker run --rm "${{ fromJson(needs.prepare.outputs.push) && '${IMAGE_NAME}@${digest}' || '${digest}' }}" sh -c "ls /go/src/app/dist | grep '^frankenphp'"); do
|
||||
docker cp "${container_id}:/go/src/app/dist/${file}" "./${file}"
|
||||
done
|
||||
docker rm "${container_id}"
|
||||
mv "${BINARY}" "${BINARY}-gnu"
|
||||
env:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
- name: Upload artifact
|
||||
if: ${{ !fromJson(needs.prepare.outputs.push) }}
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}-gnu-files
|
||||
path: gh-output/*
|
||||
- name: Upload assets
|
||||
if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
|
||||
run: gh release upload "${REF}" gh-output/* --repo dunglas/frankenphp --clobber
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REF: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}
|
||||
- if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
|
||||
uses: actions/attest-build-provenance@v3
|
||||
with:
|
||||
subject-path: ${{ github.workspace }}/gh-output/frankenphp-linux-*-gnu
|
||||
- name: Run sanity checks
|
||||
run: |
|
||||
"${BINARY}" version
|
||||
@@ -173,27 +366,36 @@ jobs:
|
||||
"${BINARY}" list-modules | grep http.handlers.mercure
|
||||
"${BINARY}" list-modules | grep http.handlers.mercure
|
||||
"${BINARY}" list-modules | grep http.handlers.vulcain
|
||||
"${BINARY}" php-cli -r "echo 'Sanity check passed';"
|
||||
env:
|
||||
BINARY: ./frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
|
||||
BINARY: ./gh-output/frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}-gnu
|
||||
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
push:
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- prepare
|
||||
- build-linux
|
||||
- build-linux-musl
|
||||
- build-linux-gnu
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
steps:
|
||||
- name: Download metadata
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: metadata-static-builder-*
|
||||
pattern: metadata-static-builder-musl-*
|
||||
path: /tmp/metadata
|
||||
merge-multiple: true
|
||||
- name: Download GNU metadata
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: metadata-static-builder-gnu-*
|
||||
path: /tmp/metadata-gnu
|
||||
merge-multiple: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
with:
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
@@ -201,71 +403,101 @@ jobs:
|
||||
working-directory: /tmp/metadata
|
||||
run: |
|
||||
# shellcheck disable=SC2046,SC2086
|
||||
docker buildx imagetools create $(jq -cr '.target."static-builder".tags | map("-t " + .) | join(" ")' <<< "${METADATA}") \
|
||||
docker buildx imagetools create $(jq -cr '.target."static-builder-musl".tags | map("-t " + .) | join(" ")' <<< "${METADATA}") \
|
||||
$(printf "${IMAGE_NAME}@sha256:%s " *)
|
||||
env:
|
||||
METADATA: ${{ needs.prepare.outputs.metadata }}
|
||||
- name: Create GNU manifest list and push
|
||||
working-directory: /tmp/metadata-gnu
|
||||
run: |
|
||||
# shellcheck disable=SC2046,SC2086
|
||||
docker buildx imagetools create $(jq -cr '.target."static-builder-gnu".tags | map("-t " + .) | join(" ")' <<< "${GNU_METADATA}") \
|
||||
$(printf "${IMAGE_NAME}@sha256:%s " *)
|
||||
env:
|
||||
GNU_METADATA: ${{ needs.prepare.outputs.gnu_metadata }}
|
||||
- name: Inspect image
|
||||
run: |
|
||||
# shellcheck disable=SC2046,SC2086
|
||||
docker buildx imagetools inspect "$(jq -cr '.target."static-builder".tags | first' <<< "${METADATA}")"
|
||||
docker buildx imagetools inspect "$(jq -cr '.target."static-builder-musl".tags | first' <<< "${METADATA}")"
|
||||
env:
|
||||
METADATA: ${{ needs.prepare.outputs.metadata }}
|
||||
- name: Inspect GNU image
|
||||
run: |
|
||||
# shellcheck disable=SC2046,SC2086
|
||||
docker buildx imagetools inspect "$(jq -cr '.target."static-builder-gnu".tags | first' <<< "${GNU_METADATA}")-gnu"
|
||||
env:
|
||||
GNU_METADATA: ${{ needs.prepare.outputs.gnu_metadata }}
|
||||
|
||||
build-mac:
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: ["arm64", "x86_64"]
|
||||
name: Build macOS ${{ matrix.platform }} binaries
|
||||
runs-on: ${{ matrix.platform == 'arm64' && 'macos-14' || 'macos-13' }}
|
||||
runs-on: ${{ matrix.platform == 'arm64' && 'macos-15' || 'macos-15-intel' }}
|
||||
needs: [prepare]
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.ref }}
|
||||
- uses: actions/setup-go@v5
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.25"
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
go.sum
|
||||
caddy/go.sum
|
||||
cache: false
|
||||
- name: Set FRANKENPHP_VERSION
|
||||
run: |
|
||||
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
|
||||
export FRANKENPHP_VERSION=${GITHUB_REF_NAME:1}
|
||||
elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
|
||||
export FRANKENPHP_VERSION="${{ needs.prepare.outputs.ref }}"
|
||||
export FRANKENPHP_VERSION="${REF}"
|
||||
else
|
||||
export FRANKENPHP_VERSION=${GITHUB_SHA}
|
||||
fi
|
||||
|
||||
echo "FRANKENPHP_VERSION=${FRANKENPHP_VERSION}" >> "${GITHUB_ENV}"
|
||||
env:
|
||||
REF: ${{ needs.prepare.outputs.ref }}
|
||||
- name: Build FrankenPHP
|
||||
run: ./build-static.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE: ${{ (needs.prepare.outputs.ref || github.ref_type == 'tag') && '1' || '' }}
|
||||
NO_COMPRESS: ${{ github.event_name == 'pull_request' && '1' || '' }}
|
||||
- name: Upload logs
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: dist/static-php-cli/log
|
||||
name: static-php-cli-log-${{ matrix.platform }}-${{ github.sha }}
|
||||
- if: needs.prepare.outputs.ref || github.ref_type == 'tag'
|
||||
uses: actions/attest-build-provenance@v2
|
||||
uses: actions/attest-build-provenance@v3
|
||||
with:
|
||||
subject-path: ${{ github.workspace }}/dist/frankenphp-mac-*
|
||||
- name: Upload artifact
|
||||
if: github.ref_type == 'branch'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: frankenphp-mac-${{ matrix.platform }}
|
||||
path: dist/frankenphp-mac-${{ matrix.platform }}
|
||||
- name: Run sanity checks
|
||||
run: |
|
||||
"${BINARY}" version
|
||||
"${BINARY}" build-info
|
||||
"${BINARY}" list-modules | grep frankenphp
|
||||
"${BINARY}" list-modules | grep http.encoders.br
|
||||
"${BINARY}" list-modules | grep http.handlers.mercure
|
||||
"${BINARY}" list-modules | grep http.handlers.mercure
|
||||
"${BINARY}" list-modules | grep http.handlers.vulcain
|
||||
"${BINARY}" php-cli -r "echo 'Sanity check passed';"
|
||||
env:
|
||||
BINARY: dist/frankenphp-mac-${{ matrix.platform }}
|
||||
|
||||
122
.github/workflows/tests.yaml
vendored
122
.github/workflows/tests.yaml
vendored
@@ -1,5 +1,8 @@
|
||||
---
|
||||
name: Tests
|
||||
concurrency:
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -13,22 +16,32 @@ on:
|
||||
- "docs/**"
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
GOTOOLCHAIN: local
|
||||
GOEXPERIMENT: cgocheck2
|
||||
jobs:
|
||||
tests:
|
||||
tests-linux:
|
||||
name: Tests (Linux, PHP ${{ matrix.php-versions }})
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: false
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: ["8.2", "8.3", "8.4"]
|
||||
include:
|
||||
- php-versions: "8.2"
|
||||
- php-versions: "8.3"
|
||||
- php-versions: "8.4"
|
||||
- php-versions: "8.5"
|
||||
env:
|
||||
GOEXPERIMENT: cgocheck2
|
||||
GOMAXPROCS: 10
|
||||
LIBRARY_PATH: ${{ github.workspace }}/watcher/target/lib
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
go-version: "1.24"
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
caddy/go.sum
|
||||
@@ -43,15 +56,17 @@ jobs:
|
||||
debug: true
|
||||
- name: Install e-dant/watcher
|
||||
uses: ./.github/actions/watcher
|
||||
- name: Set Set CGO flags
|
||||
- name: Set CGO flags
|
||||
run: echo "CGO_CFLAGS=-I${PWD}/watcher/target/include $(php-config --includes)" >> "${GITHUB_ENV}"
|
||||
- name: Build
|
||||
run: go build
|
||||
- name: Build testcli binary
|
||||
working-directory: internal/testcli/
|
||||
run: go build
|
||||
- name: Compile library tests
|
||||
run: go test -race -v -x -c
|
||||
- name: Run library tests
|
||||
run: go test -race -v ./...
|
||||
run: ./frankenphp.test -test.v
|
||||
- name: Run Caddy module tests
|
||||
working-directory: caddy/
|
||||
run: go test -tags nobadger,nomysql,nopgx -race -v ./...
|
||||
@@ -67,7 +82,94 @@ jobs:
|
||||
- name: Run integrations tests
|
||||
run: ./reload_test.sh
|
||||
- name: Lint Go code
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
if: matrix.php-versions == '8.4'
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
if: matrix.php-versions == '8.5'
|
||||
with:
|
||||
version: latest
|
||||
- name: Ensure go.mod is tidy
|
||||
if: matrix.php-versions == '8.5'
|
||||
run: go mod tidy -diff
|
||||
- name: Ensure caddy/go.mod is tidy
|
||||
if: matrix.php-versions == '8.5'
|
||||
run: go mod tidy -diff
|
||||
working-directory: caddy/
|
||||
integration-tests:
|
||||
name: Integration Tests (Linux, PHP ${{ matrix.php-versions }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: ["8.3", "8.4", "8.5"]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
caddy/go.sum
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
ini-file: development
|
||||
coverage: none
|
||||
tools: none
|
||||
env:
|
||||
phpts: ts
|
||||
debug: true
|
||||
- name: Install PHP development libraries
|
||||
run: sudo apt-get update && sudo apt-get install -y libkrb5-dev libsodium-dev libargon2-dev
|
||||
- name: Install xcaddy
|
||||
run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
||||
- name: Download PHP sources
|
||||
run: |
|
||||
PHP_VERSION=$(php -r "echo PHP_VERSION;")
|
||||
wget -q "https://www.php.net/distributions/php-${PHP_VERSION}.tar.gz"
|
||||
tar xzf "php-${PHP_VERSION}.tar.gz"
|
||||
echo "GEN_STUB_SCRIPT=${PWD}/php-${PHP_VERSION}/build/gen_stub.php" >> "${GITHUB_ENV}"
|
||||
- name: Set CGO flags
|
||||
run: |
|
||||
echo "CGO_CFLAGS=$(php-config --includes)" >> "${GITHUB_ENV}"
|
||||
echo "CGO_LDFLAGS=$(php-config --ldflags) $(php-config --libs)" >> "${GITHUB_ENV}"
|
||||
- name: Run integration tests
|
||||
working-directory: internal/extgen/
|
||||
run: go test -tags integration -v -timeout 30m
|
||||
tests-mac:
|
||||
name: Tests (macOS, PHP 8.5)
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
caddy/go.sum
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.5
|
||||
ini-file: development
|
||||
coverage: none
|
||||
tools: none
|
||||
env:
|
||||
phpts: ts
|
||||
debug: true
|
||||
- name: Set Set CGO flags
|
||||
run: |
|
||||
{
|
||||
echo "CGO_CFLAGS=-I/opt/homebrew/include/ $(php-config --includes)"
|
||||
echo "CGO_LDFLAGS=-L/opt/homebrew/lib/ $(php-config --ldflags) $(php-config --libs)"
|
||||
} >> "${GITHUB_ENV}"
|
||||
- name: Build
|
||||
run: go build -tags nowatcher
|
||||
- name: Run library tests
|
||||
run: go test -tags nowatcher -race -v ./...
|
||||
- name: Run Caddy module tests
|
||||
working-directory: caddy/
|
||||
run: go test -tags nowatcher,nobadger,nomysql,nopgx -race -v ./...
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,7 +2,11 @@
|
||||
/internal/testserver/testserver
|
||||
/internal/testcli/testcli
|
||||
/dist
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
__debug_bin
|
||||
frankenphp.test
|
||||
caddy/frankenphp/Build
|
||||
package/etc/php.ini
|
||||
*.log
|
||||
|
||||
1
.gitleaksignore
Normal file
1
.gitleaksignore
Normal file
@@ -0,0 +1 @@
|
||||
/github/workspace/docs/mercure.md:jwt:88
|
||||
7
.golangci.yaml
Normal file
7
.golangci.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
version: "2"
|
||||
run:
|
||||
build-tags:
|
||||
- nobadger
|
||||
- nomysql
|
||||
- nopgx
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
no-hard-tabs: false
|
||||
MD010: false
|
||||
MD013: false
|
||||
MD033: false
|
||||
MD060: false
|
||||
|
||||
101
CONTRIBUTING.md
101
CONTRIBUTING.md
@@ -11,9 +11,13 @@ docker build -t frankenphp-dev -f dev.Dockerfile .
|
||||
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev
|
||||
```
|
||||
|
||||
The image contains the usual development tools (Go, GDB, Valgrind, Neovim...).
|
||||
The image contains the usual development tools (Go, GDB, Valgrind, Neovim...) and uses the following php setting locations
|
||||
|
||||
If docker version is lower than 23.0, build is failed by dockerignore [pattern issue](https://github.com/moby/moby/pull/42676). Add directories to `.dockerignore`.
|
||||
- php.ini: `/etc/frankenphp/php.ini` A php.ini file with development presets is provided by default.
|
||||
- additional configuration files: `/etc/frankenphp/php.d/*.ini`
|
||||
- php extensions: `/usr/lib/frankenphp/modules/`
|
||||
|
||||
If your Docker version is lower than 23.0, the build will fail due to dockerignore [pattern issue](https://github.com/moby/moby/pull/42676). Add directories to `.dockerignore`.
|
||||
|
||||
```patch
|
||||
!testdata/*.php
|
||||
@@ -111,22 +115,22 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
|
||||
|
||||
1. Download the debug version of the FrankenPHP binary from GitHub or create your custom static build including debug symbols:
|
||||
|
||||
```console
|
||||
docker buildx bake \
|
||||
--load \
|
||||
--set static-builder.args.DEBUG_SYMBOLS=1 \
|
||||
--set "static-builder.platform=linux/amd64" \
|
||||
static-builder
|
||||
docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
|
||||
```
|
||||
```console
|
||||
docker buildx bake \
|
||||
--load \
|
||||
--set static-builder.args.DEBUG_SYMBOLS=1 \
|
||||
--set "static-builder.platform=linux/amd64" \
|
||||
static-builder
|
||||
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
|
||||
```
|
||||
|
||||
2. Replace your current version of `frankenphp` by the debug FrankenPHP executable
|
||||
3. Start FrankenPHP as usual (alternatively, you can directly start FrankenPHP with GDB: `gdb --args frankenphp run`)
|
||||
4. Attach to the process with GDB:
|
||||
|
||||
```console
|
||||
gdb -p `pidof frankenphp`
|
||||
```
|
||||
```console
|
||||
gdb -p `pidof frankenphp`
|
||||
```
|
||||
|
||||
5. If necessary, type `continue` in the GDB shell
|
||||
6. Make FrankenPHP crash
|
||||
@@ -138,63 +142,60 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
|
||||
1. Open `.github/workflows/tests.yml`
|
||||
2. Enable PHP debug symbols
|
||||
|
||||
```patch
|
||||
- uses: shivammathur/setup-php@v2
|
||||
# ...
|
||||
env:
|
||||
phpts: ts
|
||||
+ debug: true
|
||||
```
|
||||
```patch
|
||||
- uses: shivammathur/setup-php@v2
|
||||
# ...
|
||||
env:
|
||||
phpts: ts
|
||||
+ debug: true
|
||||
```
|
||||
|
||||
3. Enable `tmate` to connect to the container
|
||||
|
||||
```patch
|
||||
-
|
||||
name: Set CGO flags
|
||||
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
|
||||
+ -
|
||||
+ run: |
|
||||
+ sudo apt install gdb
|
||||
+ mkdir -p /home/runner/.config/gdb/
|
||||
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
|
||||
+ -
|
||||
+ uses: mxschmitt/action-tmate@v3
|
||||
```
|
||||
```patch
|
||||
- name: Set CGO flags
|
||||
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
|
||||
+ - run: |
|
||||
+ sudo apt install gdb
|
||||
+ mkdir -p /home/runner/.config/gdb/
|
||||
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
|
||||
+ - uses: mxschmitt/action-tmate@v3
|
||||
```
|
||||
|
||||
4. Connect to the container
|
||||
5. Open `frankenphp.go`
|
||||
6. Enable `cgosymbolizer`
|
||||
|
||||
```patch
|
||||
- //_ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
+ _ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
```
|
||||
```patch
|
||||
- //_ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
+ _ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
```
|
||||
|
||||
7. Download the module: `go get`
|
||||
8. In the container, you can use GDB and the like:
|
||||
|
||||
```console
|
||||
go test -tags watcher -c -ldflags=-w
|
||||
gdb --args frankenphp.test -test.run ^MyTest$
|
||||
```
|
||||
```console
|
||||
go test -tags watcher -c -ldflags=-w
|
||||
gdb --args frankenphp.test -test.run ^MyTest$
|
||||
```
|
||||
|
||||
9. When the bug is fixed, revert all these changes
|
||||
|
||||
## Misc Dev Resources
|
||||
|
||||
* [PHP embedding in uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
|
||||
* [PHP embedding in NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
|
||||
* [PHP embedding in Go (go-php)](https://github.com/deuill/go-php)
|
||||
* [PHP embedding in Go (GoEmPHP)](https://github.com/mikespook/goemphp)
|
||||
* [PHP embedding in C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
|
||||
* [Extending and Embedding PHP by Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
|
||||
* [What the heck is TSRMLS_CC, anyway?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
|
||||
* [SDL bindings](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)
|
||||
- [PHP embedding in uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
|
||||
- [PHP embedding in NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
|
||||
- [PHP embedding in Go (go-php)](https://github.com/deuill/go-php)
|
||||
- [PHP embedding in Go (GoEmPHP)](https://github.com/mikespook/goemphp)
|
||||
- [PHP embedding in C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
|
||||
- [Extending and Embedding PHP by Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
|
||||
- [What the heck is TSRMLS_CC, anyway?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
|
||||
- [SDL bindings](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)
|
||||
|
||||
## Docker-Related Resources
|
||||
|
||||
* [Bake file definition](https://docs.docker.com/build/customize/bake/file-definition/)
|
||||
* [docker buildx build](https://docs.docker.com/engine/reference/commandline/buildx_build/)
|
||||
- [Bake file definition](https://docs.docker.com/build/customize/bake/file-definition/)
|
||||
- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)
|
||||
|
||||
## Useful Command
|
||||
|
||||
|
||||
71
Dockerfile
71
Dockerfile
@@ -19,17 +19,19 @@ RUN set -eux; \
|
||||
/app/public \
|
||||
/config/caddy \
|
||||
/data/caddy \
|
||||
/etc/caddy; \
|
||||
/etc/caddy \
|
||||
/etc/frankenphp; \
|
||||
sed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint; \
|
||||
echo '<?php phpinfo();' > /app/public/index.php
|
||||
|
||||
COPY --link caddy/frankenphp/Caddyfile /etc/caddy/Caddyfile
|
||||
RUN curl -sSLf \
|
||||
RUN ln /etc/caddy/Caddyfile /etc/frankenphp/Caddyfile && \
|
||||
curl -sSLf \
|
||||
-o /usr/local/bin/install-php-extensions \
|
||||
https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \
|
||||
chmod +x /usr/local/bin/install-php-extensions
|
||||
|
||||
CMD ["--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||
CMD ["--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"]
|
||||
HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1
|
||||
|
||||
# See https://caddyserver.com/docs/conventions#file-locations for details
|
||||
@@ -44,7 +46,7 @@ EXPOSE 2019
|
||||
LABEL org.opencontainers.image.title=FrankenPHP
|
||||
LABEL org.opencontainers.image.description="The modern PHP app server"
|
||||
LABEL org.opencontainers.image.url=https://frankenphp.dev
|
||||
LABEL org.opencontainers.image.source=https://github.com/dunglas/frankenphp
|
||||
LABEL org.opencontainers.image.source=https://github.com/php/frankenphp
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
||||
LABEL org.opencontainers.image.vendor="Kévin Dunglas"
|
||||
|
||||
@@ -57,11 +59,13 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
COPY --from=golang-base /usr/local/go /usr/local/go
|
||||
|
||||
ENV PATH=/usr/local/go/bin:$PATH
|
||||
ENV GOTOOLCHAIN=local
|
||||
|
||||
# This is required to link the FrankenPHP binary to the PHP binary
|
||||
RUN apt-get update && \
|
||||
apt-get -y --no-install-recommends install \
|
||||
cmake \
|
||||
cmake \
|
||||
git \
|
||||
libargon2-dev \
|
||||
libbrotli-dev \
|
||||
libcurl4-openssl-dev \
|
||||
@@ -75,47 +79,49 @@ RUN apt-get update && \
|
||||
&& \
|
||||
apt-get clean
|
||||
|
||||
# Install e-dant/watcher (necessary for file watching)
|
||||
WORKDIR /usr/local/src/watcher
|
||||
RUN --mount=type=secret,id=github-token \
|
||||
if [ -f /run/secrets/github-token ] && [ -s /run/secrets/github-token ]; then \
|
||||
curl -s -H "Authorization: Bearer $(cat /run/secrets/github-token)" https://api.github.com/repos/e-dant/watcher/releases/latest; \
|
||||
else \
|
||||
curl -s https://api.github.com/repos/e-dant/watcher/releases/latest; \
|
||||
fi | \
|
||||
grep tarball_url | \
|
||||
awk '{ print $2 }' | \
|
||||
sed 's/,$//' | \
|
||||
sed 's/"//g' | \
|
||||
xargs curl -L | \
|
||||
tar xz --strip-components 1 && \
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
|
||||
cmake --build build && \
|
||||
cmake --install build && \
|
||||
ldconfig
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
COPY --link go.mod go.sum ./
|
||||
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
RUN go mod download
|
||||
|
||||
WORKDIR /go/src/app/caddy
|
||||
COPY --link caddy/go.mod caddy/go.sum ./
|
||||
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
RUN go mod download
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY --link *.* ./
|
||||
COPY --link caddy caddy
|
||||
COPY --link internal internal
|
||||
COPY --link testdata testdata
|
||||
COPY --link . ./
|
||||
|
||||
# Install e-dant/watcher (necessary for file watching)
|
||||
WORKDIR /usr/local/src/watcher
|
||||
RUN curl -s https://api.github.com/repos/e-dant/watcher/releases/latest | \
|
||||
grep tarball_url | \
|
||||
awk '{ print $2 }' | \
|
||||
sed 's/,$//' | \
|
||||
sed 's/"//g' | \
|
||||
xargs curl -L | \
|
||||
tar xz --strip-components 1 && \
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
|
||||
cmake --build build && \
|
||||
cmake --install build && \
|
||||
ldconfig
|
||||
|
||||
# See https://github.com/docker-library/php/blob/master/8.3/bookworm/zts/Dockerfile#L57-L59 for PHP values
|
||||
# See https://github.com/docker-library/php/blob/master/8.5/trixie/zts/Dockerfile#L57-L59 for PHP values
|
||||
ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS"
|
||||
ENV CGO_CPPFLAGS=$PHP_CPPFLAGS
|
||||
ENV CGO_LDFLAGS="-L/usr/local/lib -lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS"
|
||||
|
||||
RUN echo $CGO_LDFLAGS
|
||||
|
||||
WORKDIR /go/src/app/caddy/frankenphp
|
||||
RUN GOBIN=/usr/local/bin go install -tags 'nobadger,nomysql,nopgx' -ldflags "-w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" && \
|
||||
RUN GOBIN=/usr/local/bin \
|
||||
../../go.sh install -ldflags "-w -s -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" -buildvcs=true && \
|
||||
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
|
||||
cp Caddyfile /etc/caddy/Caddyfile && \
|
||||
frankenphp version
|
||||
cp Caddyfile /etc/frankenphp/Caddyfile && \
|
||||
frankenphp version && \
|
||||
frankenphp build-info
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
@@ -133,4 +139,5 @@ RUN apt-get install -y --no-install-recommends libstdc++6 && \
|
||||
|
||||
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
|
||||
RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
|
||||
frankenphp version
|
||||
frankenphp version && \
|
||||
frankenphp build-info
|
||||
|
||||
156
README.md
156
README.md
@@ -4,33 +4,89 @@
|
||||
|
||||
FrankenPHP is a modern application server for PHP built on top of the [Caddy](https://caddyserver.com/) web server.
|
||||
|
||||
FrankenPHP gives superpowers to your PHP apps thanks to its stunning features: [*Early Hints*](https://frankenphp.dev/docs/early-hints/), [worker mode](https://frankenphp.dev/docs/worker/), [real-time capabilities](https://frankenphp.dev/docs/mercure/), automatic HTTPS, HTTP/2, and HTTP/3 support...
|
||||
FrankenPHP gives superpowers to your PHP apps thanks to its stunning features: [_Early Hints_](https://frankenphp.dev/docs/early-hints/), [worker mode](https://frankenphp.dev/docs/worker/), [real-time capabilities](https://frankenphp.dev/docs/mercure/), automatic HTTPS, HTTP/2, and HTTP/3 support...
|
||||
|
||||
FrankenPHP works with any PHP app and makes your Laravel and Symfony projects faster than ever thanks to their official integrations with the worker mode.
|
||||
|
||||
FrankenPHP can also be used as a standalone Go library to embed PHP in any app using `net/http`.
|
||||
|
||||
[**Learn more** on *frankenphp.dev*](https://frankenphp.dev) and in this slide deck:
|
||||
[**Learn more** on _frankenphp.dev_](https://frankenphp.dev) and in this slide deck:
|
||||
|
||||
<a href="https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/"><img src="https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png" alt="Slides" width="600"></a>
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Standalone Binary
|
||||
|
||||
We provide static FrankenPHP binaries for Linux and macOS
|
||||
containing [PHP 8.4](https://www.php.net/releases/8.4/en.php) and most popular PHP extensions.
|
||||
|
||||
On Windows, use [WSL](https://learn.microsoft.com/windows/wsl/) to run FrankenPHP.
|
||||
|
||||
[Download FrankenPHP](https://github.com/dunglas/frankenphp/releases) or copy this line into your
|
||||
terminal to automatically install the version appropriate for your platform:
|
||||
### Install Script
|
||||
|
||||
You can copy this line into your terminal to automatically
|
||||
install an appropriate version for your platform:
|
||||
|
||||
```console
|
||||
curl https://frankenphp.dev/install.sh | sh
|
||||
mv frankenphp /usr/local/bin/
|
||||
```
|
||||
|
||||
### Standalone Binary
|
||||
|
||||
We provide static FrankenPHP binaries for development purposes on Linux and macOS
|
||||
containing [PHP 8.4](https://www.php.net/releases/8.4/en.php) and most popular PHP extensions.
|
||||
|
||||
[Download FrankenPHP](https://github.com/php/frankenphp/releases)
|
||||
|
||||
**Installing extensions:** Most common extensions are bundled. It's not possible to install more extensions.
|
||||
|
||||
### rpm Packages
|
||||
|
||||
Our maintainers offer rpm packages for all systems using `dnf`. To install, run:
|
||||
|
||||
```console
|
||||
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
|
||||
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 available
|
||||
sudo dnf install frankenphp
|
||||
```
|
||||
|
||||
**Installing extensions:** `sudo dnf install php-zts-<extension>`
|
||||
|
||||
For extensions not available by default, use [PIE](https://github.com/php/pie):
|
||||
|
||||
```console
|
||||
sudo dnf install pie-zts
|
||||
sudo pie-zts install asgrim/example-pie-extension
|
||||
```
|
||||
|
||||
### deb Packages
|
||||
|
||||
Our maintainers offer deb packages for all systems using `apt`. To install, run:
|
||||
|
||||
```console
|
||||
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" | sudo tee /etc/apt/sources.list.d/static-php.list && \
|
||||
sudo apt update
|
||||
sudo apt install frankenphp
|
||||
```
|
||||
|
||||
**Installing extensions:** `sudo apt install php-zts-<extension>`
|
||||
|
||||
For extensions not available by default, use [PIE](https://github.com/php/pie):
|
||||
|
||||
```console
|
||||
sudo apt install pie-zts
|
||||
sudo pie-zts install asgrim/example-pie-extension
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
|
||||
FrankenPHP is also available as a [Homebrew](https://brew.sh) package for macOS and Linux.
|
||||
|
||||
```console
|
||||
brew install dunglas/frankenphp/frankenphp
|
||||
```
|
||||
|
||||
**Installing extensions:** Use [PIE](https://github.com/php/pie).
|
||||
|
||||
### Usage
|
||||
|
||||
To serve the content of the current directory, run:
|
||||
|
||||
```console
|
||||
@@ -43,11 +99,11 @@ You can also run command-line scripts with:
|
||||
frankenphp php-cli /path/to/your/script.php
|
||||
```
|
||||
|
||||
> ![WARNING]
|
||||
>
|
||||
> In production, prefer using [the Docker images](#docker), [the Brew package](#homebrew)
|
||||
> or [compiling FrankenPHP from sources](https://frankenphp.dev/docs/compile/).
|
||||
> The standalone binary is provided for development and testing purposes.
|
||||
For the deb and rpm packages, you can also start the systemd service:
|
||||
|
||||
```console
|
||||
sudo systemctl start frankenphp
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
@@ -66,50 +122,36 @@ Go to `https://localhost`, and enjoy!
|
||||
> Do not attempt to use `https://127.0.0.1`. Use `https://localhost` and accept the self-signed certificate.
|
||||
> Use the [`SERVER_NAME` environment variable](docs/config.md#environment-variables) to change the domain to use.
|
||||
|
||||
### Homebrew
|
||||
|
||||
FrankenPHP is also available as a [Homebrew](https://brew.sh) package for macOS and Linux.
|
||||
|
||||
To install it:
|
||||
|
||||
```console
|
||||
brew install dunglas/frankenphp/frankenphp
|
||||
```
|
||||
|
||||
To serve the content of the current directory, run:
|
||||
|
||||
```console
|
||||
frankenphp php-server
|
||||
```
|
||||
|
||||
## Docs
|
||||
|
||||
* [Classic mode](https://frankenphp.dev/docs/classic/)
|
||||
* [Worker mode](https://frankenphp.dev/docs/worker/)
|
||||
* [Early Hints support (103 HTTP status code)](https://frankenphp.dev/docs/early-hints/)
|
||||
* [Real-time](https://frankenphp.dev/docs/mercure/)
|
||||
* [Configuration](https://frankenphp.dev/docs/config/)
|
||||
* [Docker images](https://frankenphp.dev/docs/docker/)
|
||||
* [Deploy in production](https://frankenphp.dev/docs/production/)
|
||||
* [Performance optimization](https://frankenphp.dev/docs/performance/)
|
||||
* [Create **standalone**, self-executable PHP apps](https://frankenphp.dev/docs/embed/)
|
||||
* [Create static binaries](https://frankenphp.dev/docs/static/)
|
||||
* [Compile from sources](https://frankenphp.dev/docs/compile/)
|
||||
* [Monitoring FrankenPHP](https://frankenphp.dev/docs/metrics/)
|
||||
* [Laravel integration](https://frankenphp.dev/docs/laravel/)
|
||||
* [Known issues](https://frankenphp.dev/docs/known-issues/)
|
||||
* [Demo app (Symfony) and benchmarks](https://github.com/dunglas/frankenphp-demo)
|
||||
* [Go library documentation](https://pkg.go.dev/github.com/dunglas/frankenphp)
|
||||
* [Contributing and debugging](https://frankenphp.dev/docs/contributing/)
|
||||
- [Classic mode](https://frankenphp.dev/docs/classic/)
|
||||
- [Worker mode](https://frankenphp.dev/docs/worker/)
|
||||
- [Early Hints support (103 HTTP status code)](https://frankenphp.dev/docs/early-hints/)
|
||||
- [Real-time](https://frankenphp.dev/docs/mercure/)
|
||||
- [Efficiently Serving Large Static Files](https://frankenphp.dev/docs/x-sendfile/)
|
||||
- [Configuration](https://frankenphp.dev/docs/config/)
|
||||
- [Writing PHP Extensions in Go](https://frankenphp.dev/docs/extensions/)
|
||||
- [Docker images](https://frankenphp.dev/docs/docker/)
|
||||
- [Deploy in production](https://frankenphp.dev/docs/production/)
|
||||
- [Performance optimization](https://frankenphp.dev/docs/performance/)
|
||||
- [Create **standalone**, self-executable PHP apps](https://frankenphp.dev/docs/embed/)
|
||||
- [Create static binaries](https://frankenphp.dev/docs/static/)
|
||||
- [Compile from sources](https://frankenphp.dev/docs/compile/)
|
||||
- [Monitoring FrankenPHP](https://frankenphp.dev/docs/metrics/)
|
||||
- [Laravel integration](https://frankenphp.dev/docs/laravel/)
|
||||
- [Known issues](https://frankenphp.dev/docs/known-issues/)
|
||||
- [Demo app (Symfony) and benchmarks](https://github.com/dunglas/frankenphp-demo)
|
||||
- [Go library documentation](https://pkg.go.dev/github.com/dunglas/frankenphp)
|
||||
- [Contributing and debugging](https://frankenphp.dev/docs/contributing/)
|
||||
|
||||
## Examples and Skeletons
|
||||
|
||||
* [Symfony](https://github.com/dunglas/symfony-docker)
|
||||
* [API Platform](https://api-platform.com/docs/symfony)
|
||||
* [Laravel](https://frankenphp.dev/docs/laravel/)
|
||||
* [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
|
||||
* [WordPress](https://github.com/StephenMiracle/frankenwp)
|
||||
* [Drupal](https://github.com/dunglas/frankenphp-drupal)
|
||||
* [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
|
||||
* [TYPO3](https://github.com/ochorocho/franken-typo3)
|
||||
* [Magento2](https://github.com/ekino/frankenphp-magento2)
|
||||
- [Symfony](https://github.com/dunglas/symfony-docker)
|
||||
- [API Platform](https://api-platform.com/docs/symfony)
|
||||
- [Laravel](https://frankenphp.dev/docs/laravel/)
|
||||
- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
|
||||
- [WordPress](https://github.com/StephenMiracle/frankenwp)
|
||||
- [Drupal](https://github.com/dunglas/frankenphp-drupal)
|
||||
- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
|
||||
- [TYPO3](https://github.com/ochorocho/franken-typo3)
|
||||
- [Magento2](https://github.com/ekino/frankenphp-magento2)
|
||||
|
||||
@@ -12,7 +12,7 @@ Binaries and Docker images are rebuilt nightly using the latest versions of depe
|
||||
If you believe you have discovered a security issue directly affecting FrankenPHP,
|
||||
please do **NOT** report it publicly.
|
||||
|
||||
Please write a detailed vulnerability report and send it [through GitHub](https://github.com/dunglas/frankenphp/security/advisories/new) or to [kevin+frankenphp-security@dunglas.dev](mailto:kevin+frankenphp-security@dunglas.dev?subject=Security%20issue%20affecting%20FrankenPHP).
|
||||
Please write a detailed vulnerability report and send it [through GitHub](https://github.com/php/frankenphp/security/advisories/new) or to [kevin+frankenphp-security@dunglas.dev](mailto:kevin+frankenphp-security@dunglas.dev?subject=Security%20issue%20affecting%20FrankenPHP).
|
||||
|
||||
Only vulnerabilities directly affecting FrankenPHP should be reported to this project.
|
||||
Flaws affecting components used by FrankenPHP (PHP, Caddy, Go...) or using FrankenPHP (Laravel Octane, PHP Runtime...) should be reported to the relevant projects.
|
||||
|
||||
@@ -18,17 +18,20 @@ RUN set -eux; \
|
||||
/app/public \
|
||||
/config/caddy \
|
||||
/data/caddy \
|
||||
/etc/caddy; \
|
||||
/etc/caddy \
|
||||
/etc/frankenphp; \
|
||||
sed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint; \
|
||||
echo '<?php phpinfo();' > /app/public/index.php
|
||||
|
||||
COPY --link caddy/frankenphp/Caddyfile /etc/caddy/Caddyfile
|
||||
RUN curl -sSLf \
|
||||
|
||||
RUN ln /etc/caddy/Caddyfile /etc/frankenphp/Caddyfile && \
|
||||
curl -sSLf \
|
||||
-o /usr/local/bin/install-php-extensions \
|
||||
https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \
|
||||
chmod +x /usr/local/bin/install-php-extensions
|
||||
|
||||
CMD ["--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||
CMD ["--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"]
|
||||
HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1
|
||||
|
||||
# See https://caddyserver.com/docs/conventions#file-locations for details
|
||||
@@ -43,7 +46,7 @@ EXPOSE 2019
|
||||
LABEL org.opencontainers.image.title=FrankenPHP
|
||||
LABEL org.opencontainers.image.description="The modern PHP app server"
|
||||
LABEL org.opencontainers.image.url=https://frankenphp.dev
|
||||
LABEL org.opencontainers.image.source=https://github.com/dunglas/frankenphp
|
||||
LABEL org.opencontainers.image.source=https://github.com/php/frankenphp
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
||||
LABEL org.opencontainers.image.vendor="Kévin Dunglas"
|
||||
|
||||
@@ -57,17 +60,18 @@ SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
|
||||
COPY --link --from=golang-base /usr/local/go /usr/local/go
|
||||
|
||||
ENV PATH=/usr/local/go/bin:$PATH
|
||||
ENV GOTOOLCHAIN=local
|
||||
|
||||
# hadolint ignore=SC2086
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
$PHPIZE_DEPS \
|
||||
argon2-dev \
|
||||
# Needed for the custom Go build
|
||||
# Needed for the custom Go build \
|
||||
bash \
|
||||
brotli-dev \
|
||||
coreutils \
|
||||
curl-dev \
|
||||
# Needed for the custom Go build
|
||||
# Needed for the custom Go build \
|
||||
git \
|
||||
gnu-libiconv-dev \
|
||||
libsodium-dev \
|
||||
@@ -82,33 +86,35 @@ RUN apk add --no-cache --virtual .build-deps \
|
||||
sqlite-dev \
|
||||
upx
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
COPY --link go.mod go.sum ./
|
||||
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
|
||||
WORKDIR /go/src/app/caddy
|
||||
COPY caddy/go.mod caddy/go.sum ./
|
||||
RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY --link *.* ./
|
||||
COPY --link caddy caddy
|
||||
COPY --link internal internal
|
||||
COPY --link testdata testdata
|
||||
|
||||
# Install e-dant/watcher (necessary for file watching)
|
||||
WORKDIR /usr/local/src/watcher
|
||||
RUN curl -s https://api.github.com/repos/e-dant/watcher/releases/latest | \
|
||||
RUN --mount=type=secret,id=github-token \
|
||||
if [ -f /run/secrets/github-token ] && [ -s /run/secrets/github-token ]; then \
|
||||
curl -s -H "Authorization: Bearer $(cat /run/secrets/github-token)" https://api.github.com/repos/e-dant/watcher/releases/latest; \
|
||||
else \
|
||||
curl -s https://api.github.com/repos/e-dant/watcher/releases/latest; \
|
||||
fi | \
|
||||
grep tarball_url | \
|
||||
awk '{ print $2 }' | \
|
||||
sed 's/,$//' | \
|
||||
sed 's/"//g' | \
|
||||
xargs curl -L | \
|
||||
tar xz --strip-components 1 && \
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
|
||||
cmake --build build && \
|
||||
cmake --install build
|
||||
tar xz --strip-components 1 && \
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
|
||||
cmake --build build && \
|
||||
cmake --install build
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
COPY --link go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
WORKDIR /go/src/app/caddy
|
||||
COPY caddy/go.mod caddy/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY --link . ./
|
||||
|
||||
# See https://github.com/docker-library/php/blob/master/8.3/alpine3.20/zts/Dockerfile#L53-L55
|
||||
ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS"
|
||||
@@ -116,10 +122,12 @@ ENV CGO_CPPFLAGS=$PHP_CPPFLAGS
|
||||
ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS"
|
||||
|
||||
WORKDIR /go/src/app/caddy/frankenphp
|
||||
RUN GOBIN=/usr/local/bin go install -tags 'nobadger,nomysql,nopgx' -ldflags "-w -s -extldflags '-Wl,-z,stack-size=0x80000' -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" && \
|
||||
RUN GOBIN=/usr/local/bin \
|
||||
../../go.sh install -ldflags "-w -s -extldflags '-Wl,-z,stack-size=0x80000' -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" -buildvcs=true && \
|
||||
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
|
||||
([ -z "${NO_COMPRESS}" ] && upx --best /usr/local/bin/frankenphp || true) && \
|
||||
frankenphp version
|
||||
frankenphp version && \
|
||||
frankenphp build-info
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
@@ -135,4 +143,5 @@ RUN apk add --no-cache libstdc++ && \
|
||||
|
||||
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
|
||||
RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
|
||||
frankenphp version
|
||||
frankenphp version && \
|
||||
frankenphp build-info
|
||||
|
||||
51
backoff.go
51
backoff.go
@@ -1,51 +0,0 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type exponentialBackoff struct {
|
||||
backoff time.Duration
|
||||
failureCount int
|
||||
mu sync.RWMutex
|
||||
maxBackoff time.Duration
|
||||
minBackoff time.Duration
|
||||
maxConsecutiveFailures int
|
||||
}
|
||||
|
||||
// recordSuccess resets the backoff and failureCount
|
||||
func (e *exponentialBackoff) recordSuccess() {
|
||||
e.mu.Lock()
|
||||
e.failureCount = 0
|
||||
e.backoff = e.minBackoff
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// recordFailure increments the failure count and increases the backoff, it returns true if maxConsecutiveFailures has been reached
|
||||
func (e *exponentialBackoff) recordFailure() bool {
|
||||
e.mu.Lock()
|
||||
e.failureCount += 1
|
||||
if e.backoff < e.minBackoff {
|
||||
e.backoff = e.minBackoff
|
||||
}
|
||||
|
||||
e.backoff = min(e.backoff*2, e.maxBackoff)
|
||||
|
||||
e.mu.Unlock()
|
||||
return e.failureCount >= e.maxConsecutiveFailures
|
||||
}
|
||||
|
||||
// wait sleeps for the backoff duration if failureCount is non-zero.
|
||||
// NOTE: this is not tested and should be kept 'obviously correct' (i.e., simple)
|
||||
func (e *exponentialBackoff) wait() {
|
||||
e.mu.RLock()
|
||||
if e.failureCount == 0 {
|
||||
e.mu.RUnlock()
|
||||
|
||||
return
|
||||
}
|
||||
e.mu.RUnlock()
|
||||
|
||||
time.Sleep(e.backoff)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestExponentialBackoff_Reset(t *testing.T) {
|
||||
e := &exponentialBackoff{
|
||||
maxBackoff: 5 * time.Second,
|
||||
minBackoff: 500 * time.Millisecond,
|
||||
maxConsecutiveFailures: 3,
|
||||
}
|
||||
|
||||
assert.False(t, e.recordFailure())
|
||||
assert.False(t, e.recordFailure())
|
||||
e.recordSuccess()
|
||||
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
assert.Equal(t, 0, e.failureCount, "expected failureCount to be reset to 0")
|
||||
assert.Equal(t, e.backoff, e.minBackoff, "expected backoff to be reset to minBackoff")
|
||||
}
|
||||
|
||||
func TestExponentialBackoff_Trigger(t *testing.T) {
|
||||
e := &exponentialBackoff{
|
||||
maxBackoff: 500 * 3 * time.Millisecond,
|
||||
minBackoff: 500 * time.Millisecond,
|
||||
maxConsecutiveFailures: 3,
|
||||
}
|
||||
|
||||
assert.False(t, e.recordFailure())
|
||||
assert.False(t, e.recordFailure())
|
||||
assert.True(t, e.recordFailure())
|
||||
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
assert.Equal(t, e.failureCount, e.maxConsecutiveFailures, "expected failureCount to be maxConsecutiveFailures")
|
||||
assert.Equal(t, e.backoff, e.maxBackoff, "expected backoff to be maxBackoff")
|
||||
}
|
||||
398
build-static.sh
398
build-static.sh
@@ -8,58 +8,76 @@ if ! type "git" >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_DIR=$(pwd)
|
||||
|
||||
arch="$(uname -m)"
|
||||
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
# FIXME: re-enable PHP errors when SPC will be compatible with PHP 8.4
|
||||
spcCommand="php -ddisplay_errors=Off ./bin/spc"
|
||||
md5binary="md5sum"
|
||||
if [ "${os}" = "darwin" ]; then
|
||||
os="mac"
|
||||
md5binary="md5 -q"
|
||||
[ "$os" = "darwin" ] && os="mac"
|
||||
|
||||
# Supported variables:
|
||||
# - PHP_VERSION: PHP version to build (default: "8.4")
|
||||
# - PHP_EXTENSIONS: PHP extensions to build (default: ${defaultExtensions} set below)
|
||||
# - PHP_EXTENSION_LIBS: PHP extension libraries to build (default: ${defaultExtensionLibs} set below)
|
||||
# - FRANKENPHP_VERSION: FrankenPHP version (default: current Git commit)
|
||||
# - EMBED: Path to the PHP app to embed (default: none)
|
||||
# - DEBUG_SYMBOLS: Enable debug symbols if set to 1 (default: none)
|
||||
# - MIMALLOC: Use mimalloc as the allocator if set to 1 (default: none)
|
||||
# - XCADDY_ARGS: Additional arguments to pass to xcaddy
|
||||
# - RELEASE: [maintainer only] Create a GitHub release if set to 1 (default: none)
|
||||
|
||||
# - SPC_REL_TYPE: Release type to download (accept "source" and "binary", default: "source")
|
||||
# - SPC_OPT_BUILD_ARGS: Additional arguments to pass to spc build
|
||||
# - SPC_OPT_DOWNLOAD_ARGS: Additional arguments to pass to spc download
|
||||
# - SPC_LIBC: Set to glibc to build with GNU toolchain (default: musl)
|
||||
|
||||
# init spc command, if we use spc binary, just use it instead of fetching source
|
||||
if [ -z "${SPC_REL_TYPE}" ]; then
|
||||
SPC_REL_TYPE="source"
|
||||
fi
|
||||
|
||||
if [ "${os}" = "linux" ] && ! type "cmake" >/dev/null 2>&1; then
|
||||
echo "The \"cmake\" command must be installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${os}" = "linux" ] && { [[ "${arch}" =~ "aarch" ]] || [[ "${arch}" =~ "arm" ]]; }; then
|
||||
fpic="-fPIC"
|
||||
fpie="-fPIE"
|
||||
|
||||
if [ -z "${DEBUG_SYMBOLS}" ]; then
|
||||
export SPC_PHP_DEFAULT_OPTIMIZE_CFLAGS="-g -fstack-protector-strong -fPIC -fPIE -Os -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64"
|
||||
fi
|
||||
else
|
||||
fpic="-fpic"
|
||||
fpie="-fpie"
|
||||
fi
|
||||
|
||||
if [ -z "${PHP_EXTENSIONS}" ]; then
|
||||
if [ -n "${EMBED}" ] && [ -f "${EMBED}/composer.json" ]; then
|
||||
cd "${EMBED}"
|
||||
# read the composer.json file and extract the required PHP extensions
|
||||
# remove internal extensions from the list: https://github.com/crazywhalecc/static-php-cli/blob/4b16631d45a57370b4747df15c8f105130e96d03/src/globals/defines.php#L26-L34
|
||||
PHP_EXTENSIONS="$(composer check-platform-reqs --no-dev 2>/dev/null | grep ^ext | sed -e 's/^ext-core//' -e 's/^ext-hash//' -e 's/^ext-json//' -e 's/^ext-pcre//' -e 's/^ext-reflection//' -e 's/^ext-spl//' -e 's/^ext-standard//' -e 's/^ext-//' -e 's/ .*//' | xargs | tr ' ' ',')"
|
||||
export PHP_EXTENSIONS
|
||||
cd -
|
||||
else
|
||||
export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,ftp,gd,gmp,gettext,iconv,igbinary,imagick,intl,ldap,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,parallel,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,ssh2,sysvmsg,sysvsem,sysvshm,tidy,tokenizer,xlswriter,xml,xmlreader,xmlwriter,zip,zlib,yaml,zstd"
|
||||
# init spc libc
|
||||
if [ -z "${SPC_LIBC}" ]; then
|
||||
if [ "${os}" = "linux" ]; then
|
||||
SPC_LIBC="musl"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "${PHP_EXTENSION_LIBS}" ]; then
|
||||
export PHP_EXTENSION_LIBS="bzip2,freetype,libavif,libjpeg,liblz4,libwebp,libzip,nghttp2"
|
||||
# init spc build additional args
|
||||
if [ -z "${SPC_OPT_BUILD_ARGS}" ]; then
|
||||
SPC_OPT_BUILD_ARGS=""
|
||||
fi
|
||||
|
||||
# The Brotli library must always be built as it is required by http://github.com/dunglas/caddy-cbrotli
|
||||
if ! echo "${PHP_EXTENSION_LIBS}" | grep -q "\bbrotli\b"; then
|
||||
export PHP_EXTENSION_LIBS="${PHP_EXTENSION_LIBS},brotli"
|
||||
if [ "${SPC_LIBC}" = "musl" ] && [[ "${SPC_OPT_BUILD_ARGS}" != *"--disable-opcache-jit"* ]]; then
|
||||
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --disable-opcache-jit"
|
||||
fi
|
||||
|
||||
# init spc download additional args
|
||||
if [ -z "${SPC_OPT_DOWNLOAD_ARGS}" ]; then
|
||||
SPC_OPT_DOWNLOAD_ARGS="--ignore-cache-sources=php-src --retry 5"
|
||||
if [ "${SPC_LIBC}" = "musl" ]; then
|
||||
SPC_OPT_DOWNLOAD_ARGS="${SPC_OPT_DOWNLOAD_ARGS} --prefer-pre-built"
|
||||
fi
|
||||
fi
|
||||
# if we need debug symbols, disable strip
|
||||
if [ -n "${DEBUG_SYMBOLS}" ]; then
|
||||
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --no-strip"
|
||||
fi
|
||||
# php version to build
|
||||
if [ -z "${PHP_VERSION}" ]; then
|
||||
export PHP_VERSION="8.4"
|
||||
get_latest_php_version() {
|
||||
input="$1"
|
||||
json=$(curl -s "https://www.php.net/releases/index.php?json&version=$input")
|
||||
latest=$(echo "$json" | jq -r '.version')
|
||||
|
||||
if [[ "$latest" == "$input"* ]]; then
|
||||
echo "$latest"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
PHP_VERSION="$(get_latest_php_version "8.4")"
|
||||
export PHP_VERSION
|
||||
fi
|
||||
# default extension set
|
||||
defaultExtensions="amqp,apcu,ast,bcmath,brotli,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,ftp,gd,gmp,gettext,iconv,igbinary,imagick,intl,ldap,lz4,mbregex,mbstring,memcache,memcached,mysqli,mysqlnd,opcache,openssl,password-argon2,parallel,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pdo_sqlsrv,pgsql,phar,posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,ssh2,sysvmsg,sysvsem,sysvshm,tidy,tokenizer,xlswriter,xml,xmlreader,xmlwriter,xsl,xz,zip,zlib,yaml,zstd"
|
||||
defaultExtensionLibs="libavif,nghttp2,nghttp3,ngtcp2,watcher"
|
||||
|
||||
if [ -z "${FRANKENPHP_VERSION}" ]; then
|
||||
FRANKENPHP_VERSION="$(git rev-parse --verify HEAD)"
|
||||
@@ -81,250 +99,128 @@ elif [ -d ".git/" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
bin="frankenphp-${os}-${arch}"
|
||||
|
||||
if [ -n "${CLEAN}" ]; then
|
||||
rm -Rf dist/
|
||||
go clean -cache
|
||||
fi
|
||||
|
||||
cache_key="${PHP_VERSION}-${PHP_EXTENSIONS}-${PHP_EXTENSION_LIBS}"
|
||||
mkdir -p dist/
|
||||
cd dist/
|
||||
|
||||
# Build libphp if necessary
|
||||
if [ -f dist/cache_key ] && [ "$(cat dist/cache_key)" = "${cache_key}" ] && [ -f "dist/static-php-cli/buildroot/lib/libphp.a" ]; then
|
||||
cd dist/static-php-cli
|
||||
else
|
||||
mkdir -p dist/
|
||||
cd dist/
|
||||
echo -n "${cache_key}" >cache_key
|
||||
if type "brew" >/dev/null 2>&1; then
|
||||
if ! type "composer" >/dev/null; then
|
||||
packages="composer"
|
||||
fi
|
||||
if ! type "go" >/dev/null 2>&1; then
|
||||
packages="${packages} go"
|
||||
fi
|
||||
if [ -n "${RELEASE}" ] && ! type "gh" >/dev/null 2>&1; then
|
||||
packages="${packages} gh"
|
||||
fi
|
||||
|
||||
if [ -d "static-php-cli/" ]; then
|
||||
cd static-php-cli/
|
||||
git pull
|
||||
if [ -n "${packages}" ]; then
|
||||
# shellcheck disable=SC2086
|
||||
brew install --formula --quiet ${packages}
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${SPC_REL_TYPE}" = "binary" ]; then
|
||||
mkdir -p static-php-cli/
|
||||
cd static-php-cli/
|
||||
if [[ "${arch}" =~ "arm" ]]; then
|
||||
dl_arch="aarch64"
|
||||
else
|
||||
git clone --depth 1 https://github.com/crazywhalecc/static-php-cli
|
||||
cd static-php-cli/
|
||||
dl_arch="${arch}"
|
||||
fi
|
||||
|
||||
if type "brew" >/dev/null 2>&1; then
|
||||
if ! type "composer" >/dev/null; then
|
||||
packages="composer"
|
||||
fi
|
||||
if ! type "go" >/dev/null 2>&1; then
|
||||
packages="${packages} go"
|
||||
fi
|
||||
if [ -n "${RELEASE}" ] && ! type "gh" >/dev/null 2>&1; then
|
||||
packages="${packages} gh"
|
||||
fi
|
||||
|
||||
if [ -n "${packages}" ]; then
|
||||
# shellcheck disable=SC2086
|
||||
brew install --formula --quiet ${packages}
|
||||
fi
|
||||
fi
|
||||
|
||||
composer install --no-dev -a
|
||||
|
||||
if [ "${os}" = "linux" ]; then
|
||||
extraOpts="--disable-opcache-jit"
|
||||
fi
|
||||
|
||||
if [ -n "${DEBUG_SYMBOLS}" ]; then
|
||||
extraOpts="${extraOpts} --no-strip"
|
||||
fi
|
||||
|
||||
${spcCommand} doctor --auto-fix
|
||||
${spcCommand} download --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}" --for-libs="${PHP_EXTENSION_LIBS}" --ignore-cache-sources=php-src --prefer-pre-built
|
||||
# shellcheck disable=SC2086
|
||||
${spcCommand} build --debug --enable-zts --build-embed ${extraOpts} "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}"
|
||||
fi
|
||||
|
||||
if ! type "go" >/dev/null 2>&1; then
|
||||
echo "The \"go\" command must be installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
XCADDY_COMMAND="xcaddy"
|
||||
if ! type "$XCADDY_COMMAND" >/dev/null 2>&1; then
|
||||
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
||||
XCADDY_COMMAND="$(go env GOPATH)/bin/xcaddy"
|
||||
fi
|
||||
|
||||
curlGitHubHeaders=(--header "X-GitHub-Api-Version: 2022-11-28")
|
||||
if [ "${GITHUB_TOKEN}" ]; then
|
||||
curlGitHubHeaders+=(--header "Authorization: Bearer ${GITHUB_TOKEN}")
|
||||
fi
|
||||
|
||||
# Compile e-dant/watcher as a static library
|
||||
mkdir -p watcher
|
||||
cd watcher
|
||||
curl -f --retry 5 "${curlGitHubHeaders[@]}" https://api.github.com/repos/e-dant/watcher/releases/latest |
|
||||
grep tarball_url |
|
||||
awk '{ print $2 }' |
|
||||
sed 's/,$//' |
|
||||
sed 's/"//g' |
|
||||
xargs curl -fL --retry 5 "${curlGitHubHeaders[@]}" |
|
||||
tar xz --strip-components 1
|
||||
cd watcher-c
|
||||
cc -c -o libwatcher-c.o ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra "${fpic}"
|
||||
ar rcs libwatcher-c.a libwatcher-c.o
|
||||
cp libwatcher-c.a ../../buildroot/lib/libwatcher-c.a
|
||||
mkdir -p ../../buildroot/include/wtr
|
||||
cp -R include/wtr/watcher-c.h ../../buildroot/include/wtr/watcher-c.h
|
||||
cd ../../
|
||||
|
||||
# See https://github.com/docker-library/php/blob/master/8.3/alpine3.20/zts/Dockerfile#L53-L55
|
||||
CGO_CFLAGS="-DFRANKENPHP_VERSION=${FRANKENPHP_VERSION} -I${PWD}/buildroot/include/ $(${spcCommand} spc-config "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}" --includes)"
|
||||
if [ -n "${DEBUG_SYMBOLS}" ]; then
|
||||
CGO_CFLAGS="-g ${CGO_CFLAGS}"
|
||||
curl -o spc -fsSL "https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-${dl_arch}"
|
||||
chmod +x spc
|
||||
spcCommand="./spc"
|
||||
elif [ -d "static-php-cli/src" ]; then
|
||||
cd static-php-cli/
|
||||
git pull
|
||||
composer install --no-dev -a --no-interaction
|
||||
spcCommand="./bin/spc"
|
||||
else
|
||||
CGO_CFLAGS="-fstack-protector-strong ${fpic} ${fpie} -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 ${CGO_CFLAGS}"
|
||||
fi
|
||||
export CGO_CFLAGS
|
||||
export CGO_CPPFLAGS="${CGO_CFLAGS}"
|
||||
|
||||
if [ "${os}" = "mac" ]; then
|
||||
export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration"
|
||||
elif [ "${os}" = "linux" ] && [ -z "${DEBUG_SYMBOLS}" ]; then
|
||||
CGO_LDFLAGS="-Wl,-O1 -pie"
|
||||
git clone --depth 1 https://github.com/crazywhalecc/static-php-cli --branch main
|
||||
cd static-php-cli/
|
||||
composer install --no-dev -a --no-interaction
|
||||
spcCommand="./bin/spc"
|
||||
fi
|
||||
|
||||
CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a ${PWD}/buildroot/lib/libwatcher-c.a $(${spcCommand} spc-config "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}" --libs)"
|
||||
if [ "${os}" = "linux" ]; then
|
||||
if echo "${PHP_EXTENSIONS}" | grep -qE "\b(intl|imagick|grpc|v8js|protobuf|mongodb|tbb)\b"; then
|
||||
CGO_LDFLAGS="${CGO_LDFLAGS} -lstdc++"
|
||||
# Extensions to build
|
||||
if [ -z "${PHP_EXTENSIONS}" ]; then
|
||||
# enable EMBED mode, first check if project has dumped extensions
|
||||
if [ -n "${EMBED}" ] && [ -f "${EMBED}/composer.json" ] && [ -f "${EMBED}/composer.lock" ] && [ -f "${EMBED}/vendor/installed.json" ]; then
|
||||
cd "${EMBED}"
|
||||
# read the extensions using spc dump-extensions
|
||||
PHP_EXTENSIONS=$(${spcCommand} dump-extensions "${EMBED}" --format=text --no-dev --no-ext-output="${defaultExtensions}")
|
||||
else
|
||||
PHP_EXTENSIONS="${defaultExtensions}"
|
||||
fi
|
||||
fi
|
||||
|
||||
export CGO_LDFLAGS
|
||||
# Additional libraries to build
|
||||
if [ -z "${PHP_EXTENSION_LIBS}" ]; then
|
||||
PHP_EXTENSION_LIBS="${defaultExtensionLibs}"
|
||||
fi
|
||||
|
||||
LIBPHP_VERSION="$(./buildroot/bin/php-config --version)"
|
||||
# The Brotli library must always be built as it is required by http://github.com/dunglas/caddy-cbrotli
|
||||
if ! echo "${PHP_EXTENSION_LIBS}" | grep -q "\bbrotli\b"; then
|
||||
PHP_EXTENSION_LIBS="${PHP_EXTENSION_LIBS},brotli"
|
||||
fi
|
||||
|
||||
cd ../
|
||||
|
||||
if [ "${os}" = "linux" ]; then
|
||||
if [ -n "${MIMALLOC}" ]; then
|
||||
# Replace musl's mallocng by mimalloc
|
||||
# The default musl allocator is slow, especially when used by multi-threaded apps,
|
||||
# and triggers weird bugs
|
||||
# Adapted from https://www.tweag.io/blog/2023-08-10-rust-static-link-with-mimalloc/
|
||||
|
||||
echo 'The USE_MIMALLOC environment variable is EXPERIMENTAL.'
|
||||
echo 'This option can be removed or its behavior modified at any time.'
|
||||
|
||||
if [ ! -f "mimalloc/out/libmimalloc.a" ]; then
|
||||
if [ -d "mimalloc" ]; then
|
||||
cd mimalloc/
|
||||
git reset --hard
|
||||
git clean -xdf
|
||||
git fetch --tags
|
||||
else
|
||||
git clone https://github.com/microsoft/mimalloc.git
|
||||
cd mimalloc/
|
||||
fi
|
||||
|
||||
git checkout "$(git describe --tags "$(git rev-list --tags --max-count=1 || true)" || true)"
|
||||
|
||||
curl -fL --retry 5 https://raw.githubusercontent.com/tweag/rust-alpine-mimalloc/1a756444a5c1484d26af9cd39187752728416ba8/mimalloc.diff -o mimalloc.diff
|
||||
patch -p1 <mimalloc.diff
|
||||
|
||||
mkdir -p out/
|
||||
cd out/
|
||||
if [ -n "${DEBUG_SYMBOLS}" ]; then
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMI_BUILD_SHARED=OFF \
|
||||
-DMI_BUILD_OBJECT=OFF \
|
||||
-DMI_BUILD_TESTS=OFF \
|
||||
../
|
||||
else
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DMI_BUILD_SHARED=OFF \
|
||||
-DMI_BUILD_OBJECT=OFF \
|
||||
-DMI_BUILD_TESTS=OFF \
|
||||
../
|
||||
fi
|
||||
make -j"$(nproc || true)"
|
||||
|
||||
cd ../../
|
||||
fi
|
||||
|
||||
if [ -n "${DEBUG_SYMBOLS}" ]; then
|
||||
libmimalloc_path=mimalloc/out/libmimalloc-debug.a
|
||||
else
|
||||
libmimalloc_path=mimalloc/out/libmimalloc.a
|
||||
fi
|
||||
|
||||
# Patch musl library to use mimalloc
|
||||
for libc_path in "/usr/local/musl/lib/libc.a" "/usr/local/musl/$(uname -m)-linux-musl/lib/libc.a" "/usr/lib/libc.a"; do
|
||||
if [ ! -f "${libc_path}" ] || [ -f "${libc_path}.unpatched" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
{
|
||||
echo "CREATE libc.a"
|
||||
echo "ADDLIB ${libc_path}"
|
||||
echo "DELETE aligned_alloc.lo calloc.lo donate.lo free.lo libc_calloc.lo lite_malloc.lo malloc.lo malloc_usable_size.lo memalign.lo posix_memalign.lo realloc.lo reallocarray.lo valloc.lo"
|
||||
echo "ADDLIB ${libmimalloc_path}"
|
||||
echo "SAVE"
|
||||
} | ar -M
|
||||
mv "${libc_path}" "${libc_path}.unpatched"
|
||||
mv libc.a "${libc_path}"
|
||||
done
|
||||
# The mimalloc library must be built if MIMALLOC is true
|
||||
if [ -n "${MIMALLOC}" ]; then
|
||||
if ! echo "${PHP_EXTENSION_LIBS}" | grep -q "\bmimalloc\b"; then
|
||||
PHP_EXTENSION_LIBS="${PHP_EXTENSION_LIBS},mimalloc"
|
||||
fi
|
||||
|
||||
# Increase the default stack size to prevents issues with code including many files such as Symfony containers
|
||||
extraExtldflags="-Wl,-z,stack-size=0x80000"
|
||||
fi
|
||||
|
||||
if [ -z "${DEBUG_SYMBOLS}" ]; then
|
||||
extraLdflags="-w -s"
|
||||
fi
|
||||
|
||||
cd ../
|
||||
|
||||
# Embed PHP app, if any
|
||||
if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then
|
||||
tar -cf app.tar -C "${EMBED}" .
|
||||
${md5binary} app.tar | awk '{printf $1}' >app_checksum.txt
|
||||
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --with-frankenphp-app=${EMBED}"
|
||||
fi
|
||||
|
||||
if [ -z "${XCADDY_ARGS}" ]; then
|
||||
XCADDY_ARGS="--with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy"
|
||||
SPC_OPT_INSTALL_ARGS="go-xcaddy"
|
||||
if [ -z "${DEBUG_SYMBOLS}" ] && [ -z "${NO_COMPRESS}" ] && [ "${os}" = "linux" ]; then
|
||||
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --with-upx-pack"
|
||||
SPC_OPT_INSTALL_ARGS="${SPC_OPT_INSTALL_ARGS} upx"
|
||||
fi
|
||||
|
||||
XCADDY_DEBUG=0
|
||||
export SPC_DEFAULT_C_FLAGS="-fPIC -O2"
|
||||
if [ -n "${DEBUG_SYMBOLS}" ]; then
|
||||
XCADDY_DEBUG=1
|
||||
SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="${SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS} -fPIE -g"
|
||||
else
|
||||
SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="${SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS} -fPIE -fstack-protector-strong -O2 -w -s"
|
||||
fi
|
||||
export SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS
|
||||
export SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="--with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli"
|
||||
|
||||
go env
|
||||
cd caddy/
|
||||
# Build FrankenPHP
|
||||
${spcCommand} doctor --auto-fix
|
||||
for pkg in ${SPC_OPT_INSTALL_ARGS}; do
|
||||
${spcCommand} install-pkg "${pkg}"
|
||||
done
|
||||
# shellcheck disable=SC2086
|
||||
CGO_ENABLED=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-buildmode=pie -tags cgo,netgo,osusergo,static_build,nobadger,nomysql,nopgx -ldflags \"-linkmode=external -extldflags '-static-pie ${extraExtldflags}' ${extraLdflags} -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ${FRANKENPHP_VERSION} PHP ${LIBPHP_VERSION} Caddy'\"" \
|
||||
XCADDY_DEBUG="${XCADDY_DEBUG}" \
|
||||
${XCADDY_COMMAND} build \
|
||||
--output "../dist/${bin}" \
|
||||
${XCADDY_ARGS} \
|
||||
--with github.com/dunglas/frankenphp=.. \
|
||||
--with github.com/dunglas/frankenphp/caddy=.
|
||||
cd ..
|
||||
${spcCommand} download --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}" --for-libs="${PHP_EXTENSION_LIBS}" ${SPC_OPT_DOWNLOAD_ARGS}
|
||||
export FRANKENPHP_SOURCE_PATH="${CURRENT_DIR}"
|
||||
# shellcheck disable=SC2086
|
||||
${spcCommand} build --enable-zts --build-embed --build-frankenphp ${SPC_OPT_BUILD_ARGS} "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}"
|
||||
|
||||
if [ -d "${EMBED}" ]; then
|
||||
truncate -s 0 app.tar
|
||||
truncate -s 0 app_checksum.txt
|
||||
if [ -n "$CI" ]; then
|
||||
rm -rf ./downloads
|
||||
rm -rf ./source
|
||||
fi
|
||||
|
||||
if type "upx" >/dev/null 2>&1 && [ -z "${DEBUG_SYMBOLS}" ] && [ -z "${NO_COMPRESS}" ]; then
|
||||
upx --best "dist/${bin}"
|
||||
fi
|
||||
cd ../..
|
||||
|
||||
"dist/${bin}" version
|
||||
bin="dist/frankenphp-${os}-${arch}"
|
||||
cp "dist/static-php-cli/buildroot/bin/frankenphp" "${bin}"
|
||||
"${bin}" version
|
||||
"${bin}" build-info
|
||||
|
||||
if [ -n "${RELEASE}" ]; then
|
||||
gh release upload "v${FRANKENPHP_VERSION}" "dist/${bin}" --repo dunglas/frankenphp --clobber
|
||||
gh release upload "v${FRANKENPHP_VERSION}" "${bin}" --repo dunglas/frankenphp --clobber
|
||||
fi
|
||||
|
||||
if [ -n "${CURRENT_REF}" ]; then
|
||||
|
||||
@@ -44,7 +44,7 @@ func (admin *FrankenPHPAdmin) restartWorkers(w http.ResponseWriter, r *http.Requ
|
||||
return nil
|
||||
}
|
||||
|
||||
func (admin *FrankenPHPAdmin) threads(w http.ResponseWriter, r *http.Request) error {
|
||||
func (admin *FrankenPHPAdmin) threads(w http.ResponseWriter, _ *http.Request) error {
|
||||
debugState := frankenphp.DebugState()
|
||||
prettyJson, err := json.MarshalIndent(debugState, "", " ")
|
||||
if err != nil {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package caddy_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -91,7 +95,10 @@ func TestAutoScaleWorkerThreads(t *testing.T) {
|
||||
frankenphp {
|
||||
max_threads 10
|
||||
num_threads 2
|
||||
worker ../testdata/sleep.php 1
|
||||
worker ../testdata/sleep.php {
|
||||
num 1
|
||||
max_threads 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,12 +113,12 @@ func TestAutoScaleWorkerThreads(t *testing.T) {
|
||||
|
||||
// spam an endpoint that simulates IO
|
||||
endpoint := "http://localhost:" + testPort + "/?sleep=2&work=1000"
|
||||
amountOfThreads := len(getDebugState(t, tester).ThreadDebugStates)
|
||||
amountOfThreads := getNumThreads(t, tester)
|
||||
|
||||
// try to spawn the additional threads by spamming the server
|
||||
for tries := 0; tries < maxTries; tries++ {
|
||||
for range maxTries {
|
||||
wg.Add(requestsPerTry)
|
||||
for i := 0; i < requestsPerTry; i++ {
|
||||
for range requestsPerTry {
|
||||
go func() {
|
||||
tester.AssertGetResponse(endpoint, http.StatusOK, "slept for 2 ms and worked for 1000 iterations")
|
||||
wg.Done()
|
||||
@@ -119,14 +126,14 @@ func TestAutoScaleWorkerThreads(t *testing.T) {
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
amountOfThreads = len(getDebugState(t, tester).ThreadDebugStates)
|
||||
amountOfThreads = getNumThreads(t, tester)
|
||||
if amountOfThreads > 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// assert that there are now more threads than before
|
||||
assert.NotEqual(t, amountOfThreads, 2)
|
||||
assert.NotEqual(t, amountOfThreads, 2, "at least one thread should have been auto-scaled")
|
||||
assert.LessOrEqual(t, amountOfThreads, 4, "at most 3 max_threads + 1 regular thread should be present")
|
||||
}
|
||||
|
||||
// Note this test requires at least 2x40MB available memory for the process
|
||||
@@ -158,12 +165,12 @@ func TestAutoScaleRegularThreadsOnAutomaticThreadLimit(t *testing.T) {
|
||||
|
||||
// spam an endpoint that simulates IO
|
||||
endpoint := "http://localhost:" + testPort + "/sleep.php?sleep=2&work=1000"
|
||||
amountOfThreads := len(getDebugState(t, tester).ThreadDebugStates)
|
||||
amountOfThreads := getNumThreads(t, tester)
|
||||
|
||||
// try to spawn the additional threads by spamming the server
|
||||
for tries := 0; tries < maxTries; tries++ {
|
||||
for range maxTries {
|
||||
wg.Add(requestsPerTry)
|
||||
for i := 0; i < requestsPerTry; i++ {
|
||||
for range requestsPerTry {
|
||||
go func() {
|
||||
tester.AssertGetResponse(endpoint, http.StatusOK, "slept for 2 ms and worked for 1000 iterations")
|
||||
wg.Done()
|
||||
@@ -171,7 +178,7 @@ func TestAutoScaleRegularThreadsOnAutomaticThreadLimit(t *testing.T) {
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
amountOfThreads = len(getDebugState(t, tester).ThreadDebugStates)
|
||||
amountOfThreads = getNumThreads(t, tester)
|
||||
if amountOfThreads > 1 {
|
||||
break
|
||||
}
|
||||
@@ -205,6 +212,7 @@ func getAdminResponseBody(t *testing.T, tester *caddytest.Tester, method string,
|
||||
}
|
||||
|
||||
func getDebugState(t *testing.T, tester *caddytest.Tester) frankenphp.FrankenPHPDebugState {
|
||||
t.Helper()
|
||||
threadStates := getAdminResponseBody(t, tester, "GET", "threads")
|
||||
|
||||
var debugStates frankenphp.FrankenPHPDebugState
|
||||
@@ -213,3 +221,83 @@ func getDebugState(t *testing.T, tester *caddytest.Tester) frankenphp.FrankenPHP
|
||||
|
||||
return debugStates
|
||||
}
|
||||
|
||||
func getNumThreads(t *testing.T, tester *caddytest.Tester) int {
|
||||
t.Helper()
|
||||
return len(getDebugState(t, tester).ThreadDebugStates)
|
||||
}
|
||||
|
||||
func TestAddModuleWorkerViaAdminApi(t *testing.T) {
|
||||
// Initialize a server with admin API enabled
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port `+testPort+`
|
||||
}
|
||||
|
||||
localhost:`+testPort+` {
|
||||
route {
|
||||
root ../testdata
|
||||
php
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
// Get initial debug state to check number of workers
|
||||
initialDebugState := getDebugState(t, tester)
|
||||
initialWorkerCount := 0
|
||||
for _, thread := range initialDebugState.ThreadDebugStates {
|
||||
if thread.Name != "" && thread.Name != "ready" {
|
||||
initialWorkerCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Create a Caddyfile configuration with a module worker
|
||||
workerConfig := `
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port ` + testPort + `
|
||||
}
|
||||
|
||||
localhost:` + testPort + ` {
|
||||
route {
|
||||
root ../testdata
|
||||
php {
|
||||
worker ../testdata/worker-with-counter.php 1
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Send the configuration to the admin API
|
||||
adminUrl := "http://localhost:2999/load"
|
||||
r, err := http.NewRequest("POST", adminUrl, bytes.NewBufferString(workerConfig))
|
||||
assert.NoError(t, err)
|
||||
r.Header.Set("Content-Type", "text/caddyfile")
|
||||
resp := tester.AssertResponseCode(r, http.StatusOK)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Get the updated debug state to check if the worker was added
|
||||
updatedDebugState := getDebugState(t, tester)
|
||||
updatedWorkerCount := 0
|
||||
workerFound := false
|
||||
filename, _ := fastabs.FastAbs("../testdata/worker-with-counter.php")
|
||||
for _, thread := range updatedDebugState.ThreadDebugStates {
|
||||
if thread.Name != "" && thread.Name != "ready" {
|
||||
updatedWorkerCount++
|
||||
if thread.Name == "Worker PHP Thread - "+filename {
|
||||
workerFound = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that the worker was added
|
||||
assert.Greater(t, updatedWorkerCount, initialWorkerCount, "Worker count should have increased")
|
||||
assert.True(t, workerFound, fmt.Sprintf("Worker with name %q should be found", "Worker PHP Thread - "+filename))
|
||||
|
||||
// Make a request to the worker to verify it's working
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/worker-with-counter.php", http.StatusOK, "requests:1")
|
||||
}
|
||||
|
||||
329
caddy/app.go
Normal file
329
caddy/app.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||
)
|
||||
|
||||
var (
|
||||
options []frankenphp.Option
|
||||
optionsMU sync.RWMutex
|
||||
)
|
||||
|
||||
// EXPERIMENTAL: RegisterWorkers provides a way for extensions to register frankenphp.Workers
|
||||
func RegisterWorkers(name, fileName string, num int, wo ...frankenphp.WorkerOption) frankenphp.Workers {
|
||||
w, opt := frankenphp.WithExtensionWorkers(name, fileName, num, wo...)
|
||||
|
||||
optionsMU.Lock()
|
||||
options = append(options, opt)
|
||||
optionsMU.Unlock()
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// FrankenPHPApp represents the global "frankenphp" directive in the Caddyfile
|
||||
// it's responsible for starting up the global PHP instance and all threads
|
||||
//
|
||||
// {
|
||||
// frankenphp {
|
||||
// num_threads 20
|
||||
// }
|
||||
// }
|
||||
type FrankenPHPApp struct {
|
||||
// NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.
|
||||
NumThreads int `json:"num_threads,omitempty"`
|
||||
// MaxThreads limits how many threads can be started at runtime. Default 2x NumThreads
|
||||
MaxThreads int `json:"max_threads,omitempty"`
|
||||
// Workers configures the worker scripts to start
|
||||
Workers []workerConfig `json:"workers,omitempty"`
|
||||
// Overwrites the default php ini configuration
|
||||
PhpIni map[string]string `json:"php_ini,omitempty"`
|
||||
// The maximum amount of time a request may be stalled waiting for a thread
|
||||
MaxWaitTime time.Duration `json:"max_wait_time,omitempty"`
|
||||
|
||||
opts []frankenphp.Option
|
||||
metrics frankenphp.Metrics
|
||||
ctx context.Context
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
var iniError = errors.New(`"php_ini" must be in the format: php_ini "<key>" "<value>"`)
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "frankenphp",
|
||||
New: func() caddy.Module { return &f },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up the module.
|
||||
func (f *FrankenPHPApp) Provision(ctx caddy.Context) error {
|
||||
f.ctx = ctx
|
||||
f.logger = ctx.Slogger()
|
||||
|
||||
// We have at least 7 hardcoded options
|
||||
f.opts = make([]frankenphp.Option, 0, 7+len(options))
|
||||
|
||||
if httpApp, err := ctx.AppIfConfigured("http"); err == nil {
|
||||
if httpApp.(*caddyhttp.App).Metrics != nil {
|
||||
f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
|
||||
}
|
||||
} else {
|
||||
// if the http module is not configured (this should never happen) then collect the metrics by default
|
||||
if errors.Is(err, caddy.ErrNotConfigured) {
|
||||
f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
|
||||
} else {
|
||||
// the http module failed to provision due to invalid configuration
|
||||
return fmt.Errorf("failed to provision caddy http: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FrankenPHPApp) generateUniqueModuleWorkerName(filepath string) string {
|
||||
var i uint
|
||||
filepath, _ = fastabs.FastAbs(filepath)
|
||||
name := "m#" + filepath
|
||||
|
||||
retry:
|
||||
for _, wc := range f.Workers {
|
||||
if wc.Name == name {
|
||||
name = fmt.Sprintf("m#%s_%d", filepath, i)
|
||||
i++
|
||||
|
||||
goto retry
|
||||
}
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func (f *FrankenPHPApp) addModuleWorkers(workers ...workerConfig) ([]workerConfig, error) {
|
||||
for i := range workers {
|
||||
w := &workers[i]
|
||||
|
||||
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(w.FileName) {
|
||||
w.FileName = filepath.Join(frankenphp.EmbeddedAppPath, w.FileName)
|
||||
}
|
||||
|
||||
if w.Name == "" {
|
||||
w.Name = f.generateUniqueModuleWorkerName(w.FileName)
|
||||
} else if !strings.HasPrefix(w.Name, "m#") {
|
||||
w.Name = "m#" + w.Name
|
||||
}
|
||||
|
||||
f.Workers = append(f.Workers, *w)
|
||||
}
|
||||
|
||||
return workers, nil
|
||||
}
|
||||
|
||||
func (f *FrankenPHPApp) Start() error {
|
||||
repl := caddy.NewReplacer()
|
||||
|
||||
optionsMU.RLock()
|
||||
f.opts = append(f.opts, options...)
|
||||
optionsMU.RUnlock()
|
||||
|
||||
f.opts = append(f.opts,
|
||||
frankenphp.WithContext(f.ctx),
|
||||
frankenphp.WithLogger(f.logger),
|
||||
frankenphp.WithNumThreads(f.NumThreads),
|
||||
frankenphp.WithMaxThreads(f.MaxThreads),
|
||||
frankenphp.WithMetrics(f.metrics),
|
||||
frankenphp.WithPhpIni(f.PhpIni),
|
||||
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
|
||||
)
|
||||
|
||||
for _, w := range f.Workers {
|
||||
w.options = append(w.options,
|
||||
frankenphp.WithWorkerEnv(w.Env),
|
||||
frankenphp.WithWorkerWatchMode(w.Watch),
|
||||
frankenphp.WithWorkerMaxFailures(w.MaxConsecutiveFailures),
|
||||
frankenphp.WithWorkerMaxThreads(w.MaxThreads),
|
||||
frankenphp.WithWorkerRequestOptions(w.requestOptions...),
|
||||
)
|
||||
|
||||
f.opts = append(f.opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, w.options...))
|
||||
}
|
||||
|
||||
frankenphp.Shutdown()
|
||||
if err := frankenphp.Init(f.opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FrankenPHPApp) Stop() error {
|
||||
ctx := caddy.ActiveContext()
|
||||
|
||||
if f.logger.Enabled(caddy.ActiveContext(), slog.LevelInfo) {
|
||||
f.logger.LogAttrs(ctx, slog.LevelInfo, "FrankenPHP stopped 🐘")
|
||||
}
|
||||
|
||||
// attempt a graceful shutdown if caddy is exiting
|
||||
// note: Exiting() is currently marked as 'experimental'
|
||||
// https://github.com/caddyserver/caddy/blob/e76405d55058b0a3e5ba222b44b5ef00516116aa/caddy.go#L810
|
||||
if caddy.Exiting() {
|
||||
frankenphp.DrainWorkers()
|
||||
}
|
||||
|
||||
// reset the configuration so it doesn't bleed into later tests
|
||||
f.Workers = nil
|
||||
f.NumThreads = 0
|
||||
f.MaxWaitTime = 0
|
||||
|
||||
optionsMU.Lock()
|
||||
options = nil
|
||||
optionsMU.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||
func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for d.Next() {
|
||||
for d.NextBlock(0) {
|
||||
// when adding a new directive, also update the allowedDirectives error message
|
||||
switch d.Val() {
|
||||
case "num_threads":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
v, err := strconv.ParseUint(d.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.NumThreads = int(v)
|
||||
case "max_threads":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
if d.Val() == "auto" {
|
||||
f.MaxThreads = -1
|
||||
continue
|
||||
}
|
||||
|
||||
v, err := strconv.ParseUint(d.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.MaxThreads = int(v)
|
||||
case "max_wait_time":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
v, err := time.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return d.Err("max_wait_time must be a valid duration (example: 10s)")
|
||||
}
|
||||
|
||||
f.MaxWaitTime = v
|
||||
case "php_ini":
|
||||
parseIniLine := func(d *caddyfile.Dispenser) error {
|
||||
key := d.Val()
|
||||
if !d.NextArg() {
|
||||
return d.WrapErr(iniError)
|
||||
}
|
||||
if f.PhpIni == nil {
|
||||
f.PhpIni = make(map[string]string)
|
||||
}
|
||||
f.PhpIni[key] = d.Val()
|
||||
if d.NextArg() {
|
||||
return d.WrapErr(iniError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
isBlock := false
|
||||
for d.NextBlock(1) {
|
||||
isBlock = true
|
||||
err := parseIniLine(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !isBlock {
|
||||
if !d.NextArg() {
|
||||
return d.WrapErr(iniError)
|
||||
}
|
||||
err := parseIniLine(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case "worker":
|
||||
wc, err := unmarshalWorker(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
|
||||
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
|
||||
}
|
||||
if strings.HasPrefix(wc.Name, "m#") {
|
||||
return d.Errf(`global worker names must not start with "m#": %q`, wc.Name)
|
||||
}
|
||||
// check for duplicate workers
|
||||
for _, existingWorker := range f.Workers {
|
||||
if existingWorker.FileName == wc.FileName {
|
||||
return d.Errf("global workers must not have duplicate filenames: %q", wc.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
f.Workers = append(f.Workers, wc)
|
||||
default:
|
||||
return wrongSubDirectiveError("frankenphp", "num_threads, max_threads, php_ini, worker, max_wait_time", d.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.MaxThreads > 0 && f.NumThreads > 0 && f.MaxThreads < f.NumThreads {
|
||||
return d.Err(`"max_threads"" must be greater than or equal to "num_threads"`)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseGlobalOption(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||
app := &FrankenPHPApp{}
|
||||
if err := app.UnmarshalCaddyfile(d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// tell Caddyfile adapter that this is the JSON for an app
|
||||
return httpcaddyfile.App{
|
||||
Name: "frankenphp",
|
||||
Value: caddyconfig.JSON(app, nil),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ caddy.App = (*FrankenPHPApp)(nil)
|
||||
_ caddy.Provisioner = (*FrankenPHPApp)(nil)
|
||||
)
|
||||
734
caddy/caddy.go
734
caddy/caddy.go
@@ -4,30 +4,16 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const defaultDocumentRoot = "public"
|
||||
|
||||
var iniError = errors.New("'php_ini' must be in the format: php_ini \"<key>\" \"<value>\"")
|
||||
const (
|
||||
defaultDocumentRoot = "public"
|
||||
defaultWatchPattern = "./**/*.{env,php,twig,yaml,yml}"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(FrankenPHPApp{})
|
||||
@@ -43,713 +29,7 @@ func init() {
|
||||
httpcaddyfile.RegisterDirectiveOrder("php_server", "before", "file_server")
|
||||
}
|
||||
|
||||
type workerConfig struct {
|
||||
// FileName sets the path to the worker script.
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
// Num sets the number of workers to start.
|
||||
Num int `json:"num,omitempty"`
|
||||
// Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
// Directories to watch for file changes
|
||||
Watch []string `json:"watch,omitempty"`
|
||||
// wrongSubDirectiveError returns a nice error message.
|
||||
func wrongSubDirectiveError(module string, allowedDirectives string, wrongValue string) error {
|
||||
return fmt.Errorf("unknown %q subdirective: %s (allowed directives are: %s)", module, wrongValue, allowedDirectives)
|
||||
}
|
||||
|
||||
type FrankenPHPApp struct {
|
||||
// NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.
|
||||
NumThreads int `json:"num_threads,omitempty"`
|
||||
// MaxThreads limits how many threads can be started at runtime. Default 2x NumThreads
|
||||
MaxThreads int `json:"max_threads,omitempty"`
|
||||
// Workers configures the worker scripts to start.
|
||||
Workers []workerConfig `json:"workers,omitempty"`
|
||||
// Overwrites the default php ini configuration
|
||||
PhpIni map[string]string `json:"php_ini,omitempty"`
|
||||
|
||||
metrics frankenphp.Metrics
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "frankenphp",
|
||||
New: func() caddy.Module { return &f },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up the module.
|
||||
func (f *FrankenPHPApp) Provision(ctx caddy.Context) error {
|
||||
f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
|
||||
f.logger = ctx.Logger()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FrankenPHPApp) Start() error {
|
||||
repl := caddy.NewReplacer()
|
||||
|
||||
opts := []frankenphp.Option{
|
||||
frankenphp.WithNumThreads(f.NumThreads),
|
||||
frankenphp.WithMaxThreads(f.MaxThreads),
|
||||
frankenphp.WithLogger(f.logger),
|
||||
frankenphp.WithMetrics(f.metrics),
|
||||
frankenphp.WithPhpIni(f.PhpIni),
|
||||
}
|
||||
for _, w := range f.Workers {
|
||||
opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env, w.Watch))
|
||||
}
|
||||
|
||||
frankenphp.Shutdown()
|
||||
if err := frankenphp.Init(opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FrankenPHPApp) Stop() error {
|
||||
f.logger.Info("FrankenPHP stopped 🐘")
|
||||
|
||||
// attempt a graceful shutdown if caddy is exiting
|
||||
// note: Exiting() is currently marked as 'experimental'
|
||||
// https://github.com/caddyserver/caddy/blob/e76405d55058b0a3e5ba222b44b5ef00516116aa/caddy.go#L810
|
||||
if caddy.Exiting() {
|
||||
frankenphp.DrainWorkers()
|
||||
}
|
||||
|
||||
// reset configuration so it doesn't bleed into later tests
|
||||
f.Workers = nil
|
||||
f.NumThreads = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||
func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for d.Next() {
|
||||
for d.NextBlock(0) {
|
||||
switch d.Val() {
|
||||
case "num_threads":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
v, err := strconv.Atoi(d.Val())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.NumThreads = v
|
||||
case "max_threads":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
if d.Val() == "auto" {
|
||||
f.MaxThreads = -1
|
||||
continue
|
||||
}
|
||||
|
||||
v, err := strconv.ParseUint(d.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.MaxThreads = int(v)
|
||||
case "php_ini":
|
||||
parseIniLine := func(d *caddyfile.Dispenser) error {
|
||||
key := d.Val()
|
||||
if !d.NextArg() {
|
||||
return iniError
|
||||
}
|
||||
if f.PhpIni == nil {
|
||||
f.PhpIni = make(map[string]string)
|
||||
}
|
||||
f.PhpIni[key] = d.Val()
|
||||
if d.NextArg() {
|
||||
return iniError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
isBlock := false
|
||||
for d.NextBlock(1) {
|
||||
isBlock = true
|
||||
err := parseIniLine(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !isBlock {
|
||||
if !d.NextArg() {
|
||||
return iniError
|
||||
}
|
||||
err := parseIniLine(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case "worker":
|
||||
wc := workerConfig{}
|
||||
if d.NextArg() {
|
||||
wc.FileName = d.Val()
|
||||
}
|
||||
|
||||
if d.NextArg() {
|
||||
v, err := strconv.Atoi(d.Val())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wc.Num = v
|
||||
}
|
||||
|
||||
for d.NextBlock(1) {
|
||||
v := d.Val()
|
||||
switch v {
|
||||
case "file":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
wc.FileName = d.Val()
|
||||
case "num":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
v, err := strconv.Atoi(d.Val())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wc.Num = v
|
||||
case "env":
|
||||
args := d.RemainingArgs()
|
||||
if len(args) != 2 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if wc.Env == nil {
|
||||
wc.Env = make(map[string]string)
|
||||
}
|
||||
wc.Env[args[0]] = args[1]
|
||||
case "watch":
|
||||
if !d.NextArg() {
|
||||
// the default if the watch directory is left empty:
|
||||
wc.Watch = append(wc.Watch, "./**/*.{php,yaml,yml,twig,env}")
|
||||
} else {
|
||||
wc.Watch = append(wc.Watch, d.Val())
|
||||
}
|
||||
}
|
||||
|
||||
if wc.FileName == "" {
|
||||
return errors.New(`the "file" argument must be specified`)
|
||||
}
|
||||
|
||||
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
|
||||
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
f.Workers = append(f.Workers, wc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.MaxThreads > 0 && f.NumThreads > 0 && f.MaxThreads < f.NumThreads {
|
||||
return errors.New("'max_threads' must be greater than or equal to 'num_threads'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||
app := &FrankenPHPApp{}
|
||||
if err := app.UnmarshalCaddyfile(d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// tell Caddyfile adapter that this is the JSON for an app
|
||||
return httpcaddyfile.App{
|
||||
Name: "frankenphp",
|
||||
Value: caddyconfig.JSON(app, nil),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type FrankenPHPModule struct {
|
||||
// Root sets the root folder to the site. Default: `root` directive, or the path of the public directory of the embed app it exists.
|
||||
Root string `json:"root,omitempty"`
|
||||
// SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`.
|
||||
SplitPath []string `json:"split_path,omitempty"`
|
||||
// ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
|
||||
ResolveRootSymlink *bool `json:"resolve_root_symlink,omitempty"`
|
||||
// Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
|
||||
resolvedDocumentRoot string
|
||||
preparedEnv frankenphp.PreparedEnv
|
||||
preparedEnvNeedsReplacement bool
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.handlers.php",
|
||||
New: func() caddy.Module { return new(FrankenPHPModule) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up the module.
|
||||
func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
|
||||
f.logger = ctx.Logger()
|
||||
|
||||
if f.Root == "" {
|
||||
if frankenphp.EmbeddedAppPath == "" {
|
||||
f.Root = "{http.vars.root}"
|
||||
} else {
|
||||
rrs := false
|
||||
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
|
||||
f.ResolveRootSymlink = &rrs
|
||||
}
|
||||
} else {
|
||||
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
|
||||
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.SplitPath) == 0 {
|
||||
f.SplitPath = []string{".php"}
|
||||
}
|
||||
|
||||
if f.ResolveRootSymlink == nil {
|
||||
rrs := true
|
||||
f.ResolveRootSymlink = &rrs
|
||||
}
|
||||
|
||||
if !needReplacement(f.Root) {
|
||||
root, err := fastabs.FastAbs(f.Root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to make the root path absolute: %w", err)
|
||||
}
|
||||
f.resolvedDocumentRoot = root
|
||||
|
||||
if *f.ResolveRootSymlink {
|
||||
root, err := filepath.EvalSymlinks(root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to resolve root symlink: %w", err)
|
||||
}
|
||||
|
||||
f.resolvedDocumentRoot = root
|
||||
}
|
||||
}
|
||||
|
||||
if f.preparedEnv == nil {
|
||||
f.preparedEnv = frankenphp.PrepareEnv(f.Env)
|
||||
|
||||
for _, e := range f.preparedEnv {
|
||||
if needReplacement(e) {
|
||||
f.preparedEnvNeedsReplacement = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// needReplacement checks if a string contains placeholders.
|
||||
func needReplacement(s string) bool {
|
||||
return strings.Contains(s, "{") || strings.Contains(s, "}")
|
||||
}
|
||||
|
||||
// ServeHTTP implements caddyhttp.MiddlewareHandler.
|
||||
// TODO: Expose TLS versions as env vars, as Apache's mod_ssl: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go#L298
|
||||
func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
|
||||
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
|
||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
|
||||
var documentRootOption frankenphp.RequestOption
|
||||
if f.resolvedDocumentRoot == "" {
|
||||
documentRootOption = frankenphp.WithRequestDocumentRoot(repl.ReplaceKnown(f.Root, ""), *f.ResolveRootSymlink)
|
||||
} else {
|
||||
documentRootOption = frankenphp.WithRequestResolvedDocumentRoot(f.resolvedDocumentRoot)
|
||||
}
|
||||
|
||||
env := f.preparedEnv
|
||||
if f.preparedEnvNeedsReplacement {
|
||||
env = make(frankenphp.PreparedEnv, len(f.Env))
|
||||
for k, v := range f.preparedEnv {
|
||||
env[k] = repl.ReplaceKnown(v, "")
|
||||
}
|
||||
}
|
||||
|
||||
fr, err := frankenphp.NewRequestWithContext(
|
||||
r,
|
||||
documentRootOption,
|
||||
frankenphp.WithRequestSplitPath(f.SplitPath),
|
||||
frankenphp.WithRequestPreparedEnv(env),
|
||||
frankenphp.WithOriginalRequest(&origReq),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return caddyhttp.Error(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
if err = frankenphp.ServeHTTP(w, fr); err != nil {
|
||||
return caddyhttp.Error(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||
func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for d.Next() {
|
||||
for d.NextBlock(0) {
|
||||
switch d.Val() {
|
||||
case "root":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
f.Root = d.Val()
|
||||
|
||||
case "split":
|
||||
f.SplitPath = d.RemainingArgs()
|
||||
if len(f.SplitPath) == 0 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
case "env":
|
||||
args := d.RemainingArgs()
|
||||
if len(args) != 2 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if f.Env == nil {
|
||||
f.Env = make(map[string]string)
|
||||
f.preparedEnv = make(frankenphp.PreparedEnv)
|
||||
}
|
||||
f.Env[args[0]] = args[1]
|
||||
f.preparedEnv[args[0]+"\x00"] = args[1]
|
||||
|
||||
case "resolve_root_symlink":
|
||||
if !d.NextArg() {
|
||||
continue
|
||||
}
|
||||
|
||||
v, err := strconv.ParseBool(d.Val())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
f.ResolveRootSymlink = &v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseCaddyfile unmarshals tokens from h into a new Middleware.
|
||||
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
m := FrankenPHPModule{}
|
||||
err := m.UnmarshalCaddyfile(h.Dispenser)
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
// parsePhpServer parses the php_server directive, which has a similar syntax
|
||||
// to the php_fastcgi directive. A line such as this:
|
||||
//
|
||||
// php_server
|
||||
//
|
||||
// is equivalent to a route consisting of:
|
||||
//
|
||||
// # Add trailing slash for directory requests
|
||||
// @canonicalPath {
|
||||
// file {path}/index.php
|
||||
// not path */
|
||||
// }
|
||||
// redir @canonicalPath {path}/ 308
|
||||
//
|
||||
// # If the requested file does not exist, try index files
|
||||
// @indexFiles file {
|
||||
// try_files {path} {path}/index.php index.php
|
||||
// split_path .php
|
||||
// }
|
||||
// rewrite @indexFiles {http.matchers.file.relative}
|
||||
//
|
||||
// # FrankenPHP!
|
||||
// @phpFiles path *.php
|
||||
// php @phpFiles
|
||||
// file_server
|
||||
//
|
||||
// parsePhpServer is freely inspired from the php_fastgci directive of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)
|
||||
func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
|
||||
if !h.Next() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
// set up FrankenPHP
|
||||
phpsrv := FrankenPHPModule{}
|
||||
|
||||
// set up file server
|
||||
fsrv := fileserver.FileServer{}
|
||||
disableFsrv := false
|
||||
|
||||
// set up the set of file extensions allowed to execute PHP code
|
||||
extensions := []string{".php"}
|
||||
|
||||
// set the default index file for the try_files rewrites
|
||||
indexFile := "index.php"
|
||||
|
||||
// set up for explicitly overriding try_files
|
||||
var tryFiles []string
|
||||
|
||||
// if the user specified a matcher token, use that
|
||||
// matcher in a route that wraps both of our routes;
|
||||
// either way, strip the matcher token and pass
|
||||
// the remaining tokens to the unmarshaler so that
|
||||
// we can gain the rest of the directive syntax
|
||||
userMatcherSet, err := h.ExtractMatcherSet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// make a new dispenser from the remaining tokens so that we
|
||||
// can reset the dispenser back to this point for the
|
||||
// php unmarshaler to read from it as well
|
||||
dispenser := h.NewFromNextSegment()
|
||||
|
||||
// read the subdirectives that we allow as overrides to
|
||||
// the php_server shortcut
|
||||
// NOTE: we delete the tokens as we go so that the php
|
||||
// unmarshal doesn't see these subdirectives which it cannot handle
|
||||
for dispenser.Next() {
|
||||
for dispenser.NextBlock(0) {
|
||||
// ignore any sub-subdirectives that might
|
||||
// have the same name somewhere within
|
||||
// the php passthrough tokens
|
||||
if dispenser.Nesting() != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// parse the php_server subdirectives
|
||||
switch dispenser.Val() {
|
||||
case "root":
|
||||
if !dispenser.NextArg() {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
phpsrv.Root = dispenser.Val()
|
||||
fsrv.Root = phpsrv.Root
|
||||
dispenser.DeleteN(2)
|
||||
|
||||
case "split":
|
||||
extensions = dispenser.RemainingArgs()
|
||||
dispenser.DeleteN(len(extensions) + 1)
|
||||
if len(extensions) == 0 {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
|
||||
case "index":
|
||||
args := dispenser.RemainingArgs()
|
||||
dispenser.DeleteN(len(args) + 1)
|
||||
if len(args) != 1 {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
indexFile = args[0]
|
||||
|
||||
case "try_files":
|
||||
args := dispenser.RemainingArgs()
|
||||
dispenser.DeleteN(len(args) + 1)
|
||||
if len(args) < 1 {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
tryFiles = args
|
||||
|
||||
case "file_server":
|
||||
args := dispenser.RemainingArgs()
|
||||
dispenser.DeleteN(len(args) + 1)
|
||||
if len(args) < 1 || args[0] != "off" {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
disableFsrv = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reset the dispenser after we're done so that the frankenphp
|
||||
// unmarshaler can read it from the start
|
||||
dispenser.Reset()
|
||||
|
||||
if frankenphp.EmbeddedAppPath != "" {
|
||||
if phpsrv.Root == "" {
|
||||
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
|
||||
fsrv.Root = phpsrv.Root
|
||||
rrs := false
|
||||
phpsrv.ResolveRootSymlink = &rrs
|
||||
} else if filepath.IsLocal(fsrv.Root) {
|
||||
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)
|
||||
fsrv.Root = phpsrv.Root
|
||||
}
|
||||
}
|
||||
|
||||
// set up a route list that we'll append to
|
||||
routes := caddyhttp.RouteList{}
|
||||
|
||||
// set the list of allowed path segments on which to split
|
||||
phpsrv.SplitPath = extensions
|
||||
|
||||
// if the index is turned off, we skip the redirect and try_files
|
||||
if indexFile != "off" {
|
||||
dirRedir := false
|
||||
dirIndex := "{http.request.uri.path}/" + indexFile
|
||||
tryPolicy := "first_exist_fallback"
|
||||
|
||||
// if tryFiles wasn't overridden, use a reasonable default
|
||||
if len(tryFiles) == 0 {
|
||||
if disableFsrv {
|
||||
tryFiles = []string{dirIndex, indexFile}
|
||||
} else {
|
||||
tryFiles = []string{"{http.request.uri.path}", dirIndex, indexFile}
|
||||
}
|
||||
|
||||
dirRedir = true
|
||||
} else {
|
||||
if !strings.HasSuffix(tryFiles[len(tryFiles)-1], ".php") {
|
||||
// use first_exist strategy if the last file is not a PHP file
|
||||
tryPolicy = ""
|
||||
}
|
||||
|
||||
for _, tf := range tryFiles {
|
||||
if tf == dirIndex {
|
||||
dirRedir = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// route to redirect to canonical path if index PHP file
|
||||
if dirRedir {
|
||||
redirMatcherSet := caddy.ModuleMap{
|
||||
"file": h.JSON(fileserver.MatchFile{
|
||||
TryFiles: []string{dirIndex},
|
||||
}),
|
||||
"not": h.JSON(caddyhttp.MatchNot{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{
|
||||
{
|
||||
"path": h.JSON(caddyhttp.MatchPath{"*/"}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
redirHandler := caddyhttp.StaticResponse{
|
||||
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
|
||||
Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
|
||||
}
|
||||
redirRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
|
||||
}
|
||||
|
||||
routes = append(routes, redirRoute)
|
||||
}
|
||||
|
||||
// route to rewrite to PHP index file
|
||||
rewriteMatcherSet := caddy.ModuleMap{
|
||||
"file": h.JSON(fileserver.MatchFile{
|
||||
TryFiles: tryFiles,
|
||||
TryPolicy: tryPolicy,
|
||||
SplitPath: extensions,
|
||||
}),
|
||||
}
|
||||
rewriteHandler := rewrite.Rewrite{
|
||||
URI: "{http.matchers.file.relative}",
|
||||
}
|
||||
rewriteRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
|
||||
}
|
||||
|
||||
routes = append(routes, rewriteRoute)
|
||||
}
|
||||
|
||||
// route to actually pass requests to PHP files;
|
||||
// match only requests that are for PHP files
|
||||
var pathList []string
|
||||
for _, ext := range extensions {
|
||||
pathList = append(pathList, "*"+ext)
|
||||
}
|
||||
phpMatcherSet := caddy.ModuleMap{
|
||||
"path": h.JSON(pathList),
|
||||
}
|
||||
|
||||
// the rest of the config is specified by the user
|
||||
// using the php directive syntax
|
||||
dispenser.Next() // consume the directive name
|
||||
err = phpsrv.UnmarshalCaddyfile(dispenser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create the PHP route which is
|
||||
// conditional on matching PHP files
|
||||
phpRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(phpsrv, "handler", "php", nil)},
|
||||
}
|
||||
routes = append(routes, phpRoute)
|
||||
|
||||
// create the file server route
|
||||
if !disableFsrv {
|
||||
fileRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(fsrv, "handler", "file_server", nil)},
|
||||
}
|
||||
routes = append(routes, fileRoute)
|
||||
}
|
||||
|
||||
subroute := caddyhttp.Subroute{
|
||||
Routes: routes,
|
||||
}
|
||||
|
||||
// the user's matcher is a prerequisite for ours, so
|
||||
// wrap ours in a subroute and return that
|
||||
if userMatcherSet != nil {
|
||||
return []httpcaddyfile.ConfigValue{
|
||||
{
|
||||
Class: "route",
|
||||
Value: caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// otherwise, return the literal subroute instead of
|
||||
// individual routes, to ensure they stay together and
|
||||
// are treated as a single unit, without necessarily
|
||||
// creating an actual subroute in the output
|
||||
return []httpcaddyfile.ConfigValue{
|
||||
{
|
||||
Class: "route",
|
||||
Value: subroute,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.App = (*FrankenPHPApp)(nil)
|
||||
_ caddy.Provisioner = (*FrankenPHPApp)(nil)
|
||||
_ caddy.Provisioner = (*FrankenPHPModule)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
|
||||
_ caddyfile.Unmarshaler = (*FrankenPHPModule)(nil)
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
222
caddy/config_test.go
Normal file
222
caddy/config_test.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestModuleWorkerDuplicateFilenamesFail(t *testing.T) {
|
||||
// Create a test configuration with duplicate worker filenames
|
||||
configWithDuplicateFilenames := `
|
||||
{
|
||||
php {
|
||||
worker {
|
||||
file worker-with-env.php
|
||||
num 1
|
||||
}
|
||||
worker {
|
||||
file worker-with-env.php
|
||||
num 2
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// Parse the configuration
|
||||
d := caddyfile.NewTestDispenser(configWithDuplicateFilenames)
|
||||
module := &FrankenPHPModule{}
|
||||
|
||||
// Unmarshal the configuration
|
||||
err := module.UnmarshalCaddyfile(d)
|
||||
|
||||
// Verify that an error was returned
|
||||
require.Error(t, err, "Expected an error when two workers in the same module have the same filename")
|
||||
require.Contains(t, err.Error(), "must not have duplicate filenames", "Error message should mention duplicate filenames")
|
||||
}
|
||||
|
||||
func TestModuleWorkersWithDifferentFilenames(t *testing.T) {
|
||||
// Create a test configuration with different worker filenames
|
||||
configWithDifferentFilenames := `
|
||||
{
|
||||
php {
|
||||
worker ../testdata/worker-with-env.php
|
||||
worker ../testdata/worker-with-counter.php
|
||||
}
|
||||
}`
|
||||
|
||||
// Parse the configuration
|
||||
d := caddyfile.NewTestDispenser(configWithDifferentFilenames)
|
||||
module := &FrankenPHPModule{}
|
||||
|
||||
// Unmarshal the configuration
|
||||
err := module.UnmarshalCaddyfile(d)
|
||||
|
||||
// Verify that no error was returned
|
||||
require.NoError(t, err, "Expected no error when two workers in the same module have different filenames")
|
||||
|
||||
// Verify that both workers were added to the module
|
||||
require.Len(t, module.Workers, 2, "Expected two workers to be added to the module")
|
||||
require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "First worker should have the correct filename")
|
||||
require.Equal(t, "../testdata/worker-with-counter.php", module.Workers[1].FileName, "Second worker should have the correct filename")
|
||||
}
|
||||
|
||||
func TestModuleWorkersDifferentNamesSucceed(t *testing.T) {
|
||||
// Create a test configuration with a worker name
|
||||
configWithWorkerName1 := `
|
||||
{
|
||||
php_server {
|
||||
worker {
|
||||
name test-worker-1
|
||||
file ../testdata/worker-with-env.php
|
||||
num 1
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// Parse the first configuration
|
||||
d1 := caddyfile.NewTestDispenser(configWithWorkerName1)
|
||||
app := &FrankenPHPApp{}
|
||||
module1 := &FrankenPHPModule{}
|
||||
|
||||
// Unmarshal the first configuration
|
||||
err := module1.UnmarshalCaddyfile(d1)
|
||||
require.NoError(t, err, "First module should be configured without errors")
|
||||
|
||||
// Create a second test configuration with a different worker name
|
||||
configWithWorkerName2 := `
|
||||
{
|
||||
php_server {
|
||||
worker {
|
||||
name test-worker-2
|
||||
file ../testdata/worker-with-env.php
|
||||
num 1
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// Parse the second configuration
|
||||
d2 := caddyfile.NewTestDispenser(configWithWorkerName2)
|
||||
module2 := &FrankenPHPModule{}
|
||||
|
||||
// Unmarshal the second configuration
|
||||
err = module2.UnmarshalCaddyfile(d2)
|
||||
|
||||
// Verify that no error was returned
|
||||
require.NoError(t, err, "Expected no error when two workers have different names")
|
||||
|
||||
_, err = app.addModuleWorkers(module1.Workers...)
|
||||
require.NoError(t, err, "Expected no error when adding the first module workers")
|
||||
_, err = app.addModuleWorkers(module2.Workers...)
|
||||
require.NoError(t, err, "Expected no error when adding the second module workers")
|
||||
|
||||
// Verify that both workers were added
|
||||
require.Len(t, app.Workers, 2, "Expected two workers in the app")
|
||||
require.Equal(t, "m#test-worker-1", app.Workers[0].Name, "First worker should have the correct name")
|
||||
require.Equal(t, "m#test-worker-2", app.Workers[1].Name, "Second worker should have the correct name")
|
||||
}
|
||||
|
||||
func TestModuleWorkerWithEnvironmentVariables(t *testing.T) {
|
||||
// Create a test configuration with environment variables
|
||||
configWithEnv := `
|
||||
{
|
||||
php {
|
||||
worker {
|
||||
file ../testdata/worker-with-env.php
|
||||
num 1
|
||||
env APP_ENV production
|
||||
env DEBUG true
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// Parse the configuration
|
||||
d := caddyfile.NewTestDispenser(configWithEnv)
|
||||
module := &FrankenPHPModule{}
|
||||
|
||||
// Unmarshal the configuration
|
||||
err := module.UnmarshalCaddyfile(d)
|
||||
|
||||
// Verify that no error was returned
|
||||
require.NoError(t, err, "Expected no error when configuring a worker with environment variables")
|
||||
|
||||
// Verify that the worker was added to the module
|
||||
require.Len(t, module.Workers, 1, "Expected one worker to be added to the module")
|
||||
require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "Worker should have the correct filename")
|
||||
|
||||
// Verify that the environment variables were set correctly
|
||||
require.Len(t, module.Workers[0].Env, 2, "Expected two environment variables")
|
||||
require.Equal(t, "production", module.Workers[0].Env["APP_ENV"], "APP_ENV should be set to production")
|
||||
require.Equal(t, "true", module.Workers[0].Env["DEBUG"], "DEBUG should be set to true")
|
||||
}
|
||||
|
||||
func TestModuleWorkerWithWatchConfiguration(t *testing.T) {
|
||||
// Create a test configuration with watch directories
|
||||
configWithWatch := `
|
||||
{
|
||||
php {
|
||||
worker {
|
||||
file ../testdata/worker-with-env.php
|
||||
num 1
|
||||
watch
|
||||
watch ./src/**/*.php
|
||||
watch ./config/**/*.yaml
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// Parse the configuration
|
||||
d := caddyfile.NewTestDispenser(configWithWatch)
|
||||
module := &FrankenPHPModule{}
|
||||
|
||||
// Unmarshal the configuration
|
||||
err := module.UnmarshalCaddyfile(d)
|
||||
|
||||
// Verify that no error was returned
|
||||
require.NoError(t, err, "Expected no error when configuring a worker with watch directories")
|
||||
|
||||
// Verify that the worker was added to the module
|
||||
require.Len(t, module.Workers, 1, "Expected one worker to be added to the module")
|
||||
require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "Worker should have the correct filename")
|
||||
|
||||
// Verify that the watch directories were set correctly
|
||||
require.Len(t, module.Workers[0].Watch, 3, "Expected three watch patterns")
|
||||
require.Equal(t, defaultWatchPattern, module.Workers[0].Watch[0], "First watch pattern should be the default")
|
||||
require.Equal(t, "./src/**/*.php", module.Workers[0].Watch[1], "Second watch pattern should match the configuration")
|
||||
require.Equal(t, "./config/**/*.yaml", module.Workers[0].Watch[2], "Third watch pattern should match the configuration")
|
||||
}
|
||||
|
||||
func TestModuleWorkerWithCustomName(t *testing.T) {
|
||||
// Create a test configuration with a custom worker name
|
||||
configWithCustomName := `
|
||||
{
|
||||
php {
|
||||
worker {
|
||||
file ../testdata/worker-with-env.php
|
||||
num 1
|
||||
name custom-worker-name
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// Parse the configuration
|
||||
d := caddyfile.NewTestDispenser(configWithCustomName)
|
||||
module := &FrankenPHPModule{}
|
||||
app := &FrankenPHPApp{}
|
||||
|
||||
// Unmarshal the configuration
|
||||
err := module.UnmarshalCaddyfile(d)
|
||||
|
||||
// Verify that no error was returned
|
||||
require.NoError(t, err, "Expected no error when configuring a worker with a custom name")
|
||||
|
||||
// Verify that the worker was added to the module
|
||||
require.Len(t, module.Workers, 1, "Expected one worker to be added to the module")
|
||||
require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "Worker should have the correct filename")
|
||||
|
||||
// Verify that the worker was added to app.Workers with the m# prefix
|
||||
module.Workers, err = app.addModuleWorkers(module.Workers...)
|
||||
require.NoError(t, err, "Expected no error when adding the worker to the app")
|
||||
require.Equal(t, "m#custom-worker-name", module.Workers[0].Name, "Worker should have the custom name, prefixed with m#")
|
||||
require.Equal(t, "m#custom-worker-name", app.Workers[0].Name, "Worker should have the custom name, prefixed with m#")
|
||||
}
|
||||
48
caddy/extinit.go
Normal file
48
caddy/extinit.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dunglas/frankenphp/internal/extgen"
|
||||
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddycmd.RegisterCommand(caddycmd.Command{
|
||||
Name: "extension-init",
|
||||
Usage: "go_extension.go [--verbose]",
|
||||
Short: "Initializes a PHP extension from a Go file (EXPERIMENTAL)",
|
||||
Long: `
|
||||
Initializes a PHP extension from a Go file. This command generates the necessary C files for the extension, including the header and source files, as well as the arginfo file.`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
|
||||
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdInitExtension)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func cmdInitExtension(_ caddycmd.Flags) (int, error) {
|
||||
if len(os.Args) < 3 {
|
||||
return 1, errors.New("the path to the Go source is required")
|
||||
}
|
||||
|
||||
sourceFile := os.Args[2]
|
||||
baseName := extgen.SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go"))
|
||||
|
||||
generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: filepath.Dir(sourceFile)}
|
||||
|
||||
if err := generator.Generate(); err != nil {
|
||||
return 1, err
|
||||
}
|
||||
|
||||
log.Printf("PHP extension %q initialized successfully in directory %q", baseName, generator.BuildDir)
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
# The Caddyfile is an easy way to configure FrankenPHP and the Caddy web server.
|
||||
#
|
||||
# https://frankenphp.dev/docs/config
|
||||
# https://caddyserver.com/docs/caddyfile
|
||||
{
|
||||
skip_install_trust
|
||||
|
||||
{$CADDY_GLOBAL_OPTIONS}
|
||||
|
||||
frankenphp {
|
||||
{$FRANKENPHP_CONFIG}
|
||||
#worker /path/to/your/worker.php
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +25,11 @@
|
||||
#}
|
||||
|
||||
root {$SERVER_ROOT:public/}
|
||||
root * public/
|
||||
encode zstd br gzip
|
||||
|
||||
# Uncomment the following lines to enable Mercure and Vulcain modules
|
||||
#mercure {
|
||||
# # Publisher JWT key
|
||||
# # Transport to use (default to Bolt)
|
||||
# transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
|
||||
# publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
|
||||
# # Subscriber JWT key
|
||||
# subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
|
||||
@@ -41,5 +45,13 @@
|
||||
{$CADDY_SERVER_EXTRA_DIRECTIVES}
|
||||
|
||||
php_server {
|
||||
php_server
|
||||
#worker /path/to/your/worker.php
|
||||
}
|
||||
}
|
||||
|
||||
# As an alternative to editing the above site block, you can add your own site
|
||||
# block files in the Caddyfile.d directory, and they will be included as long
|
||||
# as they use the .caddyfile extension.
|
||||
|
||||
import Caddyfile.d/*.caddyfile
|
||||
import Caddyfile.d/*.caddyfile
|
||||
|
||||
254
caddy/go.mod
254
caddy/go.mod
@@ -1,207 +1,211 @@
|
||||
module github.com/dunglas/frankenphp/caddy
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.0
|
||||
go 1.25.4
|
||||
|
||||
replace github.com/dunglas/frankenphp => ../
|
||||
|
||||
retract v1.0.0-rc.1 // Human error
|
||||
|
||||
require (
|
||||
github.com/caddyserver/caddy/v2 v2.9.1
|
||||
github.com/caddyserver/certmagic v0.21.7
|
||||
github.com/dunglas/caddy-cbrotli v1.0.0
|
||||
github.com/dunglas/frankenphp v1.4.4
|
||||
github.com/dunglas/mercure/caddy v0.18.4
|
||||
github.com/dunglas/vulcain/caddy v1.1.1
|
||||
github.com/prometheus/client_golang v1.21.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.uber.org/zap v1.27.0
|
||||
github.com/caddyserver/caddy/v2 v2.10.2
|
||||
github.com/caddyserver/certmagic v0.25.0
|
||||
github.com/dunglas/caddy-cbrotli v1.0.1
|
||||
github.com/dunglas/frankenphp v1.11.1
|
||||
github.com/dunglas/mercure v0.21.4
|
||||
github.com/dunglas/mercure/caddy v0.21.4
|
||||
github.com/dunglas/vulcain/caddy v1.2.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
|
||||
require github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect
|
||||
require github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.20.0 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go/auth v0.18.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/KimMachineGun/automemlimit v0.7.5 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
|
||||
github.com/MicahParks/jwkset v0.8.0 // indirect
|
||||
github.com/MicahParks/keyfunc/v3 v3.3.10 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/RoaringBitmap/roaring v1.9.4 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.15.0 // indirect
|
||||
github.com/MicahParks/jwkset v0.11.0 // indirect
|
||||
github.com/MicahParks/keyfunc/v3 v3.7.0 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.21.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.20.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.12.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.2.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dolthub/maphash v0.1.0 // indirect
|
||||
github.com/dunglas/httpsfv v1.0.2 // indirect
|
||||
github.com/dunglas/mercure v0.18.4 // indirect
|
||||
github.com/dunglas/vulcain v1.1.1 // indirect
|
||||
github.com/dunglas/httpsfv v1.1.0 // indirect
|
||||
github.com/dunglas/skipfilter v1.0.0 // indirect
|
||||
github.com/dunglas/vulcain v1.2.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/gammazero/deque v1.0.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.128.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.133.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.4.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/brotli/go/cbrotli v0.0.0-20241111155135-4303850b01d6 // indirect
|
||||
github.com/google/cel-go v0.23.2 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.0 // indirect
|
||||
github.com/google/go-tpm v0.9.3 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/brotli/go/cbrotli v1.1.0 // indirect
|
||||
github.com/google/cel-go v0.26.1 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.2 // indirect
|
||||
github.com/google/go-tpm v0.9.7 // indirect
|
||||
github.com/google/go-tspi v0.3.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/invopop/yaml v0.3.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.2 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kevburnsjr/skipfilter v0.0.1 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/libdns/libdns v0.2.3 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/libdns/libdns v1.1.1 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/maypok86/otter v1.2.4 // indirect
|
||||
github.com/maypok86/otter/v2 v2.2.1 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/acmez/v3 v3.0.1 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.4 // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.0 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.49.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.57.1 // indirect
|
||||
github.com/rs/cors v1.11.1 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/slackhq/nebula v1.9.4 // indirect
|
||||
github.com/smallstep/certificates v0.28.1 // indirect
|
||||
github.com/smallstep/cli-utils v0.10.0 // indirect
|
||||
github.com/slackhq/nebula v1.9.7 // indirect
|
||||
github.com/smallstep/certificates v0.29.0 // indirect
|
||||
github.com/smallstep/cli-utils v0.12.2 // indirect
|
||||
github.com/smallstep/linkedca v0.25.0 // indirect
|
||||
github.com/smallstep/nosql v0.7.0 // indirect
|
||||
github.com/smallstep/pkcs7 v0.2.1 // indirect
|
||||
github.com/smallstep/scep v0.0.0-20241223071629-a37a330173bc // indirect
|
||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect
|
||||
github.com/smallstep/truststore v0.13.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/viper v1.19.0 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/unrolled/secure v1.17.0 // indirect
|
||||
github.com/urfave/cli v1.22.16 // indirect
|
||||
github.com/urfave/cli v1.22.17 // indirect
|
||||
github.com/woodsbury/decimal128 v1.4.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yuin/goldmark v1.7.13 // indirect
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.4.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.58.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.33.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.33.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.33.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
|
||||
go.step.sm/crypto v0.57.1 // indirect
|
||||
go.step.sm/linkedca v0.22.2 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.step.sm/crypto v0.75.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250214233241-911360c8a4f4 // indirect
|
||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
|
||||
golang.org/x/mod v0.23.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.10.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/grpc v1.70.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00 // indirect
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/api v0.257.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v1.0.1 // indirect
|
||||
|
||||
768
caddy/go.sum
768
caddy/go.sum
File diff suppressed because it is too large
Load Diff
20
caddy/hotreload-skip.go
Normal file
20
caddy/hotreload-skip.go
Normal file
@@ -0,0 +1,20 @@
|
||||
//go:build nowatcher || nomercure
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
type hotReloadContext struct {
|
||||
}
|
||||
|
||||
func (_ *FrankenPHPModule) configureHotReload(_ *FrankenPHPApp) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_ *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) error {
|
||||
return errors.New("hot reload support disabled")
|
||||
}
|
||||
104
caddy/hotreload.go
Normal file
104
caddy/hotreload.go
Normal file
@@ -0,0 +1,104 @@
|
||||
//go:build !nowatcher && !nomercure
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"net/url"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
const defaultHotReloadPattern = "./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}"
|
||||
|
||||
type hotReloadContext struct {
|
||||
// HotReload specifies files to watch for file changes to trigger hot reloads updates. Supports the glob syntax.
|
||||
HotReload *hotReloadConfig `json:"hot_reload,omitempty"`
|
||||
}
|
||||
|
||||
type hotReloadConfig struct {
|
||||
Topic string `json:"topic"`
|
||||
Watch []string `json:"watch"`
|
||||
}
|
||||
|
||||
func (f *FrankenPHPModule) configureHotReload(app *FrankenPHPApp) error {
|
||||
if f.HotReload == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if f.mercureHub == nil {
|
||||
return errors.New("unable to enable hot reloading: no Mercure hub configured")
|
||||
}
|
||||
|
||||
if len(f.HotReload.Watch) == 0 {
|
||||
f.HotReload.Watch = []string{defaultHotReloadPattern}
|
||||
}
|
||||
|
||||
if f.HotReload.Topic == "" {
|
||||
uid, err := uniqueID(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.HotReload.Topic = "https://frankenphp.dev/hot-reload/" + uid
|
||||
}
|
||||
|
||||
app.opts = append(app.opts, frankenphp.WithHotReload(f.HotReload.Topic, f.mercureHub, f.HotReload.Watch))
|
||||
f.preparedEnv["FRANKENPHP_HOT_RELOAD\x00"] = "/.well-known/mercure?topic=" + url.QueryEscape(f.HotReload.Topic)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) error {
|
||||
f.HotReload = &hotReloadConfig{
|
||||
Watch: d.RemainingArgs(),
|
||||
}
|
||||
|
||||
for d.NextBlock(1) {
|
||||
switch v := d.Val(); v {
|
||||
case "topic":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
if f.HotReload == nil {
|
||||
f.HotReload = &hotReloadConfig{}
|
||||
}
|
||||
|
||||
f.HotReload.Topic = d.Val()
|
||||
|
||||
case "watch":
|
||||
patterns := d.RemainingArgs()
|
||||
if len(patterns) == 0 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
f.HotReload.Watch = append(f.HotReload.Watch, patterns...)
|
||||
|
||||
default:
|
||||
return wrongSubDirectiveError("hot_reload", "topic, watch", v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uniqueID(s any) (string, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
if err := gob.NewEncoder(&b).Encode(s); err != nil {
|
||||
return "", fmt.Errorf("unable to generate unique name: %w", err)
|
||||
}
|
||||
|
||||
h := fnv.New64a()
|
||||
if _, err := h.Write(b.Bytes()); err != nil {
|
||||
return "", fmt.Errorf("unable to generate unique name: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%016x", h.Sum64()), nil
|
||||
}
|
||||
88
caddy/hotreload_test.go
Normal file
88
caddy/hotreload_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
//go:build !nowatcher && !nomercure
|
||||
|
||||
package caddy_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHotReload(t *testing.T) {
|
||||
const topic = "https://frankenphp.dev/hot-reload/test"
|
||||
|
||||
u := "/.well-known/mercure?topic=" + url.QueryEscape(topic)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
indexFile := filepath.Join(tmpDir, "index.php")
|
||||
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
debug
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
}
|
||||
|
||||
http://localhost:`+testPort+` {
|
||||
mercure {
|
||||
transport local
|
||||
subscriber_jwt TestKey
|
||||
anonymous
|
||||
}
|
||||
|
||||
php_server {
|
||||
root `+tmpDir+`
|
||||
hot_reload {
|
||||
topic `+topic+`
|
||||
watch `+tmpDir+`/*.php
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
var connected, received sync.WaitGroup
|
||||
|
||||
connected.Add(1)
|
||||
received.Go(func() {
|
||||
cx, cancel := context.WithCancel(t.Context())
|
||||
req, _ := http.NewRequest(http.MethodGet, "http://localhost:"+testPort+u, nil)
|
||||
req = req.WithContext(cx)
|
||||
resp := tester.AssertResponseCode(req, http.StatusOK)
|
||||
|
||||
connected.Done()
|
||||
|
||||
var receivedBody strings.Builder
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
_, err := resp.Body.Read(buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
receivedBody.Write(buf)
|
||||
|
||||
if strings.Contains(receivedBody.String(), "index.php") {
|
||||
cancel()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NoError(t, resp.Body.Close())
|
||||
})
|
||||
|
||||
connected.Wait()
|
||||
|
||||
require.NoError(t, os.WriteFile(indexFile, []byte("<?=$_SERVER['FRANKENPHP_HOT_RELOAD'];"), 0644))
|
||||
|
||||
received.Wait()
|
||||
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, u)
|
||||
}
|
||||
13
caddy/mercure-skip.go
Normal file
13
caddy/mercure-skip.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build nomercure
|
||||
|
||||
package caddy
|
||||
|
||||
type mercureContext struct {
|
||||
}
|
||||
|
||||
func (f *FrankenPHPModule) configureHotReload(_ *FrankenPHPApp) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FrankenPHPModule) assignMercureHub(_ caddy.Context) {
|
||||
}
|
||||
34
caddy/mercure.go
Normal file
34
caddy/mercure.go
Normal file
@@ -0,0 +1,34 @@
|
||||
//go:build !nomercure
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"github.com/dunglas/mercure"
|
||||
mercureCaddy "github.com/dunglas/mercure/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mercureCaddy.AllowNoPublish = true
|
||||
}
|
||||
|
||||
type mercureContext struct {
|
||||
mercureHub *mercure.Hub
|
||||
}
|
||||
|
||||
func (f *FrankenPHPModule) assignMercureHub(ctx caddy.Context) {
|
||||
if f.mercureHub = mercureCaddy.FindHub(ctx.Modules()); f.mercureHub == nil {
|
||||
return
|
||||
}
|
||||
|
||||
opt := frankenphp.WithMercureHub(f.mercureHub)
|
||||
f.mercureHubRequestOption = &opt
|
||||
|
||||
for i, wc := range f.Workers {
|
||||
wc.mercureHub = f.mercureHub
|
||||
wc.options = append(wc.options, frankenphp.WithWorkerMercureHub(wc.mercureHub))
|
||||
|
||||
f.Workers[i] = wc
|
||||
}
|
||||
}
|
||||
665
caddy/module.go
Normal file
665
caddy/module.go
Normal file
@@ -0,0 +1,665 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||
)
|
||||
|
||||
var serverHeader = []string{"FrankenPHP Caddy"}
|
||||
|
||||
// FrankenPHPModule represents the "php_server" and "php" directives in the Caddyfile
|
||||
// they are responsible for forwarding requests to FrankenPHP via "ServeHTTP"
|
||||
//
|
||||
// example.com {
|
||||
// php_server {
|
||||
// root /var/www/html
|
||||
// }
|
||||
// }
|
||||
type FrankenPHPModule struct {
|
||||
mercureContext
|
||||
hotReloadContext
|
||||
|
||||
// Root sets the root folder to the site. Default: `root` directive, or the path of the public directory of the embed app it exists.
|
||||
Root string `json:"root,omitempty"`
|
||||
// SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`.
|
||||
SplitPath []string `json:"split_path,omitempty"`
|
||||
// ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
|
||||
ResolveRootSymlink *bool `json:"resolve_root_symlink,omitempty"`
|
||||
// Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
// Workers configures the worker scripts to start.
|
||||
Workers []workerConfig `json:"workers,omitempty"`
|
||||
|
||||
resolvedDocumentRoot string
|
||||
preparedEnv frankenphp.PreparedEnv
|
||||
preparedEnvNeedsReplacement bool
|
||||
logger *slog.Logger
|
||||
mercureHubRequestOption *frankenphp.RequestOption
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.handlers.php",
|
||||
New: func() caddy.Module { return new(FrankenPHPModule) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up the module.
|
||||
func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
|
||||
f.logger = ctx.Slogger()
|
||||
app, err := ctx.App("frankenphp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fapp, ok := app.(*FrankenPHPApp)
|
||||
if !ok {
|
||||
return fmt.Errorf(`expected ctx.App("frankenphp") to return *FrankenPHPApp, got %T`, app)
|
||||
}
|
||||
if fapp == nil {
|
||||
return fmt.Errorf(`expected ctx.App("frankenphp") to return *FrankenPHPApp, got nil`)
|
||||
}
|
||||
|
||||
f.assignMercureHub(ctx)
|
||||
|
||||
loggerOpt := frankenphp.WithRequestLogger(f.logger)
|
||||
for i, wc := range f.Workers {
|
||||
// make the file path absolute from the public directory
|
||||
// this can only be done if the root is defined inside php_server
|
||||
if !filepath.IsAbs(wc.FileName) && f.Root != "" {
|
||||
wc.FileName = filepath.Join(f.Root, wc.FileName)
|
||||
}
|
||||
|
||||
// Inherit environment variables from the parent php_server directive
|
||||
if f.Env != nil {
|
||||
wc.inheritEnv(f.Env)
|
||||
}
|
||||
|
||||
wc.requestOptions = append(wc.requestOptions, loggerOpt)
|
||||
f.Workers[i] = wc
|
||||
}
|
||||
|
||||
workers, err := fapp.addModuleWorkers(f.Workers...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Workers = workers
|
||||
|
||||
if f.Root == "" {
|
||||
if frankenphp.EmbeddedAppPath == "" {
|
||||
f.Root = "{http.vars.root}"
|
||||
} else {
|
||||
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
|
||||
|
||||
var rrs bool
|
||||
f.ResolveRootSymlink = &rrs
|
||||
}
|
||||
} else if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
|
||||
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
|
||||
}
|
||||
|
||||
if len(f.SplitPath) == 0 {
|
||||
f.SplitPath = []string{".php"}
|
||||
}
|
||||
|
||||
if f.ResolveRootSymlink == nil {
|
||||
rrs := true
|
||||
f.ResolveRootSymlink = &rrs
|
||||
}
|
||||
|
||||
if !needReplacement(f.Root) {
|
||||
root, err := fastabs.FastAbs(f.Root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to make the root path absolute: %w", err)
|
||||
}
|
||||
f.resolvedDocumentRoot = root
|
||||
|
||||
if *f.ResolveRootSymlink {
|
||||
root, err := filepath.EvalSymlinks(root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to resolve root symlink: %w", err)
|
||||
}
|
||||
|
||||
f.resolvedDocumentRoot = root
|
||||
}
|
||||
}
|
||||
|
||||
if f.preparedEnv == nil {
|
||||
f.preparedEnv = frankenphp.PrepareEnv(f.Env)
|
||||
|
||||
for _, e := range f.preparedEnv {
|
||||
if needReplacement(e) {
|
||||
f.preparedEnvNeedsReplacement = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.configureHotReload(fapp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// needReplacement checks if a string contains placeholders.
|
||||
func needReplacement(s string) bool {
|
||||
return strings.ContainsAny(s, "{}")
|
||||
}
|
||||
|
||||
// ServeHTTP implements caddyhttp.MiddlewareHandler.
|
||||
func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
|
||||
ctx := r.Context()
|
||||
origReq := ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
|
||||
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
|
||||
var (
|
||||
documentRootOption frankenphp.RequestOption
|
||||
documentRoot string
|
||||
)
|
||||
|
||||
if f.resolvedDocumentRoot == "" {
|
||||
documentRoot = repl.ReplaceKnown(f.Root, "")
|
||||
if documentRoot == "" && frankenphp.EmbeddedAppPath != "" {
|
||||
documentRoot = frankenphp.EmbeddedAppPath
|
||||
}
|
||||
documentRootOption = frankenphp.WithRequestDocumentRoot(documentRoot, *f.ResolveRootSymlink)
|
||||
} else {
|
||||
documentRoot = f.resolvedDocumentRoot
|
||||
documentRootOption = frankenphp.WithRequestResolvedDocumentRoot(documentRoot)
|
||||
}
|
||||
|
||||
env := f.preparedEnv
|
||||
if f.preparedEnvNeedsReplacement {
|
||||
env = make(frankenphp.PreparedEnv, len(f.Env))
|
||||
for k, v := range f.preparedEnv {
|
||||
env[k] = repl.ReplaceKnown(v, "")
|
||||
}
|
||||
}
|
||||
|
||||
workerName := ""
|
||||
for _, w := range f.Workers {
|
||||
if w.matchesPath(r, documentRoot) {
|
||||
workerName = w.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
fr *http.Request
|
||||
)
|
||||
|
||||
if f.mercureHubRequestOption == nil {
|
||||
fr, err = frankenphp.NewRequestWithContext(
|
||||
r,
|
||||
documentRootOption,
|
||||
frankenphp.WithRequestSplitPath(f.SplitPath),
|
||||
frankenphp.WithRequestPreparedEnv(env),
|
||||
frankenphp.WithOriginalRequest(&origReq),
|
||||
frankenphp.WithWorkerName(workerName),
|
||||
)
|
||||
} else {
|
||||
fr, err = frankenphp.NewRequestWithContext(
|
||||
r,
|
||||
documentRootOption,
|
||||
frankenphp.WithRequestSplitPath(f.SplitPath),
|
||||
frankenphp.WithRequestPreparedEnv(env),
|
||||
frankenphp.WithOriginalRequest(&origReq),
|
||||
frankenphp.WithWorkerName(workerName),
|
||||
*f.mercureHubRequestOption,
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return caddyhttp.Error(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
// TODO: set caddyhttp.ServerHeader when https://github.com/caddyserver/caddy/pull/7338 will be released
|
||||
w.Header()["Server"] = serverHeader
|
||||
if err = frankenphp.ServeHTTP(w, fr); err != nil && !errors.As(err, &frankenphp.ErrRejected{}) {
|
||||
return caddyhttp.Error(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||
func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for d.Next() {
|
||||
for d.NextBlock(0) {
|
||||
switch d.Val() {
|
||||
case "root":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
f.Root = d.Val()
|
||||
|
||||
case "split":
|
||||
f.SplitPath = d.RemainingArgs()
|
||||
if len(f.SplitPath) == 0 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
case "env":
|
||||
args := d.RemainingArgs()
|
||||
if len(args) != 2 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if f.Env == nil {
|
||||
f.Env = make(map[string]string)
|
||||
f.preparedEnv = make(frankenphp.PreparedEnv)
|
||||
}
|
||||
f.Env[args[0]] = args[1]
|
||||
f.preparedEnv[args[0]+"\x00"] = args[1]
|
||||
|
||||
case "resolve_root_symlink":
|
||||
if !d.NextArg() {
|
||||
continue
|
||||
}
|
||||
v, err := strconv.ParseBool(d.Val())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
f.ResolveRootSymlink = &v
|
||||
|
||||
case "worker":
|
||||
wc, err := unmarshalWorker(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Workers = append(f.Workers, wc)
|
||||
|
||||
case "hot_reload":
|
||||
if err := f.unmarshalHotReload(d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return wrongSubDirectiveError("php or php_server", "hot_reload, name, root, split, env, resolve_root_symlink, worker", d.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a worker with this filename already exists in this module
|
||||
fileNames := make(map[string]struct{}, len(f.Workers))
|
||||
for _, w := range f.Workers {
|
||||
if _, ok := fileNames[w.FileName]; ok {
|
||||
return fmt.Errorf(`workers in a single "php" or "php_server" block must not have duplicate filenames: %q`, w.FileName)
|
||||
}
|
||||
|
||||
if len(w.MatchPath) == 0 {
|
||||
fileNames[w.FileName] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseCaddyfile unmarshals tokens from h into a new Middleware.
|
||||
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
m := &FrankenPHPModule{}
|
||||
err := m.UnmarshalCaddyfile(h.Dispenser)
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
// parsePhpServer parses the php_server directive, which has a similar syntax
|
||||
// to the php_fastcgi directive. A line such as this:
|
||||
//
|
||||
// php_server
|
||||
//
|
||||
// is equivalent to a route consisting of:
|
||||
//
|
||||
// # Add trailing slash for directory requests
|
||||
// @canonicalPath {
|
||||
// file {path}/index.php
|
||||
// not path */
|
||||
// }
|
||||
// redir @canonicalPath {path}/ 308
|
||||
//
|
||||
// # If the requested file does not exist, try index files
|
||||
// @indexFiles file {
|
||||
// try_files {path} {path}/index.php index.php
|
||||
// split_path .php
|
||||
// }
|
||||
// rewrite @indexFiles {http.matchers.file.relative}
|
||||
//
|
||||
// # FrankenPHP!
|
||||
// @phpFiles path *.php
|
||||
// php @phpFiles
|
||||
// file_server
|
||||
//
|
||||
// parsePhpServer is freely inspired from the php_fastgci directive of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)
|
||||
func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
|
||||
if !h.Next() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
// set up FrankenPHP
|
||||
phpsrv := FrankenPHPModule{}
|
||||
|
||||
// set up file server
|
||||
fsrv := fileserver.FileServer{}
|
||||
disableFsrv := false
|
||||
|
||||
// set up the set of file extensions allowed to execute PHP code
|
||||
extensions := []string{".php"}
|
||||
|
||||
// set the default index file for the try_files rewrites
|
||||
indexFile := "index.php"
|
||||
|
||||
// set up for explicitly overriding try_files
|
||||
var tryFiles []string
|
||||
|
||||
// if the user specified a matcher token, use that
|
||||
// matcher in a route that wraps both of our routes;
|
||||
// either way, strip the matcher token and pass
|
||||
// the remaining tokens to the unmarshaler so that
|
||||
// we can gain the rest of the directive syntax
|
||||
userMatcherSet, err := h.ExtractMatcherSet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// make a new dispenser from the remaining tokens so that we
|
||||
// can reset the dispenser back to this point for the
|
||||
// php unmarshaler to read from it as well
|
||||
dispenser := h.NewFromNextSegment()
|
||||
|
||||
// read the subdirectives that we allow as overrides to
|
||||
// the php_server shortcut
|
||||
// NOTE: we delete the tokens as we go so that the php
|
||||
// unmarshal doesn't see these subdirectives which it cannot handle
|
||||
for dispenser.Next() {
|
||||
for dispenser.NextBlock(0) {
|
||||
// ignore any sub-subdirectives that might
|
||||
// have the same name somewhere within
|
||||
// the php passthrough tokens
|
||||
if dispenser.Nesting() != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// parse the php_server subdirectives
|
||||
switch dispenser.Val() {
|
||||
case "root":
|
||||
if !dispenser.NextArg() {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
phpsrv.Root = dispenser.Val()
|
||||
fsrv.Root = phpsrv.Root
|
||||
dispenser.DeleteN(2)
|
||||
|
||||
case "split":
|
||||
extensions = dispenser.RemainingArgs()
|
||||
dispenser.DeleteN(len(extensions) + 1)
|
||||
if len(extensions) == 0 {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
|
||||
case "index":
|
||||
args := dispenser.RemainingArgs()
|
||||
dispenser.DeleteN(len(args) + 1)
|
||||
if len(args) != 1 {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
indexFile = args[0]
|
||||
|
||||
case "try_files":
|
||||
args := dispenser.RemainingArgs()
|
||||
dispenser.DeleteN(len(args) + 1)
|
||||
if len(args) < 1 {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
tryFiles = args
|
||||
|
||||
case "file_server":
|
||||
args := dispenser.RemainingArgs()
|
||||
dispenser.DeleteN(len(args) + 1)
|
||||
if len(args) < 1 || args[0] != "off" {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
disableFsrv = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reset the dispenser after we're done so that the frankenphp
|
||||
// unmarshaler can read it from the start
|
||||
dispenser.Reset()
|
||||
|
||||
// the rest of the config is specified by the user
|
||||
// using the php directive syntax
|
||||
dispenser.Next() // consume the directive name
|
||||
if err := phpsrv.UnmarshalCaddyfile(dispenser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if frankenphp.EmbeddedAppPath != "" {
|
||||
if phpsrv.Root == "" {
|
||||
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
|
||||
fsrv.Root = phpsrv.Root
|
||||
rrs := false
|
||||
phpsrv.ResolveRootSymlink = &rrs
|
||||
} else if filepath.IsLocal(fsrv.Root) {
|
||||
phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)
|
||||
fsrv.Root = phpsrv.Root
|
||||
}
|
||||
}
|
||||
|
||||
// set up a route list that we'll append to
|
||||
routes := caddyhttp.RouteList{}
|
||||
|
||||
// prepend routes from the 'worker match *' directives
|
||||
routes = prependWorkerRoutes(routes, h, phpsrv, fsrv, disableFsrv)
|
||||
|
||||
// set the list of allowed path segments on which to split
|
||||
phpsrv.SplitPath = extensions
|
||||
|
||||
// if the index is turned off, we skip the redirect and try_files
|
||||
if indexFile != "off" {
|
||||
dirRedir := false
|
||||
dirIndex := "{http.request.uri.path}/" + indexFile
|
||||
tryPolicy := "first_exist_fallback"
|
||||
|
||||
// if tryFiles wasn't overridden, use a reasonable default
|
||||
if len(tryFiles) == 0 {
|
||||
if disableFsrv {
|
||||
tryFiles = []string{dirIndex, indexFile}
|
||||
} else {
|
||||
tryFiles = []string{"{http.request.uri.path}", dirIndex, indexFile}
|
||||
}
|
||||
|
||||
dirRedir = true
|
||||
} else {
|
||||
if !strings.HasSuffix(tryFiles[len(tryFiles)-1], ".php") {
|
||||
// use first_exist strategy if the last file is not a PHP file
|
||||
tryPolicy = ""
|
||||
}
|
||||
|
||||
if slices.Contains(tryFiles, dirIndex) {
|
||||
dirRedir = true
|
||||
}
|
||||
}
|
||||
|
||||
// route to redirect to canonical path if index PHP file
|
||||
if dirRedir {
|
||||
redirMatcherSet := caddy.ModuleMap{
|
||||
"file": h.JSON(fileserver.MatchFile{
|
||||
TryFiles: []string{dirIndex},
|
||||
Root: phpsrv.Root,
|
||||
}),
|
||||
"not": h.JSON(caddyhttp.MatchNot{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{
|
||||
{
|
||||
"path": h.JSON(caddyhttp.MatchPath{"*/"}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
redirHandler := caddyhttp.StaticResponse{
|
||||
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
|
||||
Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
|
||||
}
|
||||
redirRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
|
||||
}
|
||||
|
||||
routes = append(routes, redirRoute)
|
||||
}
|
||||
|
||||
// route to rewrite to PHP index file
|
||||
rewriteMatcherSet := caddy.ModuleMap{
|
||||
"file": h.JSON(fileserver.MatchFile{
|
||||
TryFiles: tryFiles,
|
||||
TryPolicy: tryPolicy,
|
||||
SplitPath: extensions,
|
||||
Root: phpsrv.Root,
|
||||
}),
|
||||
}
|
||||
rewriteHandler := rewrite.Rewrite{
|
||||
URI: "{http.matchers.file.relative}",
|
||||
}
|
||||
rewriteRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
|
||||
}
|
||||
|
||||
routes = append(routes, rewriteRoute)
|
||||
}
|
||||
|
||||
// route to actually pass requests to PHP files;
|
||||
// match only requests that are for PHP files
|
||||
var pathList []string
|
||||
for _, ext := range extensions {
|
||||
pathList = append(pathList, "*"+ext)
|
||||
}
|
||||
phpMatcherSet := caddy.ModuleMap{
|
||||
"path": h.JSON(pathList),
|
||||
}
|
||||
|
||||
// create the PHP route which is
|
||||
// conditional on matching PHP files
|
||||
phpRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(phpsrv, "handler", "php", nil)},
|
||||
}
|
||||
routes = append(routes, phpRoute)
|
||||
|
||||
// create the file server route
|
||||
if !disableFsrv {
|
||||
fileRoute := caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(fsrv, "handler", "file_server", nil)},
|
||||
}
|
||||
routes = append(routes, fileRoute)
|
||||
}
|
||||
|
||||
subroute := caddyhttp.Subroute{
|
||||
Routes: routes,
|
||||
}
|
||||
|
||||
// the user's matcher is a prerequisite for ours, so
|
||||
// wrap ours in a subroute and return that
|
||||
if userMatcherSet != nil {
|
||||
return []httpcaddyfile.ConfigValue{
|
||||
{
|
||||
Class: "route",
|
||||
Value: caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet},
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// otherwise, return the literal subroute instead of
|
||||
// individual routes, to ensure they stay together and
|
||||
// are treated as a single unit, without necessarily
|
||||
// creating an actual subroute in the output
|
||||
return []httpcaddyfile.ConfigValue{
|
||||
{
|
||||
Class: "route",
|
||||
Value: subroute,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// workers can also match a path without being in the public directory
|
||||
// in this case we need to prepend the worker routes to the existing routes
|
||||
func prependWorkerRoutes(routes caddyhttp.RouteList, h httpcaddyfile.Helper, f FrankenPHPModule, fsrv caddy.Module, disableFsrv bool) caddyhttp.RouteList {
|
||||
var allWorkerMatches caddyhttp.MatchPath
|
||||
for _, w := range f.Workers {
|
||||
for _, path := range w.MatchPath {
|
||||
allWorkerMatches = append(allWorkerMatches, path)
|
||||
}
|
||||
}
|
||||
|
||||
if len(allWorkerMatches) == 0 {
|
||||
return routes
|
||||
}
|
||||
|
||||
// if there are match patterns, we need to check for files beforehand
|
||||
if !disableFsrv {
|
||||
routes = append(routes, caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{
|
||||
{
|
||||
"file": h.JSON(fileserver.MatchFile{
|
||||
TryFiles: []string{"{http.request.uri.path}"},
|
||||
Root: f.Root,
|
||||
}),
|
||||
"not": h.JSON(caddyhttp.MatchNot{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{
|
||||
{"path": h.JSON(caddyhttp.MatchPath{"*.php"})},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
HandlersRaw: []json.RawMessage{
|
||||
caddyconfig.JSONModuleObject(fsrv, "handler", "file_server", nil),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// forward matching routes to the PHP handler
|
||||
routes = append(routes, caddyhttp.Route{
|
||||
MatcherSetsRaw: []caddy.ModuleMap{
|
||||
{"path": h.JSON(allWorkerMatches)},
|
||||
},
|
||||
HandlersRaw: []json.RawMessage{
|
||||
caddyconfig.JSONModuleObject(f, "handler", "php", nil),
|
||||
},
|
||||
})
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Provisioner = (*FrankenPHPModule)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
|
||||
_ caddyfile.Unmarshaler = (*FrankenPHPModule)(nil)
|
||||
)
|
||||
52
caddy/module_test.go
Normal file
52
caddy/module_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package caddy_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
)
|
||||
|
||||
func TestRootBehavesTheSameOutsideAndInsidePhpServer(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
testPortNum, _ := strconv.Atoi(testPort)
|
||||
testPortTwo := strconv.Itoa(testPortNum + 1)
|
||||
expectedFileResponse, _ := os.ReadFile("../testdata/files/static.txt")
|
||||
hostWithRootOutside := "http://localhost:" + testPort
|
||||
hostWithRootInside := "http://localhost:" + testPortTwo
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
}
|
||||
|
||||
`+hostWithRootOutside+` {
|
||||
root ../testdata
|
||||
php_server
|
||||
}
|
||||
|
||||
`+hostWithRootInside+` {
|
||||
php_server {
|
||||
root ../testdata
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
// serve a static file
|
||||
tester.AssertGetResponse(hostWithRootOutside+"/files/static.txt", http.StatusOK, string(expectedFileResponse))
|
||||
tester.AssertGetResponse(hostWithRootInside+"/files/static.txt", http.StatusOK, string(expectedFileResponse))
|
||||
|
||||
// serve a php file
|
||||
tester.AssertGetResponse(hostWithRootOutside+"/hello.php", http.StatusOK, "Hello from PHP")
|
||||
tester.AssertGetResponse(hostWithRootInside+"/hello.php", http.StatusOK, "Hello from PHP")
|
||||
|
||||
// fallback to index.php
|
||||
tester.AssertGetResponse(hostWithRootOutside+"/some-path", http.StatusOK, "I am by birth a Genevese (i not set)")
|
||||
tester.AssertGetResponse(hostWithRootInside+"/some-path", http.StatusOK, "I am by birth a Genevese (i not set)")
|
||||
|
||||
// fallback to directory index ('dirIndex' in module.go)
|
||||
tester.AssertGetResponse(hostWithRootOutside+"/dirindex/", http.StatusOK, "Hello from directory index.php")
|
||||
tester.AssertGetResponse(hostWithRootInside+"/dirindex/", http.StatusOK, "Hello from directory index.php")
|
||||
}
|
||||
@@ -25,7 +25,7 @@ Executes a PHP script similarly to the CLI SAPI.`,
|
||||
})
|
||||
}
|
||||
|
||||
func cmdPHPCLI(caddycmd.Flags) (int, error) {
|
||||
func cmdPHPCLI(fs caddycmd.Flags) (int, error) {
|
||||
args := os.Args[2:]
|
||||
if len(args) < 1 {
|
||||
return 1, errors.New("the path to the PHP script is required")
|
||||
@@ -37,7 +37,13 @@ func cmdPHPCLI(caddycmd.Flags) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
status := frankenphp.ExecuteScriptCLI(args[0], args)
|
||||
var status int
|
||||
if len(args) >= 2 && args[0] == "-r" {
|
||||
status = frankenphp.ExecutePHPCode(args[1])
|
||||
} else {
|
||||
status = frankenphp.ExecuteScriptCLI(args[0], args)
|
||||
}
|
||||
|
||||
os.Exit(status)
|
||||
|
||||
return status, nil
|
||||
|
||||
@@ -3,15 +3,14 @@ package caddy
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mercureModule "github.com/dunglas/mercure/caddy"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
@@ -29,7 +28,7 @@ import (
|
||||
func init() {
|
||||
caddycmd.RegisterCommand(caddycmd.Command{
|
||||
Name: "php-server",
|
||||
Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--worker /path/to/worker.php<,nb-workers>] [--watch <paths...>] [--access-log] [--debug] [--no-compress] [--mercure]",
|
||||
Usage: "[--domain=<example.com>] [--root=<path>] [--listen=<addr>] [--worker=/path/to/worker.php<,nb-workers>] [--watch[=<glob-pattern>]]... [--access-log] [--debug] [--no-compress] [--mercure]",
|
||||
Short: "Spins up a production-ready PHP server",
|
||||
Long: `
|
||||
A simple but production-ready PHP server. Useful for quick deployments,
|
||||
@@ -42,17 +41,20 @@ will be changed to the HTTPS port and the server will use HTTPS. If using
|
||||
a public domain, ensure A/AAAA records are properly configured before
|
||||
using this option.
|
||||
|
||||
For more advanced use cases, see https://github.com/dunglas/frankenphp/blob/main/docs/config.md`,
|
||||
For more advanced use cases, see https://github.com/php/frankenphp/blob/main/docs/config.md`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files")
|
||||
cmd.Flags().StringP("root", "r", "", "The path to the root of the site")
|
||||
cmd.Flags().StringP("listen", "l", "", "The address to which to bind the listener")
|
||||
cmd.Flags().StringArrayP("worker", "w", []string{}, "Worker script")
|
||||
cmd.Flags().StringArrayP("watch", "", []string{}, "Directory to watch for file changes")
|
||||
cmd.Flags().StringArray("watch", []string{}, "Glob pattern of directories and files to watch for changes")
|
||||
cmd.Flags().BoolP("access-log", "a", false, "Enable the access log")
|
||||
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
|
||||
cmd.Flags().BoolP("mercure", "m", false, "Enable the built-in Mercure.rocks hub")
|
||||
cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard, Brotli and Gzip compression")
|
||||
cmd.Flags().Bool("no-compress", false, "Disable Zstandard, Brotli and Gzip compression")
|
||||
|
||||
cmd.Flags().Lookup("watch").NoOptDefVal = defaultWatchPattern
|
||||
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdPHPServer)
|
||||
},
|
||||
})
|
||||
@@ -79,27 +81,30 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if frankenphp.EmbeddedAppPath != "" {
|
||||
if err := os.Chdir(frankenphp.EmbeddedAppPath); err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
}
|
||||
|
||||
var workersOption []workerConfig
|
||||
if len(workers) != 0 {
|
||||
workersOption = make([]workerConfig, 0, len(workers))
|
||||
for _, worker := range workers {
|
||||
parts := strings.SplitN(worker, ",", 2)
|
||||
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(parts[0]) {
|
||||
parts[0] = filepath.Join(frankenphp.EmbeddedAppPath, parts[0])
|
||||
}
|
||||
|
||||
var num int
|
||||
var num uint64
|
||||
if len(parts) > 1 {
|
||||
num, _ = strconv.Atoi(parts[1])
|
||||
num, _ = strconv.ParseUint(parts[1], 10, 32)
|
||||
}
|
||||
|
||||
workersOption = append(workersOption, workerConfig{FileName: parts[0], Num: num})
|
||||
workersOption = append(workersOption, workerConfig{FileName: parts[0], Num: int(num)})
|
||||
}
|
||||
workersOption[0].Watch = watch
|
||||
}
|
||||
|
||||
if frankenphp.EmbeddedAppPath != "" {
|
||||
if _, err := os.Stat(filepath.Join(frankenphp.EmbeddedAppPath, "php.ini")); err == nil {
|
||||
if _, err := os.Stat("php.ini"); err == nil {
|
||||
iniScanDir := os.Getenv("PHP_INI_SCAN_DIR")
|
||||
|
||||
if err := os.Setenv("PHP_INI_SCAN_DIR", iniScanDir+":"+frankenphp.EmbeddedAppPath); err != nil {
|
||||
@@ -107,8 +112,8 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(frankenphp.EmbeddedAppPath, "Caddyfile")); err == nil {
|
||||
config, _, err := caddycmd.LoadConfig(filepath.Join(frankenphp.EmbeddedAppPath, "Caddyfile"), "")
|
||||
if _, err := os.Stat("Caddyfile"); err == nil {
|
||||
config, _, err := caddycmd.LoadConfig("Caddyfile", "caddyfile")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@@ -121,9 +126,7 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
|
||||
}
|
||||
|
||||
if root == "" {
|
||||
root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
|
||||
} else if filepath.IsLocal(root) {
|
||||
root = filepath.Join(frankenphp.EmbeddedAppPath, root)
|
||||
root = defaultDocumentRoot
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +213,7 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
|
||||
}
|
||||
|
||||
br, err := caddy.GetModule("http.encoders.br")
|
||||
if err != nil {
|
||||
if err != nil && brotli {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
@@ -334,7 +337,7 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
|
||||
cfg.Logging = &caddy.Logging{
|
||||
Logs: map[string]*caddy.CustomLog{
|
||||
"default": {
|
||||
BaseLog: caddy.BaseLog{Level: zapcore.DebugLevel.CapitalString()},
|
||||
BaseLog: caddy.BaseLog{Level: slog.LevelDebug.String()},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
185
caddy/workerconfig.go
Normal file
185
caddy/workerconfig.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||
)
|
||||
|
||||
// workerConfig represents the "worker" directive in the Caddyfile
|
||||
// it can appear in the "frankenphp", "php_server" and "php" directives
|
||||
//
|
||||
// frankenphp {
|
||||
// worker {
|
||||
// name "my-worker"
|
||||
// file "my-worker.php"
|
||||
// }
|
||||
// }
|
||||
type workerConfig struct {
|
||||
mercureContext
|
||||
|
||||
// Name for the worker. Default: the filename for FrankenPHPApp workers, always prefixed with "m#" for FrankenPHPModule workers.
|
||||
Name string `json:"name,omitempty"`
|
||||
// FileName sets the path to the worker script.
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
// Num sets the number of workers to start.
|
||||
Num int `json:"num,omitempty"`
|
||||
// MaxThreads sets the maximum number of threads for this worker.
|
||||
MaxThreads int `json:"max_threads,omitempty"`
|
||||
// Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
// Directories to watch for file changes
|
||||
Watch []string `json:"watch,omitempty"`
|
||||
// The path to match against the worker
|
||||
MatchPath []string `json:"match_path,omitempty"`
|
||||
// MaxConsecutiveFailures sets the maximum number of consecutive failures before panicking (defaults to 6, set to -1 to never panick)
|
||||
MaxConsecutiveFailures int `json:"max_consecutive_failures,omitempty"`
|
||||
|
||||
options []frankenphp.WorkerOption
|
||||
requestOptions []frankenphp.RequestOption
|
||||
}
|
||||
|
||||
func unmarshalWorker(d *caddyfile.Dispenser) (workerConfig, error) {
|
||||
wc := workerConfig{}
|
||||
if d.NextArg() {
|
||||
wc.FileName = d.Val()
|
||||
}
|
||||
|
||||
if d.NextArg() {
|
||||
if d.Val() == "watch" {
|
||||
wc.Watch = append(wc.Watch, defaultWatchPattern)
|
||||
} else {
|
||||
v, err := strconv.ParseUint(d.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return wc, err
|
||||
}
|
||||
|
||||
wc.Num = int(v)
|
||||
}
|
||||
}
|
||||
|
||||
if d.NextArg() {
|
||||
return wc, d.Errf(`FrankenPHP: too many "worker" arguments: %s`, d.Val())
|
||||
}
|
||||
|
||||
for d.NextBlock(1) {
|
||||
switch v := d.Val(); v {
|
||||
case "name":
|
||||
if !d.NextArg() {
|
||||
return wc, d.ArgErr()
|
||||
}
|
||||
wc.Name = d.Val()
|
||||
case "file":
|
||||
if !d.NextArg() {
|
||||
return wc, d.ArgErr()
|
||||
}
|
||||
wc.FileName = d.Val()
|
||||
case "num":
|
||||
if !d.NextArg() {
|
||||
return wc, d.ArgErr()
|
||||
}
|
||||
|
||||
v, err := strconv.ParseUint(d.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return wc, d.WrapErr(err)
|
||||
}
|
||||
|
||||
wc.Num = int(v)
|
||||
case "max_threads":
|
||||
if !d.NextArg() {
|
||||
return wc, d.ArgErr()
|
||||
}
|
||||
|
||||
v, err := strconv.ParseUint(d.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return wc, d.WrapErr(err)
|
||||
}
|
||||
|
||||
wc.MaxThreads = int(v)
|
||||
case "env":
|
||||
args := d.RemainingArgs()
|
||||
if len(args) != 2 {
|
||||
return wc, d.ArgErr()
|
||||
}
|
||||
if wc.Env == nil {
|
||||
wc.Env = make(map[string]string)
|
||||
}
|
||||
wc.Env[args[0]] = args[1]
|
||||
case "watch":
|
||||
patterns := d.RemainingArgs()
|
||||
if len(patterns) == 0 {
|
||||
// the default if the watch directory is left empty:
|
||||
wc.Watch = append(wc.Watch, defaultWatchPattern)
|
||||
} else {
|
||||
wc.Watch = append(wc.Watch, patterns...)
|
||||
}
|
||||
case "match":
|
||||
// provision the path so it's identical to Caddy match rules
|
||||
// see: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/matchers.go
|
||||
caddyMatchPath := (caddyhttp.MatchPath)(d.RemainingArgs())
|
||||
if err := caddyMatchPath.Provision(caddy.Context{}); err != nil {
|
||||
return wc, d.WrapErr(err)
|
||||
}
|
||||
|
||||
wc.MatchPath = caddyMatchPath
|
||||
case "max_consecutive_failures":
|
||||
if !d.NextArg() {
|
||||
return wc, d.ArgErr()
|
||||
}
|
||||
|
||||
v, err := strconv.Atoi(d.Val())
|
||||
if err != nil {
|
||||
return wc, d.WrapErr(err)
|
||||
}
|
||||
if v < -1 {
|
||||
return wc, d.Errf("max_consecutive_failures must be >= -1")
|
||||
}
|
||||
|
||||
wc.MaxConsecutiveFailures = v
|
||||
default:
|
||||
return wc, wrongSubDirectiveError("worker", "name, file, num, env, watch, match, max_consecutive_failures, max_threads", v)
|
||||
}
|
||||
}
|
||||
|
||||
if wc.FileName == "" {
|
||||
return wc, d.Err(`the "file" argument must be specified`)
|
||||
}
|
||||
|
||||
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
|
||||
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
|
||||
}
|
||||
|
||||
return wc, nil
|
||||
}
|
||||
|
||||
func (wc *workerConfig) inheritEnv(env map[string]string) {
|
||||
if wc.Env == nil {
|
||||
wc.Env = make(map[string]string, len(env))
|
||||
}
|
||||
for k, v := range env {
|
||||
// do not overwrite existing environment variables
|
||||
if _, exists := wc.Env[k]; !exists {
|
||||
wc.Env[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *workerConfig) matchesPath(r *http.Request, documentRoot string) bool {
|
||||
|
||||
// try to match against a pattern if one is assigned
|
||||
if len(wc.MatchPath) != 0 {
|
||||
return (caddyhttp.MatchPath)(wc.MatchPath).Match(r)
|
||||
}
|
||||
|
||||
// if there is no pattern, try to match against the actual path (in the public directory)
|
||||
fullScriptPath, _ := fastabs.FastAbs(documentRoot + "/" + r.URL.Path)
|
||||
absFileName, _ := fastabs.FastAbs(wc.FileName)
|
||||
|
||||
return fullScriptPath == absFileName
|
||||
}
|
||||
123
cgi.go
123
cgi.go
@@ -12,6 +12,7 @@ package frankenphp
|
||||
// #include "frankenphp.h"
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -22,6 +23,16 @@ import (
|
||||
"github.com/dunglas/frankenphp/internal/phpheaders"
|
||||
)
|
||||
|
||||
// Protocol versions, in Apache mod_ssl format: https://httpd.apache.org/docs/current/mod/mod_ssl.html
|
||||
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
|
||||
var tlsProtocolStrings = map[uint16]string{
|
||||
tls.VersionTLS10: "TLSv1",
|
||||
tls.VersionTLS11: "TLSv1.1",
|
||||
tls.VersionTLS12: "TLSv1.2",
|
||||
tls.VersionTLS13: "TLSv1.3",
|
||||
}
|
||||
|
||||
// Known $_SERVER keys
|
||||
var knownServerKeys = []string{
|
||||
"CONTENT_LENGTH",
|
||||
"DOCUMENT_ROOT",
|
||||
@@ -42,6 +53,7 @@ var knownServerKeys = []string{
|
||||
"SERVER_PROTOCOL",
|
||||
"SERVER_SOFTWARE",
|
||||
"SSL_PROTOCOL",
|
||||
"SSL_CIPHER",
|
||||
"AUTH_TYPE",
|
||||
"REMOTE_IDENT",
|
||||
"CONTENT_TYPE",
|
||||
@@ -56,7 +68,8 @@ var knownServerKeys = []string{
|
||||
//
|
||||
// TODO: handle this case https://github.com/caddyserver/caddy/issues/3718
|
||||
// Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
|
||||
func addKnownVariablesToServer(thread *phpThread, request *http.Request, fc *FrankenPHPContext, trackVarsArray *C.zval) {
|
||||
func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
|
||||
request := fc.request
|
||||
keys := mainThread.knownServerKeys
|
||||
// Separate remote IP and port; more lenient than net.SplitHostPort
|
||||
var ip, port string
|
||||
@@ -71,13 +84,13 @@ func addKnownVariablesToServer(thread *phpThread, request *http.Request, fc *Fra
|
||||
ip = strings.Replace(ip, "[", "", 1)
|
||||
ip = strings.Replace(ip, "]", "", 1)
|
||||
|
||||
var https string
|
||||
var sslProtocol string
|
||||
var rs string
|
||||
var https, sslProtocol, sslCipher, rs string
|
||||
|
||||
if request.TLS == nil {
|
||||
rs = "http"
|
||||
https = ""
|
||||
sslProtocol = ""
|
||||
sslCipher = ""
|
||||
} else {
|
||||
rs = "https"
|
||||
https = "on"
|
||||
@@ -89,6 +102,10 @@ func addKnownVariablesToServer(thread *phpThread, request *http.Request, fc *Fra
|
||||
} else {
|
||||
sslProtocol = ""
|
||||
}
|
||||
|
||||
if request.TLS.CipherSuite != 0 {
|
||||
sslCipher = tls.CipherSuiteName(request.TLS.CipherSuite)
|
||||
}
|
||||
}
|
||||
|
||||
reqHost, reqPort, _ := net.SplitHostPort(request.Host)
|
||||
@@ -151,6 +168,7 @@ func addKnownVariablesToServer(thread *phpThread, request *http.Request, fc *Fra
|
||||
packCgiVariable(keys["REMOTE_IDENT"], ""),
|
||||
// Request uri of the original request
|
||||
packCgiVariable(keys["REQUEST_URI"], requestURI),
|
||||
packCgiVariable(keys["SSL_CIPHER"], sslCipher),
|
||||
)
|
||||
|
||||
// These values are already present in the SG(request_info), so we'll register them from there
|
||||
@@ -168,7 +186,7 @@ func packCgiVariable(key *C.zend_string, value string) C.ht_key_value_pair {
|
||||
return C.ht_key_value_pair{key, toUnsafeChar(value), C.size_t(len(value))}
|
||||
}
|
||||
|
||||
func addHeadersToServer(request *http.Request, thread *phpThread, fc *FrankenPHPContext, trackVarsArray *C.zval) {
|
||||
func addHeadersToServer(ctx context.Context, request *http.Request, trackVarsArray *C.zval) {
|
||||
for field, val := range request.Header {
|
||||
if k := mainThread.commonHeaders[field]; k != nil {
|
||||
v := strings.Join(val, ", ")
|
||||
@@ -178,13 +196,13 @@ func addHeadersToServer(request *http.Request, thread *phpThread, fc *FrankenPHP
|
||||
|
||||
// if the header name could not be cached, it needs to be registered safely
|
||||
// this is more inefficient but allows additional sanitizing by PHP
|
||||
k := phpheaders.GetUnCommonHeader(field)
|
||||
k := phpheaders.GetUnCommonHeader(ctx, field)
|
||||
v := strings.Join(val, ", ")
|
||||
C.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
|
||||
}
|
||||
}
|
||||
|
||||
func addPreparedEnvToServer(fc *FrankenPHPContext, trackVarsArray *C.zval) {
|
||||
func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
|
||||
for k, v := range fc.env {
|
||||
C.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
|
||||
}
|
||||
@@ -194,28 +212,60 @@ func addPreparedEnvToServer(fc *FrankenPHPContext, trackVarsArray *C.zval) {
|
||||
//export go_register_variables
|
||||
func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
|
||||
thread := phpThreads[threadIndex]
|
||||
r := thread.getActiveRequest()
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
fc := thread.frankenPHPContext()
|
||||
|
||||
addKnownVariablesToServer(thread, r, fc, trackVarsArray)
|
||||
addHeadersToServer(r, thread, fc, trackVarsArray)
|
||||
if fc.request != nil {
|
||||
addKnownVariablesToServer(fc, trackVarsArray)
|
||||
addHeadersToServer(thread.context(), fc.request, trackVarsArray)
|
||||
}
|
||||
|
||||
// The Prepared Environment is registered last and can overwrite any previous values
|
||||
addPreparedEnvToServer(fc, trackVarsArray)
|
||||
}
|
||||
|
||||
// splitCgiPath splits the request path into SCRIPT_NAME, SCRIPT_FILENAME, PATH_INFO, DOCUMENT_URI
|
||||
func splitCgiPath(fc *frankenPHPContext) {
|
||||
path := fc.request.URL.Path
|
||||
splitPath := fc.splitPath
|
||||
|
||||
if splitPath == nil {
|
||||
splitPath = []string{".php"}
|
||||
}
|
||||
|
||||
if splitPos := splitPos(path, splitPath); splitPos > -1 {
|
||||
fc.docURI = path[:splitPos]
|
||||
fc.pathInfo = path[splitPos:]
|
||||
|
||||
// Strip PATH_INFO from SCRIPT_NAME
|
||||
fc.scriptName = strings.TrimSuffix(path, fc.pathInfo)
|
||||
|
||||
// Ensure the SCRIPT_NAME has a leading slash for compliance with RFC3875
|
||||
// Info: https://tools.ietf.org/html/rfc3875#section-4.1.13
|
||||
if fc.scriptName != "" && !strings.HasPrefix(fc.scriptName, "/") {
|
||||
fc.scriptName = "/" + fc.scriptName
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: is it possible to delay this and avoid saving everything in the context?
|
||||
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
||||
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
|
||||
fc.worker = getWorkerByPath(fc.scriptFilename)
|
||||
}
|
||||
|
||||
// splitPos returns the index where path should
|
||||
// be split based on SplitPath.
|
||||
// example: if splitPath is [".php"]
|
||||
// "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path")
|
||||
//
|
||||
// Adapted from https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
func splitPos(fc *FrankenPHPContext, path string) int {
|
||||
if len(fc.splitPath) == 0 {
|
||||
func splitPos(path string, splitPath []string) int {
|
||||
if len(splitPath) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
lowerPath := strings.ToLower(path)
|
||||
for _, split := range fc.splitPath {
|
||||
for _, split := range splitPath {
|
||||
if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 {
|
||||
return idx + len(split)
|
||||
}
|
||||
@@ -223,13 +273,44 @@ func splitPos(fc *FrankenPHPContext, path string) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
// Map of supported protocols to Apache ssl_mod format
|
||||
// Note that these are slightly different from SupportedProtocols in caddytls/config.go
|
||||
var tlsProtocolStrings = map[uint16]string{
|
||||
tls.VersionTLS10: "TLSv1",
|
||||
tls.VersionTLS11: "TLSv1.1",
|
||||
tls.VersionTLS12: "TLSv1.2",
|
||||
tls.VersionTLS13: "TLSv1.3",
|
||||
// go_update_request_info updates the sapi_request_info struct
|
||||
// See: https://github.com/php/php-src/blob/345e04b619c3bc11ea17ee02cdecad6ae8ce5891/main/SAPI.h#L72
|
||||
//
|
||||
//export go_update_request_info
|
||||
func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) {
|
||||
thread := phpThreads[threadIndex]
|
||||
fc := thread.frankenPHPContext()
|
||||
request := fc.request
|
||||
|
||||
if request == nil {
|
||||
return
|
||||
}
|
||||
|
||||
authUser, authPassword, ok := request.BasicAuth()
|
||||
if ok {
|
||||
if authPassword != "" {
|
||||
info.auth_password = thread.pinCString(authPassword)
|
||||
}
|
||||
if authUser != "" {
|
||||
info.auth_user = thread.pinCString(authUser)
|
||||
}
|
||||
}
|
||||
|
||||
info.request_method = thread.pinCString(request.Method)
|
||||
info.query_string = thread.pinCString(request.URL.RawQuery)
|
||||
info.content_length = C.zend_long(request.ContentLength)
|
||||
|
||||
if contentType := request.Header.Get("Content-Type"); contentType != "" {
|
||||
info.content_type = thread.pinCString(contentType)
|
||||
}
|
||||
|
||||
if fc.pathInfo != "" {
|
||||
info.path_translated = thread.pinCString(sanitizedPathJoin(fc.documentRoot, fc.pathInfo)) // See: http://www.oreilly.com/openbook/cgi/ch02_04.html
|
||||
}
|
||||
|
||||
info.request_uri = thread.pinCString(request.URL.RequestURI())
|
||||
|
||||
info.proto_num = C.int(request.ProtoMajor*1000 + request.ProtoMinor)
|
||||
}
|
||||
|
||||
// SanitizedPathJoin performs filepath.Join(root, reqPath) that
|
||||
|
||||
9
cgo.go
Normal file
9
cgo.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package frankenphp
|
||||
|
||||
// #cgo darwin pkg-config: libxml-2.0
|
||||
// #cgo CFLAGS: -Wall -Werror
|
||||
// #cgo linux CFLAGS: -D_GNU_SOURCE
|
||||
// #cgo LDFLAGS: -lphp -lm -lutil
|
||||
// #cgo linux LDFLAGS: -ldl -lresolv
|
||||
// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -liconv -ldl
|
||||
import "C"
|
||||
186
context.go
Normal file
186
context.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// frankenPHPContext provides contextual information about the Request to handle.
|
||||
type frankenPHPContext struct {
|
||||
mercureContext
|
||||
|
||||
documentRoot string
|
||||
splitPath []string
|
||||
env PreparedEnv
|
||||
logger *slog.Logger
|
||||
request *http.Request
|
||||
originalRequest *http.Request
|
||||
worker *worker
|
||||
|
||||
docURI string
|
||||
pathInfo string
|
||||
scriptName string
|
||||
scriptFilename string
|
||||
|
||||
// Whether the request is already closed by us
|
||||
isDone bool
|
||||
|
||||
responseWriter http.ResponseWriter
|
||||
handlerParameters any
|
||||
handlerReturn any
|
||||
|
||||
done chan any
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
type contextHolder struct {
|
||||
ctx context.Context
|
||||
frankenPHPContext *frankenPHPContext
|
||||
}
|
||||
|
||||
// fromContext extracts the frankenPHPContext from a context.
|
||||
func fromContext(ctx context.Context) (fctx *frankenPHPContext, ok bool) {
|
||||
fctx, ok = ctx.Value(contextKey).(*frankenPHPContext)
|
||||
return
|
||||
}
|
||||
|
||||
func newFrankenPHPContext() *frankenPHPContext {
|
||||
return &frankenPHPContext{
|
||||
done: make(chan any),
|
||||
startedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewRequestWithContext creates a new FrankenPHP request context.
|
||||
func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Request, error) {
|
||||
fc := newFrankenPHPContext()
|
||||
fc.request = r
|
||||
|
||||
for _, o := range opts {
|
||||
if err := o(fc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if fc.logger == nil {
|
||||
fc.logger = globalLogger
|
||||
}
|
||||
|
||||
if fc.documentRoot == "" {
|
||||
if EmbeddedAppPath != "" {
|
||||
fc.documentRoot = EmbeddedAppPath
|
||||
} else {
|
||||
var err error
|
||||
if fc.documentRoot, err = os.Getwd(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a worker is already assigned explicitly, use its filename and skip parsing path variables
|
||||
if fc.worker != nil {
|
||||
fc.scriptFilename = fc.worker.fileName
|
||||
} else {
|
||||
// If no worker was assigned, split the path into the "traditional" CGI path variables.
|
||||
// This needs to already happen here in case a worker script still matches the path.
|
||||
splitCgiPath(fc)
|
||||
}
|
||||
|
||||
c := context.WithValue(r.Context(), contextKey, fc)
|
||||
|
||||
return r.WithContext(c), nil
|
||||
}
|
||||
|
||||
// newDummyContext creates a fake context from a request path
|
||||
func newDummyContext(requestPath string, opts ...RequestOption) (*frankenPHPContext, error) {
|
||||
r, err := http.NewRequestWithContext(globalCtx, http.MethodGet, requestPath, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fr, err := NewRequestWithContext(r, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fc, _ := fromContext(fr.Context())
|
||||
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
// closeContext sends the response to the client
|
||||
func (fc *frankenPHPContext) closeContext() {
|
||||
if fc.isDone {
|
||||
return
|
||||
}
|
||||
|
||||
close(fc.done)
|
||||
fc.isDone = true
|
||||
}
|
||||
|
||||
// validate checks if the request should be outright rejected
|
||||
func (fc *frankenPHPContext) validate() error {
|
||||
if strings.Contains(fc.request.URL.Path, "\x00") {
|
||||
fc.reject(ErrInvalidRequestPath)
|
||||
|
||||
return ErrInvalidRequestPath
|
||||
}
|
||||
|
||||
contentLengthStr := fc.request.Header.Get("Content-Length")
|
||||
if contentLengthStr != "" {
|
||||
if contentLength, err := strconv.Atoi(contentLengthStr); err != nil || contentLength < 0 {
|
||||
e := fmt.Errorf("%w: %q", ErrInvalidContentLengthHeader, contentLengthStr)
|
||||
|
||||
fc.reject(e)
|
||||
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fc *frankenPHPContext) clientHasClosed() bool {
|
||||
if fc.request == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
select {
|
||||
case <-fc.request.Context().Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// reject sends a response with the given status code and error
|
||||
func (fc *frankenPHPContext) reject(err error) {
|
||||
if fc.isDone {
|
||||
return
|
||||
}
|
||||
|
||||
re := &ErrRejected{}
|
||||
if !errors.As(err, re) {
|
||||
// Should never happen
|
||||
panic("only instance of ErrRejected can be passed to reject")
|
||||
}
|
||||
|
||||
rw := fc.responseWriter
|
||||
if rw != nil {
|
||||
rw.WriteHeader(re.status)
|
||||
_, _ = rw.Write([]byte(err.Error()))
|
||||
|
||||
if f, ok := rw.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
fc.closeContext()
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"github.com/dunglas/frankenphp/internal/state"
|
||||
)
|
||||
|
||||
// EXPERIMENTAL: ThreadDebugState prints the state of a single PHP thread - debugging purposes only
|
||||
type ThreadDebugState struct {
|
||||
Index int
|
||||
@@ -23,7 +27,7 @@ func DebugState() FrankenPHPDebugState {
|
||||
ReservedThreadCount: 0,
|
||||
}
|
||||
for _, thread := range phpThreads {
|
||||
if thread.state.is(stateReserved) {
|
||||
if thread.state.Is(state.Reserved) {
|
||||
fullState.ReservedThreadCount++
|
||||
continue
|
||||
}
|
||||
@@ -37,10 +41,10 @@ func DebugState() FrankenPHPDebugState {
|
||||
func threadDebugState(thread *phpThread) ThreadDebugState {
|
||||
return ThreadDebugState{
|
||||
Index: thread.threadIndex,
|
||||
Name: thread.handler.name(),
|
||||
State: thread.state.name(),
|
||||
IsWaiting: thread.state.isInWaitingState(),
|
||||
IsBusy: !thread.state.isInWaitingState(),
|
||||
WaitingSinceMilliseconds: thread.state.waitTime(),
|
||||
Name: thread.name(),
|
||||
State: thread.state.Name(),
|
||||
IsWaiting: thread.state.IsInWaitingState(),
|
||||
IsBusy: !thread.state.IsInWaitingState(),
|
||||
WaitingSinceMilliseconds: thread.state.WaitTime(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
#checkov:skip=CKV_DOCKER_2
|
||||
#checkov:skip=CKV_DOCKER_3
|
||||
FROM golang:1.24-alpine
|
||||
FROM golang:1.25-alpine
|
||||
|
||||
ENV GOTOOLCHAIN=local
|
||||
ENV CFLAGS="-ggdb3"
|
||||
ENV PHPIZE_DEPS="\
|
||||
autoconf \
|
||||
@@ -31,7 +32,7 @@ RUN apk add --no-cache \
|
||||
zlib-dev \
|
||||
bison \
|
||||
nss-tools \
|
||||
# file watcher
|
||||
# file watcher \
|
||||
libstdc++ \
|
||||
linux-headers \
|
||||
# Dev tools \
|
||||
@@ -47,27 +48,30 @@ RUN apk add --no-cache \
|
||||
echo 'set auto-load safe-path /' > /root/.gdbinit
|
||||
|
||||
WORKDIR /usr/local/src/php
|
||||
RUN git clone --branch=PHP-8.4 https://github.com/php/php-src.git . && \
|
||||
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
|
||||
RUN git clone --branch=PHP-8.5 https://github.com/php/php-src.git . && \
|
||||
# --enable-embed is necessary to generate libphp.so, but we don't use this SAPI directly
|
||||
./buildconf --force && \
|
||||
./configure \
|
||||
EXTENSION_DIR=/usr/lib/frankenphp/modules ./configure \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--enable-zend-max-execution-timers \
|
||||
--with-config-file-path=/etc/frankenphp/php.ini \
|
||||
--with-config-file-scan-dir=/etc/frankenphp/php.d \
|
||||
--enable-debug && \
|
||||
make -j"$(nproc)" && \
|
||||
make install && \
|
||||
ldconfig /etc/ld.so.conf.d && \
|
||||
cp php.ini-development /usr/local/lib/php.ini && \
|
||||
echo "zend_extension=opcache.so" >> /usr/local/lib/php.ini && \
|
||||
echo "opcache.enable=1" >> /usr/local/lib/php.ini && \
|
||||
mkdir -p /etc/frankenphp/php.d && \
|
||||
cp php.ini-development /etc/frankenphp/php.ini && \
|
||||
echo "zend_extension=opcache.so" >> /etc/frankenphp/php.ini && \
|
||||
echo "opcache.enable=1" >> /etc/frankenphp/php.ini && \
|
||||
php --version
|
||||
|
||||
# Install e-dant/watcher (necessary for file watching)
|
||||
WORKDIR /usr/local/src/watcher
|
||||
RUN git clone https://github.com/e-dant/watcher . && \
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
|
||||
cmake --build build/ && \
|
||||
cmake --install build
|
||||
|
||||
@@ -75,7 +79,7 @@ WORKDIR /go/src/app
|
||||
COPY . .
|
||||
|
||||
WORKDIR /go/src/app/caddy/frankenphp
|
||||
RUN go build
|
||||
RUN ../../go.sh build -buildvcs=false
|
||||
|
||||
WORKDIR /go/src/app
|
||||
CMD [ "zsh" ]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
#checkov:skip=CKV_DOCKER_2
|
||||
#checkov:skip=CKV_DOCKER_3
|
||||
FROM golang:1.24
|
||||
FROM golang:1.25
|
||||
|
||||
ENV GOTOOLCHAIN=local
|
||||
ENV CFLAGS="-ggdb3"
|
||||
ENV PHPIZE_DEPS="\
|
||||
autoconf \
|
||||
@@ -49,21 +50,24 @@ RUN apt-get update && \
|
||||
apt-get clean
|
||||
|
||||
WORKDIR /usr/local/src/php
|
||||
RUN git clone --branch=PHP-8.4 https://github.com/php/php-src.git . && \
|
||||
RUN git clone --branch=PHP-8.5 https://github.com/php/php-src.git . && \
|
||||
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
|
||||
./buildconf --force && \
|
||||
./configure \
|
||||
EXTENSION_DIR=/usr/lib/frankenphp/modules ./configure \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--enable-zend-max-execution-timers \
|
||||
--with-config-file-path=/etc/frankenphp/php.ini \
|
||||
--with-config-file-scan-dir=/etc/frankenphp/php.d \
|
||||
--enable-debug && \
|
||||
make -j"$(nproc)" && \
|
||||
make install && \
|
||||
ldconfig && \
|
||||
cp php.ini-development /usr/local/lib/php.ini && \
|
||||
echo "zend_extension=opcache.so" >> /usr/local/lib/php.ini && \
|
||||
echo "opcache.enable=1" >> /usr/local/lib/php.ini && \
|
||||
mkdir -p /etc/frankenphp/php.d && \
|
||||
cp php.ini-development /etc/frankenphp/php.ini && \
|
||||
echo "zend_extension=opcache.so" >> /etc/frankenphp/php.ini && \
|
||||
echo "opcache.enable=1" >> /etc/frankenphp/php.ini && \
|
||||
php --version
|
||||
|
||||
# Install e-dant/watcher (necessary for file watching)
|
||||
@@ -76,10 +80,10 @@ RUN git clone https://github.com/e-dant/watcher . && \
|
||||
ldconfig
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY . .
|
||||
COPY --link . ./
|
||||
|
||||
WORKDIR /go/src/app/caddy/frankenphp
|
||||
RUN go build -buildvcs=false -tags 'nobadger,nomysql,nopgx'
|
||||
RUN ../../go.sh build -buildvcs=false
|
||||
|
||||
WORKDIR /go/src/app
|
||||
CMD [ "zsh" ]
|
||||
|
||||
@@ -7,11 +7,15 @@ variable "VERSION" {
|
||||
}
|
||||
|
||||
variable "PHP_VERSION" {
|
||||
default = "8.2,8.3,8.4"
|
||||
default = "8.2,8.3,8.4,8.5"
|
||||
}
|
||||
|
||||
variable "GO_VERSION" {
|
||||
default = "1.24"
|
||||
default = "1.25"
|
||||
}
|
||||
|
||||
variable "SPC_OPT_BUILD_ARGS" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "SHA" {}
|
||||
@@ -24,17 +28,22 @@ variable "CACHE" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "CI" {
|
||||
# CI flag coming from the environment or --set; empty by default
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable DEFAULT_PHP_VERSION {
|
||||
default = "8.4"
|
||||
default = "8.5"
|
||||
}
|
||||
|
||||
function "tag" {
|
||||
params = [version, os, php-version, tgt]
|
||||
result = [
|
||||
version == "" ? "" : "${IMAGE_NAME}:${trimprefix("${version}${tgt == "builder" ? "-builder" : ""}-php${php-version}-${os}", "latest-")}",
|
||||
php-version == DEFAULT_PHP_VERSION && os == "bookworm" && version != "" ? "${IMAGE_NAME}:${trimprefix("${version}${tgt == "builder" ? "-builder" : ""}", "latest-")}" : "",
|
||||
php-version == DEFAULT_PHP_VERSION && os == "trixie" && version != "" ? "${IMAGE_NAME}:${trimprefix("${version}${tgt == "builder" ? "-builder" : ""}", "latest-")}" : "",
|
||||
php-version == DEFAULT_PHP_VERSION && version != "" ? "${IMAGE_NAME}:${trimprefix("${version}${tgt == "builder" ? "-builder" : ""}-${os}", "latest-")}" : "",
|
||||
os == "bookworm" && version != "" ? "${IMAGE_NAME}:${trimprefix("${version}${tgt == "builder" ? "-builder" : ""}-php${php-version}", "latest-")}" : "",
|
||||
os == "trixie" && version != "" ? "${IMAGE_NAME}:${trimprefix("${version}${tgt == "builder" ? "-builder" : ""}-php${php-version}", "latest-")}" : "",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -76,7 +85,7 @@ function "_php_version" {
|
||||
target "default" {
|
||||
name = "${tgt}-php-${replace(php-version, ".", "-")}-${os}"
|
||||
matrix = {
|
||||
os = ["bookworm", "alpine"]
|
||||
os = ["trixie", "bookworm", "alpine"]
|
||||
php-version = split(",", PHP_VERSION)
|
||||
tgt = ["builder", "runner"]
|
||||
}
|
||||
@@ -91,8 +100,7 @@ target "default" {
|
||||
platforms = os == "alpine" ? [
|
||||
"linux/amd64",
|
||||
"linux/386",
|
||||
# FIXME: armv6 doesn't build in GitHub actions because we use a custom Go build
|
||||
#"linux/arm/v6",
|
||||
"linux/arm/v6",
|
||||
"linux/arm/v7",
|
||||
"linux/arm64",
|
||||
] : [
|
||||
@@ -116,22 +124,23 @@ target "default" {
|
||||
args = {
|
||||
FRANKENPHP_VERSION = VERSION
|
||||
}
|
||||
secret = ["id=github-token,env=GITHUB_TOKEN"]
|
||||
}
|
||||
|
||||
target "static-builder" {
|
||||
target "static-builder-musl" {
|
||||
contexts = {
|
||||
golang-base = "docker-image://golang:${GO_VERSION}-alpine"
|
||||
}
|
||||
dockerfile = "static-builder.Dockerfile"
|
||||
dockerfile = "static-builder-musl.Dockerfile"
|
||||
context = "./"
|
||||
platforms = [
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
]
|
||||
tags = distinct(flatten([
|
||||
LATEST ? "${IMAGE_NAME}:static-builder" : "",
|
||||
SHA == "" || VERSION != "dev" ? "" : "${IMAGE_NAME}:static-builder-sha-${substr(SHA, 0, 7)}",
|
||||
VERSION == "dev" ? [] : [for v in semver(VERSION) : "${IMAGE_NAME}:static-builder-${v}"]
|
||||
LATEST ? "${IMAGE_NAME}:static-builder-musl" : "",
|
||||
SHA == "" || VERSION != "dev" ? "" : "${IMAGE_NAME}:static-builder-musl-sha-${substr(SHA, 0, 7)}",
|
||||
VERSION == "dev" ? [] : [for v in semver(VERSION) : "${IMAGE_NAME}:static-builder-musl-${v}"]
|
||||
]))
|
||||
labels = {
|
||||
"org.opencontainers.image.created" = "${timestamp()}"
|
||||
@@ -140,6 +149,34 @@ target "static-builder" {
|
||||
}
|
||||
args = {
|
||||
FRANKENPHP_VERSION = VERSION
|
||||
CI = CI
|
||||
SPC_OPT_BUILD_ARGS = SPC_OPT_BUILD_ARGS
|
||||
}
|
||||
secret = ["id=github-token,env=GITHUB_TOKEN"]
|
||||
}
|
||||
|
||||
target "static-builder-gnu" {
|
||||
dockerfile = "static-builder-gnu.Dockerfile"
|
||||
context = "./"
|
||||
platforms = [
|
||||
"linux/amd64",
|
||||
"linux/arm64"
|
||||
]
|
||||
tags = distinct(flatten([
|
||||
LATEST ? "${IMAGE_NAME}:static-builder-gnu" : "",
|
||||
SHA == "" || VERSION != "dev" ? "" : "${IMAGE_NAME}:static-builder-gnu-sha-${substr(SHA, 0, 7)}",
|
||||
VERSION == "dev" ? [] : [for v in semver(VERSION) : "${IMAGE_NAME}:static-builder-gnu-${v}"]
|
||||
]))
|
||||
labels = {
|
||||
"org.opencontainers.image.created" = "${timestamp()}"
|
||||
"org.opencontainers.image.version" = VERSION
|
||||
"org.opencontainers.image.revision" = SHA
|
||||
}
|
||||
args = {
|
||||
FRANKENPHP_VERSION = VERSION
|
||||
GO_VERSION = GO_VERSION
|
||||
CI = CI
|
||||
SPC_OPT_BUILD_ARGS = SPC_OPT_BUILD_ARGS
|
||||
}
|
||||
secret = ["id=github-token,env=GITHUB_TOKEN"]
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
Without any additional configuration, FrankenPHP operates in classic mode. In this mode, FrankenPHP functions like a traditional PHP server, directly serving PHP files. This makes it a seamless drop-in replacement for PHP-FPM or Apache with mod_php.
|
||||
|
||||
Similar to Caddy, FrankenPHP accepts an unlimited number of connections and uses a [fixed number of threads](config.md#caddyfile-config) to serve them. The number of accepted and queued connections is limited only by the available system resources. The PHP thread pool operates with a fixed number of threads initialized at startup, comparable to the static mode of PHP-FPM.
|
||||
Similar to Caddy, FrankenPHP accepts an unlimited number of connections and uses a [fixed number of threads](config.md#caddyfile-config) to serve them. The number of accepted and queued connections is limited only by the available system resources.
|
||||
The PHP thread pool operates with a fixed number of threads initialized at startup, comparable to the static mode of PHP-FPM. It's also possible to let threads [scale automatically at runtime](performance.md#max_threads), similar to the dynamic mode of PHP-FPM.
|
||||
|
||||
Queued connections will wait indefinitely until a PHP thread is available to serve them. To prevent that, set a reasonable [write timeout in Caddy](https://caddyserver.com/docs/caddyfile/options#timeouts).
|
||||
Queued connections will wait indefinitely until a PHP thread is available to serve them. To avoid this, you can use the max_wait_time [configuration](config.md#caddyfile-config) in FrankenPHP's global configuration to limit the duration a request can wait for a free PHP thread before being rejected.
|
||||
Additionally, you can set a reasonable [write timeout in Caddy](https://caddyserver.com/docs/caddyfile/options#timeouts).
|
||||
|
||||
Each Caddy instance will only spin up one FrankenPHP thread pool, which will be shared across all `php_server` blocks.
|
||||
|
||||
@@ -11,9 +11,13 @@ docker build -t frankenphp-dev -f dev.Dockerfile .
|
||||
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev
|
||||
```
|
||||
|
||||
该镜像包含常用的开发工具(Go、GDB、Valgrind、Neovim等)。
|
||||
该镜像包含常用的开发工具(Go、GDB、Valgrind、Neovim等)并使用以下 php 设置位置
|
||||
|
||||
如果 docker 版本低于 23.0,则会因为 dockerignore [pattern issue](https://github.com/moby/moby/pull/42676) 而导致构建失败。将目录添加到 `.dockerignore`。
|
||||
- php.ini: `/etc/frankenphp/php.ini` 默认提供了一个带有开发预设的 php.ini 文件。
|
||||
- 附加配置文件: `/etc/frankenphp/php.d/*.ini`
|
||||
- php 扩展: `/usr/lib/frankenphp/modules/`
|
||||
|
||||
如果你的 Docker 版本低于 23.0,则会因为 dockerignore [pattern issue](https://github.com/moby/moby/pull/42676) 而导致构建失败。将目录添加到 `.dockerignore`。
|
||||
|
||||
```patch
|
||||
!testdata/*.php
|
||||
@@ -38,7 +42,7 @@ go test -tags watcher -race -v ./...
|
||||
|
||||
```console
|
||||
cd caddy/frankenphp/
|
||||
go build
|
||||
go build -tags watcher,brotli,nobadger,nomysql,nopgx
|
||||
cd ../../
|
||||
```
|
||||
|
||||
@@ -49,10 +53,13 @@ cd testdata/
|
||||
../caddy/frankenphp/frankenphp run
|
||||
```
|
||||
|
||||
服务器正在监听 `127.0.0.1:8080`:
|
||||
服务器正在监听 `127.0.0.1:80`:
|
||||
|
||||
> [!NOTE]
|
||||
> 如果您正在使用 Docker,您必须绑定容器的 80 端口或者在容器内部执行命令。
|
||||
|
||||
```console
|
||||
curl -vk https://localhost/phpinfo.php
|
||||
curl -vk http://127.0.0.1/phpinfo.php
|
||||
```
|
||||
|
||||
## 最小测试服务器
|
||||
@@ -108,22 +115,22 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
|
||||
|
||||
1. 从 GitHub 下载 FrankenPHP 二进制文件的调试版本或创建包含调试符号的自定义静态构建:
|
||||
|
||||
```console
|
||||
docker buildx bake \
|
||||
--load \
|
||||
--set static-builder.args.DEBUG_SYMBOLS=1 \
|
||||
--set "static-builder.platform=linux/amd64" \
|
||||
static-builder
|
||||
docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
|
||||
```
|
||||
```console
|
||||
docker buildx bake \
|
||||
--load \
|
||||
--set static-builder.args.DEBUG_SYMBOLS=1 \
|
||||
--set "static-builder.platform=linux/amd64" \
|
||||
static-builder
|
||||
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
|
||||
```
|
||||
|
||||
2. 将当前版本的 `frankenphp` 替换为 debug FrankenPHP 可执行文件
|
||||
3. 照常启动 FrankenPHP(或者,你可以直接使用 GDB 启动 FrankenPHP: `gdb --args frankenphp run`)
|
||||
4. 使用 GDB 附加到进程:
|
||||
|
||||
```console
|
||||
gdb -p `pidof frankenphp`
|
||||
```
|
||||
```console
|
||||
gdb -p `pidof frankenphp`
|
||||
```
|
||||
|
||||
5. 如有必要,请在 GDB shell 中输入 `continue`
|
||||
6. 使 FrankenPHP 崩溃
|
||||
@@ -135,64 +142,60 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
|
||||
1. 打开 `.github/workflows/tests.yml`
|
||||
2. 启用 PHP 调试符号
|
||||
|
||||
```patch
|
||||
- uses: shivammathur/setup-php@v2
|
||||
# ...
|
||||
env:
|
||||
phpts: ts
|
||||
+ debug: true
|
||||
```
|
||||
```patch
|
||||
- uses: shivammathur/setup-php@v2
|
||||
# ...
|
||||
env:
|
||||
phpts: ts
|
||||
+ debug: true
|
||||
```
|
||||
|
||||
3. 启用 `tmate` 以连接到容器
|
||||
|
||||
```patch
|
||||
-
|
||||
name: Set CGO flags
|
||||
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
|
||||
+ -
|
||||
+ run: |
|
||||
+ sudo apt install gdb
|
||||
+ mkdir -p /home/runner/.config/gdb/
|
||||
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
|
||||
+ -
|
||||
+ uses: mxschmitt/action-tmate@v3
|
||||
```
|
||||
```patch
|
||||
- name: Set CGO flags
|
||||
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
|
||||
+ - run: |
|
||||
+ sudo apt install gdb
|
||||
+ mkdir -p /home/runner/.config/gdb/
|
||||
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
|
||||
+ - uses: mxschmitt/action-tmate@v3
|
||||
```
|
||||
|
||||
4. 连接到容器
|
||||
5. 打开 `frankenphp.go`
|
||||
6. 启用 `cgosymbolizer`
|
||||
|
||||
```patch
|
||||
- //_ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
+ _ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
```
|
||||
```patch
|
||||
- //_ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
+ _ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
```
|
||||
|
||||
7. 下载模块: `go get`
|
||||
8. 在容器中,可以使用 GDB 和以下:
|
||||
|
||||
```console
|
||||
go test -tags watcher -c -ldflags=-w
|
||||
gdb --args frankenphp.test -test.run ^MyTest$
|
||||
```
|
||||
```console
|
||||
go test -tags watcher -c -ldflags=-w
|
||||
gdb --args frankenphp.test -test.run ^MyTest$
|
||||
```
|
||||
|
||||
9. 当错误修复后,恢复所有这些更改
|
||||
|
||||
## 其他开发资源
|
||||
|
||||
* [PHP 嵌入 uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
|
||||
* [PHP 嵌入 NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
|
||||
* [PHP 嵌入 Go (go-php)](https://github.com/deuill/go-php)
|
||||
* [PHP 嵌入 Go (GoEmPHP)](https://github.com/mikespook/goemphp)
|
||||
* [PHP 嵌入 C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
|
||||
* [扩展和嵌入 PHP 作者:Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
|
||||
* [TSRMLS_CC到底是什么?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
|
||||
* [Mac 上的 PHP 嵌入](https://gist.github.com/jonnywang/61427ffc0e8dde74fff40f479d147db4)
|
||||
* [SDL 绑定](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)
|
||||
- [PHP 嵌入 uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
|
||||
- [PHP 嵌入 NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
|
||||
- [PHP 嵌入 Go (go-php)](https://github.com/deuill/go-php)
|
||||
- [PHP 嵌入 Go (GoEmPHP)](https://github.com/mikespook/goemphp)
|
||||
- [PHP 嵌入 C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
|
||||
- [扩展和嵌入 PHP 作者:Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
|
||||
- [TSRMLS_CC到底是什么?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
|
||||
- [SDL 绑定](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)
|
||||
|
||||
## Docker 相关资源
|
||||
|
||||
* [Bake 文件定义](https://docs.docker.com/build/customize/bake/file-definition/)
|
||||
* [docker buildx 构建](https://docs.docker.com/engine/reference/commandline/buildx_build/)
|
||||
- [Bake 文件定义](https://docs.docker.com/build/customize/bake/file-definition/)
|
||||
- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)
|
||||
|
||||
## 有用的命令
|
||||
|
||||
|
||||
@@ -4,22 +4,82 @@
|
||||
|
||||
FrankenPHP 是建立在 [Caddy](https://caddyserver.com/) Web 服务器之上的现代 PHP 应用程序服务器。
|
||||
|
||||
FrankenPHP 凭借其令人惊叹的功能为您的 PHP 应用程序提供了超能力:[早期提示](early-hints.md)、[worker 模式](worker.md)、[实时功能](mercure.md)、自动 HTTPS、HTTP/2 和 HTTP/3 支持......
|
||||
FrankenPHP 凭借其令人惊叹的功能为你的 PHP 应用程序提供了超能力:[早期提示](early-hints.md)、[worker 模式](worker.md)、[实时功能](mercure.md)、自动 HTTPS、HTTP/2 和 HTTP/3 支持......
|
||||
|
||||
FrankenPHP 可与任何 PHP 应用程序一起使用,并且由于提供了与 worker 模式的集成,使您的 Symfony 和 Laravel 项目比以往任何时候都更快。
|
||||
FrankenPHP 可与任何 PHP 应用程序一起使用,并且由于提供了与 worker 模式的集成,使你的 Symfony 和 Laravel 项目比以往任何时候都更快。
|
||||
|
||||
FrankenPHP 也可以用作独立的 Go 库,将 PHP 嵌入到任何使用 net/http 的应用程序中。
|
||||
FrankenPHP 也可以用作独立的 Go 库,将 PHP 嵌入到任何使用 `net/http` 的应用程序中。
|
||||
|
||||
[**了解更多** *frankenphp.dev*](https://frankenphp.dev/cn) 以及在以下地址中:
|
||||
[**了解更多** _frankenphp.dev_](https://frankenphp.dev/cn/) 以及查看此演示文稿:
|
||||
|
||||
<a href="https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/"><img src="https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png" alt="Slides" width="600"></a>
|
||||
|
||||
## 开始
|
||||
|
||||
### Docker
|
||||
在 Windows 上,请使用 [WSL](https://learn.microsoft.com/windows/wsl/) 运行 FrankenPHP。
|
||||
|
||||
### 安装脚本
|
||||
|
||||
你可以将以下命令复制到终端中,自动安装适用于你平台的版本:
|
||||
|
||||
```console
|
||||
docker run -v $PWD:/app/public \
|
||||
curl https://frankenphp.dev/install.sh | sh
|
||||
```
|
||||
|
||||
### 独立二进制
|
||||
|
||||
我们为 Linux 和 macOS 提供用于开发的 FrankenPHP 静态二进制文件,
|
||||
包含 [PHP 8.4](https://www.php.net/releases/8.4/zh.php) 以及大多数常用 PHP 扩展。
|
||||
|
||||
[下载 FrankenPHP](https://github.com/dunglas/frankenphp/releases)
|
||||
|
||||
**安装扩展:** 常见扩展已内置,无法再安装更多扩展。
|
||||
|
||||
### rpm 软件包
|
||||
|
||||
我们的维护者为所有使用 `dnf` 的系统提供 rpm 包。安装方式:
|
||||
|
||||
```console
|
||||
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
|
||||
sudo dnf module enable php-zts:static-8.4 # 可用 8.2-8.5
|
||||
sudo dnf install frankenphp
|
||||
```
|
||||
|
||||
**安装扩展:** `sudo dnf install php-zts-<extension>`
|
||||
|
||||
对于默认不可用的扩展,请使用 [PIE](https://github.com/php/pie):
|
||||
|
||||
```console
|
||||
sudo dnf install pie-zts
|
||||
sudo pie-zts install asgrim/example-pie-extension
|
||||
```
|
||||
|
||||
### deb 软件包
|
||||
|
||||
我们的维护者为所有使用 `apt` 的系统提供 deb 包。安装方式:
|
||||
|
||||
```console
|
||||
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" | sudo tee /etc/apt/sources.list.d/static-php.list && \
|
||||
sudo apt update
|
||||
sudo apt install frankenphp
|
||||
```
|
||||
|
||||
**安装扩展:** `sudo apt install php-zts-<extension>`
|
||||
|
||||
对于默认不可用的扩展,请使用 [PIE](https://github.com/php/pie):
|
||||
|
||||
```console
|
||||
sudo apt install pie-zts
|
||||
sudo pie-zts install asgrim/example-pie-extension
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
此外,还可以使用 [Docker 镜像](https://frankenphp.dev/docs/docker/):
|
||||
|
||||
```console
|
||||
docker run -v .:/app/public \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
@@ -29,50 +89,69 @@ docker run -v $PWD:/app/public \
|
||||
> [!TIP]
|
||||
>
|
||||
> 不要尝试使用 `https://127.0.0.1`。使用 `https://localhost` 并接受自签名证书。
|
||||
> 使用 [`SERVER_NAME` 环境变量](config.md#环境变量) 更改要使用的域。
|
||||
> 使用 [`SERVER_NAME` 环境变量](config.md#environment-variables) 更改要使用的域。
|
||||
|
||||
### 独立二进制
|
||||
### Homebrew
|
||||
|
||||
如果您不想使用 Docker,我们为 Linux 和 macOS 提供独立的 FrankenPHP 二进制文件
|
||||
,其中包含 [PHP 8.4](https://www.php.net/releases/8.4/en.php) 和最流行的 PHP 扩展:[下载 FrankenPHP](https://github.com/dunglas/frankenphp/releases)。
|
||||
FrankenPHP 也作为 [Homebrew](https://brew.sh) 软件包提供,适用于 macOS 和 Linux 系统。
|
||||
|
||||
若要启动当前目录的内容,请运行:
|
||||
安装方法:
|
||||
|
||||
```console
|
||||
./frankenphp php-server
|
||||
brew install dunglas/frankenphp/frankenphp
|
||||
```
|
||||
|
||||
您还可以使用以下命令运行命令行脚本:
|
||||
**安装扩展:** 使用 [PIE](https://github.com/php/pie)。
|
||||
|
||||
### 用法
|
||||
|
||||
要提供当前目录的内容,请运行:
|
||||
|
||||
```console
|
||||
./frankenphp php-cli /path/to/your/script.php
|
||||
frankenphp php-server
|
||||
```
|
||||
|
||||
你还可以使用以下命令运行命令行脚本:
|
||||
|
||||
```console
|
||||
frankenphp php-cli /path/to/your/script.php
|
||||
```
|
||||
|
||||
对于 deb 和 rpm 软件包,还可以启动 systemd 服务:
|
||||
|
||||
```console
|
||||
sudo systemctl start frankenphp
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
* [worker 模式](worker.md)
|
||||
* [早期提示支持(103 HTTP status code)](early-hints.md)
|
||||
* [实时功能](mercure.md)
|
||||
* [配置](config.md)
|
||||
* [Docker 镜像](docker.md)
|
||||
* [在生产环境中部署](production.md)
|
||||
* [创建独立、可自行执行的 PHP 应用程序](embed.md)
|
||||
* [创建静态二进制文件](static.md)
|
||||
* [从源代码编译](compile.md)
|
||||
* [Laravel 集成](laravel.md)
|
||||
* [已知问题](known-issues.md)
|
||||
* [演示应用程序 (Symfony) 和性能测试](https://github.com/dunglas/frankenphp-demo)
|
||||
* [Go 库文档](https://pkg.go.dev/github.com/dunglas/frankenphp)
|
||||
* [贡献和调试](https://frankenphp.dev/docs/contributing/)
|
||||
- [Classic 模式](classic.md)
|
||||
- [worker 模式](worker.md)
|
||||
- [早期提示支持(103 HTTP status code)](early-hints.md)
|
||||
- [实时功能](mercure.md)
|
||||
- [高效地服务大型静态文件](x-sendfile.md)
|
||||
- [配置](config.md)
|
||||
- [用 Go 编写 PHP 扩展](extensions.md)
|
||||
- [Docker 镜像](docker.md)
|
||||
- [在生产环境中部署](production.md)
|
||||
- [性能优化](performance.md)
|
||||
- [创建独立、可自行执行的 PHP 应用程序](embed.md)
|
||||
- [创建静态二进制文件](static.md)
|
||||
- [从源代码编译](compile.md)
|
||||
- [Laravel 集成](laravel.md)
|
||||
- [已知问题](known-issues.md)
|
||||
- [演示应用程序 (Symfony) 和性能测试](https://github.com/dunglas/frankenphp-demo)
|
||||
- [Go 库文档](https://pkg.go.dev/github.com/dunglas/frankenphp)
|
||||
- [贡献和调试](https://frankenphp.dev/docs/contributing/)
|
||||
|
||||
## 示例和框架
|
||||
|
||||
* [Symfony](https://github.com/dunglas/symfony-docker)
|
||||
* [API Platform](https://api-platform.com/docs/distribution/)
|
||||
* [Laravel](laravel.md)
|
||||
* [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
|
||||
* [WordPress](https://github.com/StephenMiracle/frankenwp)
|
||||
* [Drupal](https://github.com/dunglas/frankenphp-drupal)
|
||||
* [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
|
||||
* [TYPO3](https://github.com/ochorocho/franken-typo3)
|
||||
* [Magento2](https://github.com/ekino/frankenphp-magento2)
|
||||
- [Symfony](https://github.com/dunglas/symfony-docker)
|
||||
- [API Platform](https://api-platform.com/docs/distribution/)
|
||||
- [Laravel](laravel.md)
|
||||
- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
|
||||
- [WordPress](https://github.com/StephenMiracle/frankenwp)
|
||||
- [Drupal](https://github.com/dunglas/frankenphp-drupal)
|
||||
- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
|
||||
- [TYPO3](https://github.com/ochorocho/franken-typo3)
|
||||
- [Magento2](https://github.com/ekino/frankenphp-magento2)
|
||||
|
||||
11
docs/cn/classic.md
Normal file
11
docs/cn/classic.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# 使用经典模式
|
||||
|
||||
在没有任何额外配置的情况下,FrankenPHP 以经典模式运行。在此模式下,FrankenPHP 的功能类似于传统的 PHP 服务器,直接提供 PHP 文件服务。这使其成为 PHP-FPM 或 Apache with mod_php 的无缝替代品。
|
||||
|
||||
与 Caddy 类似,FrankenPHP 接受无限数量的连接,并使用[固定数量的线程](config.md#caddyfile-配置)来为它们提供服务。接受和排队的连接数量仅受可用系统资源的限制。
|
||||
PHP 线程池使用在启动时初始化的固定数量的线程运行,类似于 PHP-FPM 的静态模式。也可以让线程在[运行时自动扩展](performance.md#max_threads),类似于 PHP-FPM 的动态模式。
|
||||
|
||||
排队的连接将无限期等待,直到有 PHP 线程可以为它们提供服务。为了避免这种情况,你可以在 FrankenPHP 的全局配置中使用 max_wait_time [配置](config.md#caddyfile-配置)来限制请求可以等待空闲的 PHP 线程的时间,超时后将被拒绝。
|
||||
此外,你还可以在 Caddy 中设置合理的[写超时](https://caddyserver.com/docs/caddyfile/options#timeouts)。
|
||||
|
||||
每个 Caddy 实例只会启动一个 FrankenPHP 线程池,该线程池将在所有 `php_server` 块之间共享。
|
||||
@@ -9,6 +9,23 @@
|
||||
|
||||
FrankenPHP 支持 PHP 8.2 及更高版本。
|
||||
|
||||
### 使用 Homebrew (Linux 和 Mac)
|
||||
|
||||
安装与 FrankenPHP 兼容的 libphp 版本的最简单方法是使用 [Homebrew PHP](https://github.com/shivammathur/homebrew-php) 提供的 ZTS 包。
|
||||
|
||||
首先,如果尚未安装,请安装 [Homebrew](https://brew.sh)。
|
||||
|
||||
然后,安装 PHP 的 ZTS 变体、Brotli(可选,用于压缩支持)和 watcher(可选,用于文件更改检测):
|
||||
|
||||
```console
|
||||
brew install shivammathur/php/php-zts brotli watcher
|
||||
brew link --overwrite --force shivammathur/php/php-zts
|
||||
```
|
||||
|
||||
### 通过编译 PHP
|
||||
|
||||
或者,你可以按照以下步骤,使用 FrankenPHP 所需的选项从源代码编译 PHP。
|
||||
|
||||
首先,[获取 PHP 源代码](https://www.php.net/downloads.php) 并提取它们:
|
||||
|
||||
```console
|
||||
@@ -16,11 +33,10 @@ tar xf php-*
|
||||
cd php-*/
|
||||
```
|
||||
|
||||
然后,为您的平台配置 PHP.
|
||||
然后,运行适用于你平台的 `configure` 脚本。
|
||||
以下 `./configure` 标志是必需的,但你可以添加其他标志,例如编译扩展或附加功能。
|
||||
|
||||
这些参数是必需的,但你也可以添加其他编译参数(例如额外的扩展)。
|
||||
|
||||
### Linux
|
||||
#### Linux
|
||||
|
||||
```console
|
||||
./configure \
|
||||
@@ -30,12 +46,12 @@ cd php-*/
|
||||
--enable-zend-max-execution-timers
|
||||
```
|
||||
|
||||
### Mac
|
||||
#### Mac
|
||||
|
||||
使用 [Homebrew](https://brew.sh/) 包管理器安装 `libiconv`、`bison`、`re2c` 和 `pkg-config`:
|
||||
使用 [Homebrew](https://brew.sh/) 包管理器安装所需的和可选的依赖项:
|
||||
|
||||
```console
|
||||
brew install libiconv bison re2c pkg-config
|
||||
brew install libiconv bison brotli re2c pkg-config watcher
|
||||
echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
|
||||
```
|
||||
|
||||
@@ -43,16 +59,13 @@ echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
|
||||
|
||||
```console
|
||||
./configure \
|
||||
--enable-embed=static \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--disable-opcache-jit \
|
||||
--enable-static \
|
||||
--enable-shared=no \
|
||||
--with-iconv=/opt/homebrew/opt/libiconv/
|
||||
```
|
||||
|
||||
## 编译并安装 PHP
|
||||
#### 编译 PHP
|
||||
|
||||
最后,编译并安装 PHP:
|
||||
|
||||
@@ -61,38 +74,54 @@ make -j"$(getconf _NPROCESSORS_ONLN)"
|
||||
sudo make install
|
||||
```
|
||||
|
||||
## 安装可选依赖项
|
||||
|
||||
某些 FrankenPHP 功能依赖于必须安装的可选系统依赖项。
|
||||
或者,可以通过向 Go 编译器传递构建标签来禁用这些功能。
|
||||
|
||||
| 功能 | 依赖项 | 用于禁用的构建标签 |
|
||||
| --------------------- | --------------------------------------------------------------------- | ------------------ |
|
||||
| Brotli 压缩 | [Brotli](https://github.com/google/brotli) | nobrotli |
|
||||
| 文件更改时重启 worker | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
|
||||
|
||||
## 编译 Go 应用
|
||||
|
||||
您现在可以使用 Go 库并编译我们的 Caddy 构建:
|
||||
|
||||
```console
|
||||
curl -L https://github.com/dunglas/frankenphp/archive/refs/heads/main.tar.gz | tar xz
|
||||
cd frankenphp-main/caddy/frankenphp
|
||||
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build
|
||||
```
|
||||
你现在可以构建最终的二进制文件。
|
||||
|
||||
### 使用 xcaddy
|
||||
|
||||
你可以使用 [xcaddy](https://github.com/caddyserver/xcaddy) 来编译 [自定义 Caddy 模块](https://caddyserver.com/docs/modules/) 的 FrankenPHP:
|
||||
推荐的方法是使用 [xcaddy](https://github.com/caddyserver/xcaddy) 来编译 FrankenPHP。
|
||||
`xcaddy` 还允许轻松添加 [自定义 Caddy 模块](https://caddyserver.com/docs/modules/) 和 FrankenPHP 扩展:
|
||||
|
||||
```console
|
||||
CGO_ENABLED=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'" \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
||||
CGO_CFLAGS=$(php-config --includes) \
|
||||
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
||||
xcaddy build \
|
||||
--output frankenphp \
|
||||
--with github.com/dunglas/frankenphp/caddy \
|
||||
--with github.com/dunglas/caddy-cbrotli \
|
||||
--with github.com/dunglas/mercure/caddy \
|
||||
--with github.com/dunglas/vulcain/caddy
|
||||
# Add extra Caddy modules here
|
||||
# 在这里添加额外的 Caddy 模块和 FrankenPHP 扩展
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> 如果你的系统基于 musl libc(Alpine Linux 上默认使用)并搭配 Symfony 使用,
|
||||
> 您可能需要增加默认堆栈大小。
|
||||
> 否则,您可能会收到如下错误 `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`
|
||||
> 你可能需要增加默认堆栈大小。
|
||||
> 否则,你可能会收到如下错误 `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`
|
||||
>
|
||||
> 请将 `XCADDY_GO_BUILD_FLAGS` 环境变量更改为如下类似的值
|
||||
> `XCADDY_GO_BUILD_FLAGS=$'-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'`
|
||||
> (根据您的应用需求更改堆栈大小)。
|
||||
> (根据你的应用需求更改堆栈大小)。
|
||||
|
||||
### 不使用 xcaddy
|
||||
|
||||
或者,可以通过直接使用 `go` 命令来编译 FrankenPHP 而不使用 `xcaddy`:
|
||||
|
||||
```console
|
||||
curl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz
|
||||
cd frankenphp-main/caddy/frankenphp
|
||||
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build -tags=nobadger,nomysql,nopgx
|
||||
```
|
||||
|
||||
@@ -1,56 +1,77 @@
|
||||
# 配置
|
||||
|
||||
FrankenPHP,Caddy 以及 Mercure 和 Vulcain 模块可以使用 [Caddy 支持的格式](https://caddyserver.com/docs/getting-started#your-first-config) 进行配置。
|
||||
FrankenPHP、Caddy 以及 Mercure 和 Vulcain 模块可以使用 [Caddy 支持的格式](https://caddyserver.com/docs/getting-started#your-first-config) 进行配置。
|
||||
|
||||
在 [Docker 镜像](docker.md) 中,`Caddyfile` 位于 `/etc/frankenphp/Caddyfile`。
|
||||
静态二进制文件也会在执行 `frankenphp run` 命令的目录中查找 `Caddyfile`。
|
||||
你可以使用 `-c` 或 `--config` 选项指定自定义路径。
|
||||
|
||||
在[Docker 映像](docker.md) 中,`Caddyfile` 位于 `/etc/caddy/Caddyfile`。
|
||||
静态二进制文件会在启动时所在的目录中查找 `Caddyfile`。
|
||||
PHP 本身可以[使用 `php.ini` 文件](https://www.php.net/manual/zh/configuration.file.php)进行配置。
|
||||
默认情况下,随 Docker 映像提供的 PHP 和静态二进制文件中包含的 PHP 将在启动 FrankenPHP 的目录和 `/usr/local/etc/php/` 中查找`php.ini` 文件。它们还会从 `/usr/local/etc/php/conf.d/` 中加载所有以 `.ini` 结尾的文件。
|
||||
默认情况下没有 `php.ini` 文件,因此应复制 PHP 项目提供的官方模板。
|
||||
在 Docker 上,模板在镜像中提供:
|
||||
|
||||
根据你的安装方法,PHP 解释器将在上述位置查找配置文件。
|
||||
|
||||
## Docker
|
||||
|
||||
- `php.ini`: `/usr/local/etc/php/php.ini`(默认情况下不提供 `php.ini`)
|
||||
- 附加配置文件: `/usr/local/etc/php/conf.d/*.ini`
|
||||
- PHP 扩展: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`
|
||||
- 你应该复制 PHP 项目提供的官方模板:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
|
||||
# 生产:
|
||||
# 生产环境:
|
||||
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
|
||||
|
||||
# 开发:
|
||||
# 或开发环境:
|
||||
RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
|
||||
```
|
||||
|
||||
如果不使用 Docker,请复制[PHP 源代码](https://github.com/php/php-src/)中提供的`php.ini-production`或`php.ini-development`中的一个。
|
||||
## RPM 和 Debian 包
|
||||
|
||||
- `php.ini`: `/etc/frankenphp/php.ini`(默认情况下提供带有生产预设的 `php.ini` 文件)
|
||||
- 附加配置文件: `/etc/frankenphp/php.d/*.ini`
|
||||
- PHP 扩展: `/usr/lib/frankenphp/modules/`
|
||||
|
||||
## 静态二进制文件
|
||||
|
||||
- `php.ini`: 执行 `frankenphp run` 或 `frankenphp php-server` 的目录,然后是 `/etc/frankenphp/php.ini`
|
||||
- 附加配置文件: `/etc/frankenphp/php.d/*.ini`
|
||||
- PHP 扩展: 无法加载,将它们打包在二进制文件本身中
|
||||
- 复制 [PHP 源代码](https://github.com/php/php-src/) 中提供的 `php.ini-production` 或 `php.ini-development` 中的一个。
|
||||
|
||||
## Caddyfile 配置
|
||||
|
||||
要注册 FrankenPHP 执行器,必须设置 `frankenphp` [全局选项](https://caddyserver.com/docs/caddyfile/concepts#global-options),然后可以在站点块中使用 `php_server` 或 `php` [HTTP 指令](https://caddyserver.com/docs/caddyfile/concepts#directives) 来为您的 PHP 应用程序提供服务。
|
||||
可以在站点块中使用 `php_server` 或 `php` [HTTP 指令](https://caddyserver.com/docs/caddyfile/concepts#directives) 来为你的 PHP 应用程序提供服务。
|
||||
|
||||
最小示例:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
# 启用 FrankenPHP
|
||||
frankenphp
|
||||
}
|
||||
|
||||
localhost {
|
||||
# 启用压缩(可选)
|
||||
encode zstd br gzip
|
||||
# 执行当前目录中的 PHP 文件并提供资产
|
||||
# 在当前目录中执行 PHP 文件并提供资源服务
|
||||
php_server
|
||||
}
|
||||
```
|
||||
|
||||
或者,可以在全局选项下指定要创建的线程数和要从服务器启动的 [worker 脚本](worker.md)。
|
||||
你还可以使用全局选项显式配置 FrankenPHP:
|
||||
`frankenphp` [全局选项](https://caddyserver.com/docs/caddyfile/concepts#global-options) 可用于配置 FrankenPHP。
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
num_threads <num_threads> # 设置要启动的 PHP 线程数。默认值:可用 CPU 数量的 2 倍。
|
||||
num_threads <num_threads> # 设置要启动的 PHP 线程数量。默认:可用 CPU 数量的 2 倍。
|
||||
max_threads <max_threads> # 限制可以在运行时启动的额外 PHP 线程的数量。默认值:num_threads。可以设置为 'auto'。
|
||||
max_wait_time <duration> # 设置请求在超时之前可以等待的最大时间,直到找到一个空闲的 PHP 线程。 默认:禁用。
|
||||
php_ini <key> <value> # 设置一个 php.ini 指令。可以多次使用以设置多个指令。
|
||||
worker {
|
||||
file <path> # 设置 worker 脚本的路径。
|
||||
num <num> # 设置要启动的 PHP 线程数,默认为可用 CPU 数的 2 倍。
|
||||
env <key> <value> # 将额外的环境变量设置为给定值。可以为多个环境变量多次指定。
|
||||
file <path> # 设置工作脚本的路径。
|
||||
num <num> # 设置要启动的 PHP 线程数量,默认为可用 CPU 数量的 2 倍。
|
||||
env <key> <value> # 设置一个额外的环境变量为给定的值。可以多次指定以设置多个环境变量。
|
||||
watch <path> # 设置要监视文件更改的路径。可以为多个路径多次指定。
|
||||
name <name> # 设置worker的名称,用于日志和指标。默认值:worker文件的绝对路径。
|
||||
max_consecutive_failures <num> # 设置在工人被视为不健康之前的最大连续失败次数,-1意味着工人将始终重新启动。默认值:6。
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +79,7 @@ localhost {
|
||||
# ...
|
||||
```
|
||||
|
||||
或者,您可以使用 `worker` 选项的一行缩写形式:
|
||||
或者,您可以使用 `worker` 选项的一行简短形式。
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
@@ -70,36 +91,38 @@ localhost {
|
||||
# ...
|
||||
```
|
||||
|
||||
如果在同一服务器上运行多个应用,还可以定义多个 worker:
|
||||
如果您在同一服务器上服务多个应用程序,您还可以定义多个工作线程:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker /path/to/app/public/index.php <num>
|
||||
worker /path/to/other/public/index.php <num>
|
||||
app.example.com {
|
||||
root /path/to/app/public
|
||||
php_server {
|
||||
root /path/to/app/public # 允许更好的缓存
|
||||
worker index.php <num>
|
||||
}
|
||||
}
|
||||
|
||||
app.example.com {
|
||||
root * /path/to/app/public
|
||||
php_server
|
||||
other.example.com {
|
||||
root /path/to/other/public
|
||||
php_server {
|
||||
root /path/to/other/public
|
||||
worker index.php <num>
|
||||
}
|
||||
}
|
||||
|
||||
other.example.com {
|
||||
root * /path/to/other/public
|
||||
php_server
|
||||
}
|
||||
# ...
|
||||
```
|
||||
|
||||
通常你只需要 `php_server` 指令,
|
||||
但如果要完全控制,则可以使用较低级别的 `php` 指令:
|
||||
使用 `php_server` 指令通常是您需要的。
|
||||
但是如果你需要完全控制,你可以使用更低级的 `php` 指令。
|
||||
`php` 指令将所有输入传递给 PHP,而不是先检查是否
|
||||
是一个PHP文件。在[性能页面](performance.md#try_files)中了解更多关于它的信息。
|
||||
|
||||
使用 `php_server` 指令等效于以下配置:
|
||||
使用 `php_server` 指令等同于以下配置:
|
||||
|
||||
```caddyfile
|
||||
route {
|
||||
# 为目录请求添加尾部斜杠
|
||||
# 为目录请求添加尾斜杠
|
||||
@canonicalPath {
|
||||
file {path}/index.php
|
||||
not path */
|
||||
@@ -118,34 +141,161 @@ route {
|
||||
}
|
||||
```
|
||||
|
||||
`php_server` 和 `php` 指令具有以下选项:
|
||||
`php_server` 和 `php` 指令有以下选项:
|
||||
|
||||
```caddyfile
|
||||
php_server [<matcher>] {
|
||||
root <directory> # 设置站点的根目录。默认值:`root` 指令。
|
||||
split_path <delim...> # 设置用于将 URI 拆分为两部分的子字符串。第一个匹配的子字符串将用于从路径中拆分“路径信息”。第一个部分以匹配的子字符串为后缀,并将假定为实际资源(CGI 脚本)名称。第二部分将设置为PATH_INFO,供脚本使用。默认值:`.php`
|
||||
resolve_root_symlink false # 禁用将 `root` 目录在符号链接时将其解析为实际值(默认启用)。
|
||||
env <key> <value> # 设置额外的环境变量,可以设置多个环境变量。
|
||||
root <directory> # 将根文件夹设置为站点。默认值:`root` 指令。
|
||||
split_path <delim...> # 设置用于将 URI 分割成两部分的子字符串。第一个匹配的子字符串将用来将 "路径信息" 与路径分开。第一部分后缀为匹配的子字符串,并将被视为实际资源(CGI 脚本)名称。第二部分将被设置为脚本使用的 PATH_INFO。默认值:`.php`。
|
||||
resolve_root_symlink false # 禁用通过评估符号链接(如果存在)将 `root` 目录解析为其实际值(默认启用)。
|
||||
env <key> <value> # 设置一个额外的环境变量为给定的值。可以多次指定以设置多个环境变量。
|
||||
file_server off # 禁用内置的 file_server 指令。
|
||||
worker { # 为此服务器创建特定的worker。可以多次指定以创建多个workers。
|
||||
file <path> # 设置工作脚本的路径,可以相对于 php_server 根目录
|
||||
num <num> # 设置要启动的 PHP 线程数,默认为可用数量的 2 倍
|
||||
name <name> # 为worker设置名称,用于日志和指标。默认值:worker文件的绝对路径。定义在 php_server 块中时,始终以 m# 开头。
|
||||
watch <path> # 设置要监视文件更改的路径。可以为多个路径多次指定。
|
||||
env <key> <value> # 设置一个额外的环境变量为给定值。可以多次指定以设置多个环境变量。此工作进程的环境变量也从 php_server 父进程继承,但可以在此处覆盖。
|
||||
match <path> # 将worker匹配到路径模式。覆盖 try_files,并且只能在 php_server 指令中使用。
|
||||
}
|
||||
worker <other_file> <num> # 也可以像在全局 frankenphp 块中那样使用简短形式。
|
||||
}
|
||||
```
|
||||
|
||||
### 监控文件变化
|
||||
|
||||
由于 workers 只会启动您的应用程序一次并将其保留在内存中,
|
||||
因此对您的 PHP 文件的任何更改不会立即反映出来。
|
||||
|
||||
Wworkers 可以通过 `watch` 指令在文件更改时重新启动。
|
||||
这对开发环境很有用。
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker {
|
||||
file /path/to/app/public/worker.php
|
||||
watch
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果没有指定 `watch` 目录,它将回退到 `./**/*.{php,yaml,yml,twig,env}`,
|
||||
这将监视启动 FrankenPHP 进程的目录及其子目录中的所有 `.php`、`.yaml`、`.yml`、`.twig` 和 `.env` 文件。
|
||||
你也可以通过 [shell 文件名模式](https://pkg.go.dev/path/filepath#Match) 指定一个或多个目录:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker {
|
||||
file /path/to/app/public/worker.php
|
||||
watch /path/to/app # 监视 /path/to/app 所有子目录中的所有文件
|
||||
watch /path/to/app/*.php # 监视位于/path/to/app中的以.php结尾的文件
|
||||
watch /path/to/app/**/*.php # 监视 /path/to/app 及子目录中的 PHP 文件
|
||||
watch /path/to/app/**/*.{php,twig} # 在/path/to/app及其子目录中监视PHP和Twig文件
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `**` 模式表示递归监视
|
||||
- 目录也可以是相对的(相对于FrankenPHP进程启动的位置)
|
||||
- 如果您定义了多个workers,当文件发生更改时,将重新启动所有workers。
|
||||
- 小心查看在运行时创建的文件(如日志),因为它们可能导致不必要的工作进程重启。
|
||||
|
||||
文件监视器基于[e-dant/watcher](https://github.com/e-dant/watcher)。
|
||||
|
||||
## 将 worker 匹配到一条路径
|
||||
|
||||
在传统的PHP应用程序中,脚本总是放在公共目录中。
|
||||
这对于工作脚本也是如此,这些脚本被视为任何其他PHP脚本。
|
||||
如果您想将工作脚本放在公共目录外,可以通过 `match` 指令来实现。
|
||||
|
||||
`match` 指令是 `try_files` 的一种优化替代方案,仅在 `php_server` 和 `php` 内部可用。
|
||||
以下示例将在公共目录中提供文件(如果存在)
|
||||
并将请求转发给与路径模式匹配的 worker。
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
php_server {
|
||||
worker {
|
||||
file /path/to/worker.php # 文件可以在公共路径之外
|
||||
match /api/* # 所有以 /api/ 开头的请求将由此 worker 处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 全双工 (HTTP/1)
|
||||
|
||||
在使用HTTP/1.x时,可能希望启用全双工模式,以便在完整主体之前写入响应。
|
||||
已被阅读。(例如:WebSocket、服务器发送事件等。)
|
||||
|
||||
这是一个可选配置,需要添加到 `Caddyfile` 中的全局选项中:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
servers {
|
||||
enable_full_duplex
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> 启用此选项可能导致不支持全双工的旧HTTP/1.x客户端死锁。
|
||||
> 这也可以通过配置 `CADDY_GLOBAL_OPTIONS` 环境配置来实现:
|
||||
|
||||
```sh
|
||||
CADDY_GLOBAL_OPTIONS="servers {
|
||||
enable_full_duplex
|
||||
}"
|
||||
```
|
||||
|
||||
您可以在[Caddy文档](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex)中找到有关此设置的更多信息。
|
||||
|
||||
## 环境变量
|
||||
|
||||
以下环境变量可用于在 `Caddyfile` 中注入 Caddy 指令,而无需对其进行修改:
|
||||
可以使用以下环境变量在不修改 `Caddyfile` 的情况下注入 Caddy 指令:
|
||||
|
||||
* `SERVER_NAME`: 更改 [要监听的地址](https://caddyserver.com/docs/caddyfile/concepts#addresses),提供的主机名也将用于生成的 TLS 证书
|
||||
* `CADDY_GLOBAL_OPTIONS`: 注入 [全局选项](https://caddyserver.com/docs/caddyfile/options)
|
||||
* `FRANKENPHP_CONFIG`: 在 `frankenphp` 指令下注入配置
|
||||
- `SERVER_NAME`: 更改[监听的地址](https://caddyserver.com/docs/caddyfile/concepts#addresses),提供的宿主名也将用于生成的TLS证书。
|
||||
- `SERVER_ROOT`: 更改网站的根目录,默认为 `public/`
|
||||
- `CADDY_GLOBAL_OPTIONS`: 注入[全局选项](https://caddyserver.com/docs/caddyfile/options)
|
||||
- `FRANKENPHP_CONFIG`: 在 `frankenphp` 指令下注入配置
|
||||
|
||||
至于 FPM 和 CLI SAPIs,环境变量默认在 `$_SERVER` 超全局中暴露。
|
||||
|
||||
[the `variables_order` PHP 指令](https://www.php.net/manual/en/ini.core.php#ini.variables-order) 的 `S` 值始终等于 `ES`,无论 `E` 在该指令中的其他位置如何。
|
||||
|
||||
## PHP 配置
|
||||
|
||||
要加载 [其他 PHP INI 配置文件](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan),
|
||||
可以使用 `PHP_INI_SCAN_DIR` 环境变量。
|
||||
设置后,PHP 将加载给定目录中存在 `.ini` 扩展名的所有文件。
|
||||
加载[附加的 PHP 配置文件](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan),
|
||||
`PHP_INI_SCAN_DIR`环境变量可以被使用。
|
||||
设置后,PHP 将加载给定目录中所有带有 `.ini` 扩展名的文件。
|
||||
|
||||
您还可以通过在 `Caddyfile` 中使用 `php_ini` 指令来更改 PHP 配置:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
php_ini memory_limit 256M
|
||||
|
||||
# 或者
|
||||
|
||||
php_ini {
|
||||
memory_limit 256M
|
||||
max_execution_time 15
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 启用调试模式
|
||||
|
||||
使用 Docker 镜像时,将 `CADDY_GLOBAL_OPTIONS` 环境变量设置为 `debug` 以启用调试模式:
|
||||
使用Docker镜像时,将`CADDY_GLOBAL_OPTIONS`环境变量设置为`debug`以启用调试模式:
|
||||
|
||||
```console
|
||||
docker run -v $PWD:/app/public \
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
# 构建自定义 Docker 镜像
|
||||
|
||||
[FrankenPHP Docker 镜像](https://hub.docker.com/r/dunglas/frankenphp) 基于 [官方 PHP 镜像](https://hub.docker.com/_/php/)。
|
||||
Alpine Linux 和 Debian 衍生版适用于常见的处理器架构,支持 PHP 8.2、8.3 和 8.4。。[查看 Tags](https://hub.docker.com/r/dunglas/frankenphp/tags)。
|
||||
[FrankenPHP Docker 镜像](https://hub.docker.com/r/dunglas/frankenphp) 基于 [官方 PHP 镜像](https://hub.docker.com/_/php/)。提供适用于流行架构的 Debian 和 Alpine Linux 变体。推荐使用 Debian 变体。
|
||||
|
||||
提供 PHP 8.2、8.3、8.4 和 8.5 的变体。
|
||||
|
||||
标签遵循此模式:`dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`
|
||||
|
||||
- `<frankenphp-version>` 和 `<php-version>` 分别是 FrankenPHP 和 PHP 的版本号,范围从主版本(例如 `1`)、次版本(例如 `1.2`)到补丁版本(例如 `1.2.3`)。
|
||||
- `<os>` 要么是 `bookworm`(用于 Debian Bookworm)要么是 `alpine`(用于 Alpine 的最新稳定版本)。
|
||||
|
||||
[浏览标签](https://hub.docker.com/r/dunglas/frankenphp/tags)。
|
||||
|
||||
## 如何使用镜像
|
||||
|
||||
@@ -71,13 +79,13 @@ FROM dunglas/frankenphp AS runner
|
||||
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
|
||||
```
|
||||
|
||||
FrankenPHP 提供的 `builder` 镜像包含 libphp 的编译版本。
|
||||
FrankenPHP 提供的 `builder` 镜像包含 `libphp` 的编译版本。
|
||||
[用于构建的镜像](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) 适用于所有版本的 FrankenPHP 和 PHP,包括 Alpine 和 Debian。
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> 如果你的系统基于 musl libc(Alpine Linux 上默认使用)并搭配 Symfony 使用,
|
||||
> 您可能需要 [增加默认堆栈大小](compile.md#使用-xcaddy)。
|
||||
> 你可能需要 [增加默认堆栈大小](compile.md#using-xcaddy)。
|
||||
|
||||
## 默认启用 worker 模式
|
||||
|
||||
@@ -136,7 +144,7 @@ volumes:
|
||||
|
||||
FrankenPHP 可以在 Docker 中以非 root 用户身份运行。
|
||||
|
||||
下面是一个示例 Dockerfile:
|
||||
下面是一个示例 `Dockerfile`:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
@@ -148,18 +156,45 @@ RUN \
|
||||
useradd ${USER}; \
|
||||
# 需要开放80和443端口的权限
|
||||
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
|
||||
# 需要 /data/caddy 和 /config/caddy 目录的写入权限
|
||||
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy;
|
||||
# 需要 /config/caddy 和 /data/caddy 目录的写入权限
|
||||
chown -R ${USER}:${USER} /config/caddy /data/caddy
|
||||
|
||||
USER ${USER}
|
||||
```
|
||||
|
||||
### 无权限运行
|
||||
|
||||
即使在无根运行时,FrankenPHP 也需要 `CAP_NET_BIND_SERVICE` 权限来将
|
||||
Web 服务器绑定到特权端口(80 和 443)。
|
||||
|
||||
如果你在非特权端口(1024 及以上)上公开 FrankenPHP,则可以以非 root 用户身份运行
|
||||
Web 服务器,并且不需要任何权限:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
|
||||
ARG USER=appuser
|
||||
|
||||
RUN \
|
||||
# 在基于 alpine 的发行版使用 "adduser -D ${USER}"
|
||||
useradd ${USER}; \
|
||||
# 移除默认权限
|
||||
setcap -r /usr/local/bin/frankenphp; \
|
||||
# 给予 /config/caddy 和 /data/caddy 写入权限
|
||||
chown -R ${USER}:${USER} /config/caddy /data/caddy
|
||||
|
||||
USER ${USER}
|
||||
```
|
||||
|
||||
接下来,设置 `SERVER_NAME` 环境变量以使用非特权端口。
|
||||
示例:`:8000`
|
||||
|
||||
## 更新
|
||||
|
||||
Docker 镜像会按照以下条件更新:
|
||||
|
||||
* 发布新的版本后
|
||||
* 每日 4:00(UTC 时间)检查新的 PHP 镜像
|
||||
- 发布新的版本后
|
||||
- 每日 4:00(UTC 时间)检查新的 PHP 镜像
|
||||
|
||||
## 开发版本
|
||||
|
||||
|
||||
@@ -6,18 +6,20 @@ FrankenPHP 能够将 PHP 应用程序的源代码和资源文件嵌入到静态
|
||||
|
||||
了解有关此功能的更多信息 [Kévin 在 SymfonyCon 上的演讲](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/)。
|
||||
|
||||
有关嵌入 Laravel 应用程序,请[阅读此特定文档条目](laravel.md#laravel-apps-as-standalone-binaries)。
|
||||
|
||||
## 准备你的应用
|
||||
|
||||
在创建独立二进制文件之前,请确保应用已准备好进行打包。
|
||||
|
||||
例如,您可能希望:
|
||||
例如,你可能希望:
|
||||
|
||||
* 给应用安装生产环境的依赖
|
||||
* 导出 autoloader
|
||||
* 如果可能,为应用启用生产模式
|
||||
* 丢弃不需要的文件,例如 `.git` 或测试文件,以减小最终二进制文件的大小
|
||||
- 给应用安装生产环境的依赖
|
||||
- 导出 autoloader
|
||||
- 如果可能,为应用启用生产模式
|
||||
- 丢弃不需要的文件,例如 `.git` 或测试文件,以减小最终二进制文件的大小
|
||||
|
||||
例如,对于 Symfony 应用程序,您可以使用以下命令:
|
||||
例如,对于 Symfony 应用程序,你可以使用以下命令:
|
||||
|
||||
```console
|
||||
# 导出项目以避免 .git/ 等目录
|
||||
@@ -29,7 +31,8 @@ cd $TMPDIR/my-prepared-app
|
||||
echo APP_ENV=prod > .env.local
|
||||
echo APP_DEBUG=0 >> .env.local
|
||||
|
||||
# 删除测试文件
|
||||
# 删除测试和其他不需要的文件以节省空间
|
||||
# 或者,将这些文件添加到您的 .gitattributes 文件中,并设置 export-ignore 属性
|
||||
rm -Rf tests/
|
||||
|
||||
# 安装依赖项
|
||||
@@ -39,62 +42,64 @@ composer install --ignore-platform-reqs --no-dev -a
|
||||
composer dump-env prod
|
||||
```
|
||||
|
||||
### 自定义配置
|
||||
|
||||
要自定义[配置](config.md),您可以放置一个 `Caddyfile` 以及一个 `php.ini` 文件
|
||||
在应用程序的主目录中嵌入(在之前的示例中是`$TMPDIR/my-prepared-app`)。
|
||||
|
||||
## 创建 Linux 二进制文件
|
||||
|
||||
创建 Linux 二进制文件的最简单方法是使用我们提供的基于 Docker 的构建器。
|
||||
|
||||
1. 在准备好的应用的存储库中创建一个名为 `static-build.Dockerfile` 的文件。
|
||||
|
||||
```dockerfile
|
||||
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
|
||||
```dockerfile
|
||||
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
|
||||
# 如果你打算在 glibc 系统上运行该二进制文件,请使用 static-builder-gnu
|
||||
|
||||
# 复制应用代码
|
||||
WORKDIR /go/src/app/dist/app
|
||||
COPY . .
|
||||
# 复制应用代码
|
||||
WORKDIR /go/src/app/dist/app
|
||||
COPY . .
|
||||
|
||||
# 构建静态二进制文件,只选择你需要的 PHP 扩展
|
||||
WORKDIR /go/src/app/
|
||||
RUN EMBED=dist/app/ \
|
||||
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
|
||||
./build-static.sh
|
||||
```
|
||||
# 构建静态二进制文件
|
||||
WORKDIR /go/src/app/
|
||||
RUN EMBED=dist/app/ ./build-static.sh
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> 某些 .dockerignore 文件(例如默认的 [symfony-docker .dockerignore](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))会忽略 vendor
|
||||
> 文件夹和环境文件。在构建之前,请务必调整或删除 .dockerignore 文件。
|
||||
> [!CAUTION]
|
||||
>
|
||||
> 某些 `.dockerignore` 文件(例如默认的 [Symfony Docker `.dockerignore`](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))
|
||||
> 会忽略 `vendor/` 文件夹和 `.env` 文件。在构建之前,请务必调整或删除 `.dockerignore` 文件。
|
||||
|
||||
2. 构建:
|
||||
|
||||
```console
|
||||
docker build -t static-app -f static-build.Dockerfile .
|
||||
```
|
||||
```console
|
||||
docker build -t static-app -f static-build.Dockerfile .
|
||||
```
|
||||
|
||||
3. 提取二进制文件
|
||||
|
||||
```console
|
||||
docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp
|
||||
```
|
||||
```console
|
||||
docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp
|
||||
```
|
||||
|
||||
生成的二进制文件是当前目录中名为 `my-app` 的文件。
|
||||
|
||||
## 为其他操作系统创建二进制文件
|
||||
|
||||
如果您不想使用 Docker,或者想要构建 macOS 二进制文件,你可以使用我们提供的 shell 脚本:
|
||||
如果你不想使用 Docker,或者想要构建 macOS 二进制文件,你可以使用我们提供的 shell 脚本:
|
||||
|
||||
```console
|
||||
git clone https://github.com/dunglas/frankenphp
|
||||
git clone https://github.com/php/frankenphp
|
||||
cd frankenphp
|
||||
EMBED=/path/to/your/app \
|
||||
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
|
||||
./build-static.sh
|
||||
EMBED=/path/to/your/app ./build-static.sh
|
||||
```
|
||||
|
||||
在 `dist/` 目录中生成的二进制文件名称为 `frankenphp-<os>-<arch>`。
|
||||
|
||||
## 使用二进制文件
|
||||
|
||||
就是这样!`my-app` 文件(或其他操作系统上的 `dist/frankenphp-<os>-<arch>`)包含您的独立应用程序!
|
||||
就是这样!`my-app` 文件(或其他操作系统上的 `dist/frankenphp-<os>-<arch>`)包含你的独立应用程序!
|
||||
|
||||
若要启动 Web 应用,请执行:
|
||||
|
||||
@@ -102,7 +107,7 @@ EMBED=/path/to/your/app \
|
||||
./my-app php-server
|
||||
```
|
||||
|
||||
如果您的应用包含 [worker 脚本](worker.md),请使用如下命令启动 worker:
|
||||
如果你的应用包含 [worker 脚本](worker.md),请使用如下命令启动 worker:
|
||||
|
||||
```console
|
||||
./my-app php-server --worker public/index.php
|
||||
@@ -114,19 +119,26 @@ EMBED=/path/to/your/app \
|
||||
./my-app php-server --domain localhost
|
||||
```
|
||||
|
||||
您还可以运行二进制文件中嵌入的 PHP CLI 脚本:
|
||||
你还可以运行二进制文件中嵌入的 PHP CLI 脚本:
|
||||
|
||||
```console
|
||||
./my-app php-cli bin/console
|
||||
```
|
||||
|
||||
## PHP Extensions
|
||||
|
||||
默认情况下,脚本将构建您项目的 `composer.json` 文件中所需的扩展(如果有的话)。
|
||||
如果 `composer.json` 文件不存在,将构建默认扩展,如 [静态构建条目](static.md) 中所述。
|
||||
|
||||
要自定义扩展,请使用 `PHP_EXTENSIONS` 环境变量。
|
||||
|
||||
## 自定义构建
|
||||
|
||||
[阅读静态构建文档](static.md) 查看如何自定义二进制文件(扩展、PHP 版本等)。
|
||||
|
||||
## 分发二进制文件
|
||||
|
||||
创建的二进制文件不会被压缩。
|
||||
若要在发送文件之前减小文件的大小,可以对其进行压缩。
|
||||
在Linux上,创建的二进制文件使用[UPX](https://upx.github.io)进行压缩。
|
||||
|
||||
在Mac上,您可以在发送文件之前压缩它以减小文件大小。
|
||||
我们推荐使用 `xz`。
|
||||
|
||||
747
docs/cn/extensions.md
Normal file
747
docs/cn/extensions.md
Normal file
@@ -0,0 +1,747 @@
|
||||
# 使用 Go 编写 PHP 扩展
|
||||
|
||||
使用 FrankenPHP,你可以**使用 Go 编写 PHP 扩展**,这允许你创建**高性能的原生函数**,可以直接从 PHP 调用。你的应用程序可以利用任何现有或新的 Go 库,以及直接从你的 PHP 代码中使用**协程(goroutines)的并发模型**。
|
||||
|
||||
编写 PHP 扩展通常使用 C 语言完成,但通过一些额外的工作,也可以使用其他语言编写。PHP 扩展允许你利用底层语言的强大功能来扩展 PHP 的功能,例如,通过添加原生函数或优化特定操作。
|
||||
|
||||
借助 Caddy 模块,你可以使用 Go 编写 PHP 扩展,并将其快速集成到 FrankenPHP 中。
|
||||
|
||||
## 两种方法
|
||||
|
||||
FrankenPHP 提供两种方式来创建 Go 语言的 PHP 扩展:
|
||||
|
||||
1. **使用扩展生成器** - 推荐的方法,为大多数用例生成所有必要的样板代码,让你专注于编写 Go 代码
|
||||
2. **手动实现** - 对于高级用例,完全控制扩展结构
|
||||
|
||||
我们将从生成器方法开始,因为这是最简单的入门方式,然后为那些需要完全控制的人展示手动实现。
|
||||
|
||||
## 使用扩展生成器
|
||||
|
||||
FrankenPHP 捆绑了一个工具,允许你**仅使用 Go 创建 PHP 扩展**。**无需编写 C 代码**或直接使用 CGO:FrankenPHP 还包含一个**公共类型 API**,帮助你在 Go 中编写扩展,而无需担心**PHP/C 和 Go 之间的类型转换**。
|
||||
|
||||
> [!TIP]
|
||||
> 如果你想了解如何从头开始在 Go 中编写扩展,可以阅读下面的手动实现部分,该部分演示了如何在不使用生成器的情况下在 Go 中编写 PHP 扩展。
|
||||
|
||||
请记住,此工具**不是功能齐全的扩展生成器**。它旨在帮助你在 Go 中编写简单的扩展,但它不提供 PHP 扩展的最高级功能。如果你需要编写更**复杂和优化**的扩展,你可能需要编写一些 C 代码或直接使用 CGO。
|
||||
|
||||
### 先决条件
|
||||
|
||||
正如下面的手动实现部分所涵盖的,你需要[获取 PHP 源代码](https://www.php.net/downloads.php)并创建一个新的 Go 模块。
|
||||
|
||||
#### 创建新模块并获取 PHP 源代码
|
||||
|
||||
在 Go 中编写 PHP 扩展的第一步是创建一个新的 Go 模块。你可以使用以下命令:
|
||||
|
||||
```console
|
||||
go mod init github.com/my-account/my-module
|
||||
```
|
||||
|
||||
第二步是为后续步骤[获取 PHP 源代码](https://www.php.net/downloads.php)。获取后,将它们解压到你选择的目录中,不要放在你的 Go 模块内:
|
||||
|
||||
```console
|
||||
tar xf php-*
|
||||
```
|
||||
|
||||
### 编写扩展
|
||||
|
||||
现在一切都设置好了,可以在 Go 中编写你的原生函数。创建一个名为 `stringext.go` 的新文件。我们的第一个函数将接受一个字符串作为参数,重复次数,一个布尔值来指示是否反转字符串,并返回结果字符串。这应该看起来像这样:
|
||||
|
||||
```go
|
||||
import (
|
||||
"C"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
|
||||
func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(s))
|
||||
|
||||
result := strings.Repeat(str, int(count))
|
||||
if reverse {
|
||||
runes := []rune(result)
|
||||
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
|
||||
runes[i], runes[j] = runes[j], runes[i]
|
||||
}
|
||||
result = string(runes)
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(result, false)
|
||||
}
|
||||
```
|
||||
|
||||
这里有两个重要的事情要注意:
|
||||
|
||||
- 指令注释 `//export_php:function` 定义了 PHP 中的函数签名。这是生成器知道如何使用正确的参数和返回类型生成 PHP 函数的方式;
|
||||
- 函数必须返回 `unsafe.Pointer`。FrankenPHP 提供了一个 API 来帮助你在 C 和 Go 之间进行类型转换。
|
||||
|
||||
虽然第一点不言自明,但第二点可能更难理解。让我们在下一节中深入了解类型转换。
|
||||
|
||||
### 类型转换
|
||||
|
||||
虽然一些变量类型在 C/PHP 和 Go 之间具有相同的内存表示,但某些类型需要更多逻辑才能直接使用。这可能是编写扩展时最困难的部分,因为它需要了解 Zend 引擎的内部结构以及变量在 PHP 中的内部存储方式。此表总结了你需要知道的内容:
|
||||
|
||||
| PHP 类型 | Go 类型 | 直接转换 | C 到 Go 助手 | Go 到 C 助手 | 类方法支持 |
|
||||
| ------------------ | ------------------- | -------- | --------------------- | ---------------------- | ---------- |
|
||||
| `int` | `int64` | ✅ | - | - | ✅ |
|
||||
| `?int` | `*int64` | ✅ | - | - | ✅ |
|
||||
| `float` | `float64` | ✅ | - | - | ✅ |
|
||||
| `?float` | `*float64` | ✅ | - | - | ✅ |
|
||||
| `bool` | `bool` | ✅ | - | - | ✅ |
|
||||
| `?bool` | `*bool` | ✅ | - | - | ✅ |
|
||||
| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
|
||||
| `array` | `*frankenphp.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | ✅ |
|
||||
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
|
||||
| `object` | `struct` | ❌ | _尚未实现_ | _尚未实现_ | ❌ |
|
||||
|
||||
> [!NOTE]
|
||||
> 此表尚不详尽,将随着 FrankenPHP 类型 API 变得更加完整而完善。
|
||||
>
|
||||
> 特别是对于类方法,目前支持原始类型和数组。对象尚不能用作方法参数或返回类型。
|
||||
|
||||
如果你参考上一节的代码片段,你可以看到助手用于转换第一个参数和返回值。我们的 `repeat_this()` 函数的第二和第三个参数不需要转换,因为底层类型的内存表示对于 C 和 Go 都是相同的。
|
||||
|
||||
#### 处理数组
|
||||
|
||||
FrankenPHP 通过 `frankenphp.Array` 类型为 PHP 数组提供原生支持。此类型表示 PHP 索引数组(列表)和关联数组(哈希映射),具有有序的键值对。
|
||||
|
||||
**在 Go 中创建和操作数组:**
|
||||
|
||||
```go
|
||||
//export_php:function process_data(array $input): array
|
||||
func process_data(arr *C.zval) unsafe.Pointer {
|
||||
// 将 PHP 数组转换为 Go
|
||||
goArray := frankenphp.GoArray(unsafe.Pointer(arr))
|
||||
|
||||
result := &frankenphp.Array{}
|
||||
|
||||
result.SetInt(0, "first")
|
||||
result.SetInt(1, "second")
|
||||
result.Append("third") // 自动分配下一个整数键
|
||||
|
||||
result.SetString("name", "John")
|
||||
result.SetString("age", int64(30))
|
||||
|
||||
for i := uint32(0); i < goArray.Len(); i++ {
|
||||
key, value := goArray.At(i)
|
||||
if key.Type == frankenphp.PHPStringKey {
|
||||
result.SetString("processed_"+key.Str, value)
|
||||
} else {
|
||||
result.SetInt(key.Int+100, value)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换回 PHP 数组
|
||||
return frankenphp.PHPArray(result)
|
||||
}
|
||||
```
|
||||
|
||||
**`frankenphp.Array` 的关键特性:**
|
||||
|
||||
- **有序键值对** - 像 PHP 数组一样维护插入顺序
|
||||
- **混合键类型** - 在同一数组中支持整数和字符串键
|
||||
- **类型安全** - `PHPKey` 类型确保正确的键处理
|
||||
- **自动列表检测** - 转换为 PHP 时,自动检测数组应该是打包列表还是哈希映射
|
||||
- **不支持对象** - 目前,只有标量类型和数组可以用作值。提供对象将导致 PHP 数组中的 `null` 值。
|
||||
|
||||
**可用方法:**
|
||||
|
||||
- `SetInt(key int64, value any)` - 使用整数键设置值
|
||||
- `SetString(key string, value any)` - 使用字符串键设置值
|
||||
- `Append(value any)` - 使用下一个可用整数键添加值
|
||||
- `Len() uint32` - 获取元素数量
|
||||
- `At(index uint32) (PHPKey, any)` - 获取索引处的键值对
|
||||
- `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - 转换为 PHP 数组
|
||||
|
||||
### 声明原生 PHP 类
|
||||
|
||||
生成器支持将 Go 结构体声明为**不透明类**,可用于创建 PHP 对象。你可以使用 `//export_php:class` 指令注释来定义 PHP 类。例如:
|
||||
|
||||
```go
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
```
|
||||
|
||||
#### 什么是不透明类?
|
||||
|
||||
**不透明类**是内部结构(属性)对 PHP 代码隐藏的类。这意味着:
|
||||
|
||||
- **无直接属性访问**:你不能直接从 PHP 读取或写入属性(`$user->name` 不起作用)
|
||||
- **仅方法接口** - 所有交互必须通过你定义的方法进行
|
||||
- **更好的封装** - 内部数据结构完全由 Go 代码控制
|
||||
- **类型安全** - 没有 PHP 代码使用错误类型破坏内部状态的风险
|
||||
- **更清晰的 API** - 强制设计适当的公共接口
|
||||
|
||||
这种方法提供了更好的封装,并防止 PHP 代码意外破坏 Go 对象的内部状态。与对象的所有交互都必须通过你明确定义的方法进行。
|
||||
|
||||
#### 为类添加方法
|
||||
|
||||
由于属性不能直接访问,你**必须定义方法**来与不透明类交互。使用 `//export_php:method` 指令来定义行为:
|
||||
|
||||
```go
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
//export_php:method User::getName(): string
|
||||
func (us *UserStruct) GetUserName() unsafe.Pointer {
|
||||
return frankenphp.PHPString(us.Name, false)
|
||||
}
|
||||
|
||||
//export_php:method User::setAge(int $age): void
|
||||
func (us *UserStruct) SetUserAge(age int64) {
|
||||
us.Age = int(age)
|
||||
}
|
||||
|
||||
//export_php:method User::getAge(): int
|
||||
func (us *UserStruct) GetUserAge() int64 {
|
||||
return int64(us.Age)
|
||||
}
|
||||
|
||||
//export_php:method User::setNamePrefix(string $prefix = "User"): void
|
||||
func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
|
||||
us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + ": " + us.Name
|
||||
}
|
||||
```
|
||||
|
||||
#### 可空参数
|
||||
|
||||
生成器支持在 PHP 签名中使用 `?` 前缀的可空参数。当参数可空时,它在你的 Go 函数中变成指针,允许你检查值在 PHP 中是否为 `null`:
|
||||
|
||||
```go
|
||||
//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void
|
||||
func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {
|
||||
// 检查是否提供了 name(不为 null)
|
||||
if name != nil {
|
||||
us.Name = frankenphp.GoString(unsafe.Pointer(name))
|
||||
}
|
||||
|
||||
// 检查是否提供了 age(不为 null)
|
||||
if age != nil {
|
||||
us.Age = int(*age)
|
||||
}
|
||||
|
||||
// 检查是否提供了 active(不为 null)
|
||||
if active != nil {
|
||||
us.Active = *active
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关于可空参数的要点:**
|
||||
|
||||
- **可空原始类型**(`?int`、`?float`、`?bool`)在 Go 中变成指针(`*int64`、`*float64`、`*bool`)
|
||||
- **可空字符串**(`?string`)仍然是 `*C.zend_string`,但可以是 `nil`
|
||||
- **在解引用指针值之前检查 `nil`**
|
||||
- **PHP `null` 变成 Go `nil`** - 当 PHP 传递 `null` 时,你的 Go 函数接收 `nil` 指针
|
||||
|
||||
> [!WARNING]
|
||||
> 目前,类方法有以下限制。**不支持对象**作为参数类型或返回类型。**完全支持数组**作为参数和返回类型。支持的类型:`string`、`int`、`float`、`bool`、`array` 和 `void`(用于返回类型)。**完全支持可空参数类型**,适用于所有标量类型(`?string`、`?int`、`?float`、`?bool`)。
|
||||
|
||||
生成扩展后,你将被允许在 PHP 中使用类及其方法。请注意,你**不能直接访问属性**:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
$user = new User();
|
||||
|
||||
// ✅ 这可以工作 - 使用方法
|
||||
$user->setAge(25);
|
||||
echo $user->getName(); // 输出:(空,默认值)
|
||||
echo $user->getAge(); // 输出:25
|
||||
$user->setNamePrefix("Employee");
|
||||
|
||||
// ✅ 这也可以工作 - 可空参数
|
||||
$user->updateInfo("John", 30, true); // 提供所有参数
|
||||
$user->updateInfo("Jane", null, false); // Age 为 null
|
||||
$user->updateInfo(null, 25, null); // Name 和 active 为 null
|
||||
|
||||
// ❌ 这不会工作 - 直接属性访问
|
||||
// echo $user->name; // 错误:无法访问私有属性
|
||||
// $user->age = 30; // 错误:无法访问私有属性
|
||||
```
|
||||
|
||||
这种设计确保你的 Go 代码完全控制如何访问和修改对象的状态,提供更好的封装和类型安全。
|
||||
|
||||
### 声明常量
|
||||
|
||||
生成器支持使用两个指令将 Go 常量导出到 PHP:`//export_php:const` 用于全局常量,`//export_php:classconst` 用于类常量。这允许你在 Go 和 PHP 代码之间共享配置值、状态代码和其他常量。
|
||||
|
||||
#### 全局常量
|
||||
|
||||
使用 `//export_php:const` 指令创建全局 PHP 常量:
|
||||
|
||||
```go
|
||||
//export_php:const
|
||||
const MAX_CONNECTIONS = 100
|
||||
|
||||
//export_php:const
|
||||
const API_VERSION = "1.2.3"
|
||||
|
||||
//export_php:const
|
||||
const STATUS_OK = iota
|
||||
|
||||
//export_php:const
|
||||
const STATUS_ERROR = iota
|
||||
```
|
||||
|
||||
#### 类常量
|
||||
|
||||
使用 `//export_php:classconst ClassName` 指令创建属于特定 PHP 类的常量:
|
||||
|
||||
```go
|
||||
//export_php:classconst User
|
||||
const STATUS_ACTIVE = 1
|
||||
|
||||
//export_php:classconst User
|
||||
const STATUS_INACTIVE = 0
|
||||
|
||||
//export_php:classconst User
|
||||
const ROLE_ADMIN = "admin"
|
||||
|
||||
//export_php:classconst Order
|
||||
const STATE_PENDING = iota
|
||||
|
||||
//export_php:classconst Order
|
||||
const STATE_PROCESSING = iota
|
||||
|
||||
//export_php:classconst Order
|
||||
const STATE_COMPLETED = iota
|
||||
```
|
||||
|
||||
类常量在 PHP 中使用类名作用域访问:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// 全局常量
|
||||
echo MAX_CONNECTIONS; // 100
|
||||
echo API_VERSION; // "1.2.3"
|
||||
|
||||
// 类常量
|
||||
echo User::STATUS_ACTIVE; // 1
|
||||
echo User::ROLE_ADMIN; // "admin"
|
||||
echo Order::STATE_PENDING; // 0
|
||||
```
|
||||
|
||||
该指令支持各种值类型,包括字符串、整数、布尔值、浮点数和 iota 常量。使用 `iota` 时,生成器自动分配顺序值(0、1、2 等)。全局常量在你的 PHP 代码中作为全局常量可用,而类常量使用公共可见性限定在各自的类中。使用整数时,支持不同的可能记法(二进制、十六进制、八进制)并在 PHP 存根文件中按原样转储。
|
||||
|
||||
你可以像在 Go 代码中习惯的那样使用常量。例如,让我们采用我们之前声明的 `repeat_this()` 函数,并将最后一个参数更改为整数:
|
||||
|
||||
```go
|
||||
import (
|
||||
"C"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//export_php:const
|
||||
const STR_REVERSE = iota
|
||||
|
||||
//export_php:const
|
||||
const STR_NORMAL = iota
|
||||
|
||||
//export_php:classconst StringProcessor
|
||||
const MODE_LOWERCASE = 1
|
||||
|
||||
//export_php:classconst StringProcessor
|
||||
const MODE_UPPERCASE = 2
|
||||
|
||||
//export_php:function repeat_this(string $str, int $count, int $mode): string
|
||||
func repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(s))
|
||||
|
||||
result := strings.Repeat(str, int(count))
|
||||
if mode == STR_REVERSE {
|
||||
// 反转字符串
|
||||
}
|
||||
|
||||
if mode == STR_NORMAL {
|
||||
// 无操作,只是为了展示常量
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(result, false)
|
||||
}
|
||||
|
||||
//export_php:class StringProcessor
|
||||
type StringProcessorStruct struct {
|
||||
// 内部字段
|
||||
}
|
||||
|
||||
//export_php:method StringProcessor::process(string $input, int $mode): string
|
||||
func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(input))
|
||||
|
||||
switch mode {
|
||||
case MODE_LOWERCASE:
|
||||
str = strings.ToLower(str)
|
||||
case MODE_UPPERCASE:
|
||||
str = strings.ToUpper(str)
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(str, false)
|
||||
}
|
||||
```
|
||||
|
||||
### 使用命名空间
|
||||
|
||||
生成器支持使用 `//export_php:namespace` 指令将 PHP 扩展的函数、类和常量组织在命名空间下。这有助于避免命名冲突,并为扩展的 API 提供更好的组织。
|
||||
|
||||
#### 声明命名空间
|
||||
|
||||
在你的 Go 文件顶部使用 `//export_php:namespace` 指令,将所有导出的符号放在特定命名空间下:
|
||||
|
||||
```go
|
||||
//export_php:namespace My\Extension
|
||||
package main
|
||||
|
||||
import "C"
|
||||
|
||||
//export_php:function hello(): string
|
||||
func hello() string {
|
||||
return "Hello from My\\Extension namespace!"
|
||||
}
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
// 内部字段
|
||||
}
|
||||
|
||||
//export_php:method User::getName(): string
|
||||
func (u *UserStruct) GetName() unsafe.Pointer {
|
||||
return frankenphp.PHPString("John Doe", false)
|
||||
}
|
||||
|
||||
//export_php:const
|
||||
const STATUS_ACTIVE = 1
|
||||
```
|
||||
|
||||
#### 在 PHP 中使用命名空间扩展
|
||||
|
||||
当声明命名空间时,所有函数、类和常量都放在 PHP 中的该命名空间下:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
echo My\Extension\hello(); // "Hello from My\Extension namespace!"
|
||||
|
||||
$user = new My\Extension\User();
|
||||
echo $user->getName(); // "John Doe"
|
||||
|
||||
echo My\Extension\STATUS_ACTIVE; // 1
|
||||
```
|
||||
|
||||
#### 重要说明
|
||||
|
||||
- 每个文件只允许**一个**命名空间指令。如果找到多个命名空间指令,生成器将返回错误。
|
||||
- 命名空间适用于文件中的**所有**导出符号:函数、类、方法和常量。
|
||||
- 命名空间名称遵循 PHP 命名空间约定,使用反斜杠(`\`)作为分隔符。
|
||||
- 如果没有声明命名空间,符号将照常导出到全局命名空间。
|
||||
|
||||
### 生成扩展
|
||||
|
||||
这就是魔法发生的地方,现在可以生成你的扩展。你可以使用以下命令运行生成器:
|
||||
|
||||
```console
|
||||
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> 不要忘记将 `GEN_STUB_SCRIPT` 环境变量设置为你之前下载的 PHP 源代码中 `gen_stub.php` 文件的路径。这是在手动实现部分中提到的同一个 `gen_stub.php` 脚本。
|
||||
|
||||
如果一切顺利,应该创建了一个名为 `build` 的新目录。此目录包含扩展的生成文件,包括带有生成的 PHP 函数存根的 `my_extension.go` 文件。
|
||||
|
||||
### 将生成的扩展集成到 FrankenPHP 中
|
||||
|
||||
我们的扩展现在已准备好编译并集成到 FrankenPHP 中。为此,请参阅 FrankenPHP [编译文档](compile.md)以了解如何编译 FrankenPHP。使用 `--with` 标志添加模块,指向你的模块路径:
|
||||
|
||||
```console
|
||||
CGO_ENABLED=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
||||
CGO_CFLAGS=$(php-config --includes) \
|
||||
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
||||
xcaddy build \
|
||||
--output frankenphp \
|
||||
--with github.com/my-account/my-module/build
|
||||
```
|
||||
|
||||
请注意,你指向在生成步骤中创建的 `/build` 子目录。但是,这不是强制性的:你也可以将生成的文件复制到你的模块目录并直接指向它。
|
||||
|
||||
### 测试你的生成扩展
|
||||
|
||||
你可以创建一个 PHP 文件来测试你创建的函数和类。例如,创建一个包含以下内容的 `index.php` 文件:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// 使用全局常量
|
||||
var_dump(repeat_this('Hello World', 5, STR_REVERSE));
|
||||
|
||||
// 使用类常量
|
||||
$processor = new StringProcessor();
|
||||
echo $processor->process('Hello World', StringProcessor::MODE_LOWERCASE); // "hello world"
|
||||
echo $processor->process('Hello World', StringProcessor::MODE_UPPERCASE); // "HELLO WORLD"
|
||||
```
|
||||
|
||||
一旦你按照上一节所示将扩展集成到 FrankenPHP 中,你就可以使用 `./frankenphp php-server` 运行此测试文件,你应该看到你的扩展正在工作。
|
||||
|
||||
## 手动实现
|
||||
|
||||
如果你想了解扩展的工作原理或需要完全控制你的扩展,你可以手动编写它们。这种方法给你完全的控制,但需要更多的样板代码。
|
||||
|
||||
### 基本函数
|
||||
|
||||
我们将看到如何在 Go 中编写一个简单的 PHP 扩展,定义一个新的原生函数。此函数将从 PHP 调用,并将触发一个在 Caddy 日志中记录消息的协程。此函数不接受任何参数并且不返回任何内容。
|
||||
|
||||
#### 定义 Go 函数
|
||||
|
||||
在你的模块中,你需要定义一个新的原生函数,该函数将从 PHP 调用。为此,创建一个你想要的名称的文件,例如 `extension.go`,并添加以下代码:
|
||||
|
||||
```go
|
||||
package ext_go
|
||||
|
||||
//#include "extension.h"
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
|
||||
}
|
||||
|
||||
//export go_print_something
|
||||
func go_print_something() {
|
||||
go func() {
|
||||
caddy.Log().Info("Hello from a goroutine!")
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
`frankenphp.RegisterExtension()` 函数通过处理内部 PHP 注册逻辑简化了扩展注册过程。`go_print_something` 函数使用 `//export` 指令表示它将在我们将编写的 C 代码中可访问,这要归功于 CGO。
|
||||
|
||||
在此示例中,我们的新函数将触发一个在 Caddy 日志中记录消息的协程。
|
||||
|
||||
#### 定义 PHP 函数
|
||||
|
||||
为了允许 PHP 调用我们的函数,我们需要定义相应的 PHP 函数。为此,我们将创建一个存根文件,例如 `extension.stub.php`,其中包含以下代码:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @generate-class-entries */
|
||||
|
||||
function go_print(): void {}
|
||||
```
|
||||
|
||||
此文件定义了 `go_print()` 函数的签名,该函数将从 PHP 调用。`@generate-class-entries` 指令允许 PHP 自动为我们的扩展生成函数条目。
|
||||
|
||||
这不是手动完成的,而是使用 PHP 源代码中提供的脚本(确保根据你的 PHP 源代码所在位置调整 `gen_stub.php` 脚本的路径):
|
||||
|
||||
```bash
|
||||
php ../php-src/build/gen_stub.php extension.stub.php
|
||||
```
|
||||
|
||||
此脚本将生成一个名为 `extension_arginfo.h` 的文件,其中包含 PHP 知道如何定义和调用我们函数所需的信息。
|
||||
|
||||
#### 编写 Go 和 C 之间的桥梁
|
||||
|
||||
现在,我们需要编写 Go 和 C 之间的桥梁。在你的模块目录中创建一个名为 `extension.h` 的文件,内容如下:
|
||||
|
||||
```c
|
||||
#ifndef _EXTENSION_H
|
||||
#define _EXTENSION_H
|
||||
|
||||
#include <php.h>
|
||||
|
||||
extern zend_module_entry ext_module_entry;
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
接下来,创建一个名为 `extension.c` 的文件,该文件将执行以下步骤:
|
||||
|
||||
- 包含 PHP 头文件;
|
||||
- 声明我们的新原生 PHP 函数 `go_print()`;
|
||||
- 声明扩展元数据。
|
||||
|
||||
让我们首先包含所需的头文件:
|
||||
|
||||
```c
|
||||
#include <php.h>
|
||||
#include "extension.h"
|
||||
#include "extension_arginfo.h"
|
||||
|
||||
// 包含 Go 导出的符号
|
||||
#include "_cgo_export.h"
|
||||
```
|
||||
|
||||
然后我们将 PHP 函数定义为原生语言函数:
|
||||
|
||||
```c
|
||||
PHP_FUNCTION(go_print)
|
||||
{
|
||||
ZEND_PARSE_PARAMETERS_NONE();
|
||||
|
||||
go_print_something();
|
||||
}
|
||||
|
||||
zend_module_entry ext_module_entry = {
|
||||
STANDARD_MODULE_HEADER,
|
||||
"ext_go",
|
||||
ext_functions, /* Functions */
|
||||
NULL, /* MINIT */
|
||||
NULL, /* MSHUTDOWN */
|
||||
NULL, /* RINIT */
|
||||
NULL, /* RSHUTDOWN */
|
||||
NULL, /* MINFO */
|
||||
"0.1.1",
|
||||
STANDARD_MODULE_PROPERTIES
|
||||
};
|
||||
```
|
||||
|
||||
在这种情况下,我们的函数不接受参数并且不返回任何内容。它只是调用我们之前定义的 Go 函数,使用 `//export` 指令导出。
|
||||
|
||||
最后,我们在 `zend_module_entry` 结构中定义扩展的元数据,例如其名称、版本和属性。这些信息对于 PHP 识别和加载我们的扩展是必需的。请注意,`ext_functions` 是指向我们定义的 PHP 函数的指针数组,它由 `gen_stub.php` 脚本在 `extension_arginfo.h` 文件中自动生成。
|
||||
|
||||
扩展注册由我们在 Go 代码中调用的 FrankenPHP 的 `RegisterExtension()` 函数自动处理。
|
||||
|
||||
### 高级用法
|
||||
|
||||
现在我们知道了如何在 Go 中创建基本的 PHP 扩展,让我们复杂化我们的示例。我们现在将创建一个 PHP 函数,该函数接受一个字符串作为参数并返回其大写版本。
|
||||
|
||||
#### 定义 PHP 函数存根
|
||||
|
||||
为了定义新的 PHP 函数,我们将修改我们的 `extension.stub.php` 文件以包含新的函数签名:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @generate-class-entries */
|
||||
|
||||
/**
|
||||
* 将字符串转换为大写。
|
||||
*
|
||||
* @param string $string 要转换的字符串。
|
||||
* @return string 字符串的大写版本。
|
||||
*/
|
||||
function go_upper(string $string): string {}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> 不要忽视函数的文档!你可能会与其他开发人员共享扩展存根,以记录如何使用你的扩展以及哪些功能可用。
|
||||
|
||||
通过使用 `gen_stub.php` 脚本重新生成存根文件,`extension_arginfo.h` 文件应该如下所示:
|
||||
|
||||
```c
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)
|
||||
ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
ZEND_FUNCTION(go_upper);
|
||||
|
||||
static const zend_function_entry ext_functions[] = {
|
||||
ZEND_FE(go_upper, arginfo_go_upper)
|
||||
ZEND_FE_END
|
||||
};
|
||||
```
|
||||
|
||||
我们可以看到 `go_upper` 函数定义了一个 `string` 类型的参数和一个 `string` 的返回类型。
|
||||
|
||||
#### Go 和 PHP/C 之间的类型转换
|
||||
|
||||
你的 Go 函数不能直接接受 PHP 字符串作为参数。你需要将其转换为 Go 字符串。幸运的是,FrankenPHP 提供了助手函数来处理 PHP 字符串和 Go 字符串之间的转换,类似于我们在生成器方法中看到的。
|
||||
|
||||
头文件保持简单:
|
||||
|
||||
```c
|
||||
#ifndef _EXTENSION_H
|
||||
#define _EXTENSION_H
|
||||
|
||||
#include <php.h>
|
||||
|
||||
extern zend_module_entry ext_module_entry;
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
我们现在可以在我们的 `extension.c` 文件中编写 Go 和 C 之间的桥梁。我们将 PHP 字符串直接传递给我们的 Go 函数:
|
||||
|
||||
```c
|
||||
PHP_FUNCTION(go_upper)
|
||||
{
|
||||
zend_string *str;
|
||||
|
||||
ZEND_PARSE_PARAMETERS_START(1, 1)
|
||||
Z_PARAM_STR(str)
|
||||
ZEND_PARSE_PARAMETERS_END();
|
||||
|
||||
zend_string *result = go_upper(str);
|
||||
RETVAL_STR(result);
|
||||
}
|
||||
```
|
||||
|
||||
你可以在 [PHP 内部手册](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters) 的专门页面中了解更多关于 `ZEND_PARSE_PARAMETERS_START` 和参数解析的信息。在这里,我们告诉 PHP 我们的函数接受一个 `string` 类型的强制参数作为 `zend_string`。然后我们将此字符串直接传递给我们的 Go 函数,并使用 `RETVAL_STR` 返回结果。
|
||||
|
||||
只剩下一件事要做:在 Go 中实现 `go_upper` 函数。
|
||||
|
||||
#### 实现 Go 函数
|
||||
|
||||
我们的 Go 函数将接受 `*C.zend_string` 作为参数,使用 FrankenPHP 的助手函数将其转换为 Go 字符串,处理它,并将结果作为新的 `*C.zend_string` 返回。助手函数为我们处理所有内存管理和转换复杂性。
|
||||
|
||||
```go
|
||||
import "strings"
|
||||
|
||||
//export go_upper
|
||||
func go_upper(s *C.zend_string) *C.zend_string {
|
||||
str := frankenphp.GoString(unsafe.Pointer(s))
|
||||
|
||||
upper := strings.ToUpper(str)
|
||||
|
||||
return (*C.zend_string)(frankenphp.PHPString(upper, false))
|
||||
}
|
||||
```
|
||||
|
||||
这种方法比手动内存管理更清洁、更安全。FrankenPHP 的助手函数自动处理 PHP 的 `zend_string` 格式和 Go 字符串之间的转换。`PHPString()` 中的 `false` 参数表示我们想要创建一个新的非持久字符串(在请求结束时释放)。
|
||||
|
||||
> [!TIP]
|
||||
> 在此示例中,我们不执行任何错误处理,但你应该始终检查指针不是 `nil` 并且数据在 Go 函数中使用之前是有效的。
|
||||
|
||||
### 将扩展集成到 FrankenPHP 中
|
||||
|
||||
我们的扩展现在已准备好编译并集成到 FrankenPHP 中。为此,请参阅 FrankenPHP [编译文档](compile.md)以了解如何编译 FrankenPHP。使用 `--with` 标志添加模块,指向你的模块路径:
|
||||
|
||||
```console
|
||||
CGO_ENABLED=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
||||
CGO_CFLAGS=$(php-config --includes) \
|
||||
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
||||
xcaddy build \
|
||||
--output frankenphp \
|
||||
--with github.com/my-account/my-module
|
||||
```
|
||||
|
||||
就是这样!你的扩展现在集成到 FrankenPHP 中,可以在你的 PHP 代码中使用。
|
||||
|
||||
### 测试你的扩展
|
||||
|
||||
将扩展集成到 FrankenPHP 后,你可以为你实现的函数创建一个包含示例的 `index.php` 文件:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// 测试基本函数
|
||||
go_print();
|
||||
|
||||
// 测试高级函数
|
||||
echo go_upper("hello world") . "\n";
|
||||
```
|
||||
|
||||
你现在可以使用 `./frankenphp php-server` 运行带有此文件的 FrankenPHP,你应该看到你的扩展正在工作。
|
||||
@@ -1,13 +1,13 @@
|
||||
# 使用 GitHub Actions
|
||||
|
||||
此存储库构建 Docker 镜像并将其部署到 [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) 上
|
||||
每个批准的拉取请求或设置后在您自己的分支上。
|
||||
每个批准的拉取请求或设置后在你自己的分支上。
|
||||
|
||||
## 设置 GitHub Actions
|
||||
|
||||
在存储库设置中的 `secrets` 下,添加以下字段:
|
||||
|
||||
- `REGISTRY_LOGIN_SERVER`: 要使用的 docker registry(如 `docker.io`)。
|
||||
- `REGISTRY_LOGIN_SERVER`: 要使用的 Docker registry(如 `docker.io`)。
|
||||
- `REGISTRY_USERNAME`: 用于登录 registry 的用户名(如 `dunglas`)。
|
||||
- `REGISTRY_PASSWORD`: 用于登录 registry 的密码(如 `access key`)。
|
||||
- `IMAGE_NAME`: 镜像的名称(如 `dunglas/frankenphp`)。
|
||||
|
||||
@@ -4,9 +4,18 @@
|
||||
|
||||
已知以下扩展与 FrankenPHP 不兼容:
|
||||
|
||||
| 名称 | 原因 | 替代方案 |
|
||||
|-------------------------------------------------------------|-------|----------------------------------------------------------------------------------------------------------------------|
|
||||
| [imap](https://www.php.net/manual/en/imap.installation.php) | 非线程安全 | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |
|
||||
| 名称 | 原因 | 替代方案 |
|
||||
| ----------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| [imap](https://www.php.net/manual/en/imap.installation.php) | 不安全的线程 | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |
|
||||
| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | 不安全的线程 | - |
|
||||
|
||||
## 有缺陷的 PHP 扩展
|
||||
|
||||
以下扩展在与 FrankenPHP 一起使用时已知存在错误和意外行为:
|
||||
|
||||
| 名称 | 问题 |
|
||||
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [ext-openssl](https://www.php.net/manual/en/book.openssl.php) | 在使用静态构建的 FrankenPHP(使用 musl libc 构建)时,在重负载下 OpenSSL 扩展可能会崩溃。一个解决方法是使用动态链接的构建(如 Docker 镜像中使用的版本)。此错误正在由 PHP 跟踪。[查看问题](https://github.com/php/php-src/issues/13648)。 |
|
||||
|
||||
## get_browser
|
||||
|
||||
@@ -14,8 +23,7 @@
|
||||
|
||||
## 独立的二进制和基于 Alpine 的 Docker 镜像
|
||||
|
||||
独立的二进制文件和基于 Alpine 的 docker 镜像 (`dunglas/frankenphp:*-alpine`) 使用的是 [musl libc](https://musl.libc.org/) 而不是 [glibc and friends](https://www.etalabs.net/compare_libcs.html),为的是保持较小的二进制大小。
|
||||
这可能会导致一些兼容性问题。特别是,glob 标志 `GLOB_BRACE` [不可用](https://www.php.net/manual/en/function.glob.php)。
|
||||
独立的二进制文件和基于 Alpine 的 Docker 镜像 (`dunglas/frankenphp:*-alpine`) 使用的是 [musl libc](https://musl.libc.org/) 而不是 [glibc and friends](https://www.etalabs.net/compare_libcs.html),为的是保持较小的二进制大小。这可能会导致一些兼容性问题。特别是,glob 标志 `GLOB_BRACE` [不可用](https://www.php.net/manual/en/function.glob.php)。
|
||||
|
||||
## 在 Docker 中使用 `https://127.0.0.1`
|
||||
|
||||
@@ -25,7 +33,7 @@
|
||||
如果确实想使用 `127.0.0.1` 作为主机,可以通过将服务器名称设置为 `127.0.0.1` 来配置它以为其生成证书。
|
||||
|
||||
如果你使用 Docker,因为 [Docker 网络](https://docs.docker.com/network/) 问题,只做这些是不够的。
|
||||
您将收到类似于以下内容的 TLS 错误 `curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`。
|
||||
你将收到类似于以下内容的 TLS 错误 `curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`。
|
||||
|
||||
如果你使用的是 Linux,解决方案是使用 [使用宿主机网络](https://docs.docker.com/network/network-tutorial-host/):
|
||||
|
||||
@@ -37,7 +45,7 @@ docker run \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
Mac 和 Windows 不支持 Docker 使用宿主机网络。在这些平台上,您必须猜测容器的 IP 地址并将其包含在服务器名称中。
|
||||
Mac 和 Windows 不支持 Docker 使用宿主机网络。在这些平台上,你必须猜测容器的 IP 地址并将其包含在服务器名称中。
|
||||
|
||||
运行 `docker network inspect bridge` 并查看 `Containers`,找到 `IPv4Address` 当前分配的最后一个 IP 地址,并增加 1。如果没有容器正在运行,则第一个分配的 IP 地址通常为 `172.17.0.2`。
|
||||
|
||||
@@ -55,7 +63,7 @@ docker run \
|
||||
>
|
||||
> 请务必将 `172.17.0.3` 替换为将分配给容器的 IP。
|
||||
|
||||
您现在应该能够从主机访问 `https://127.0.0.1`。
|
||||
你现在应该能够从主机访问 `https://127.0.0.1`。
|
||||
|
||||
如果不是这种情况,请在调试模式下启动 FrankenPHP 以尝试找出问题:
|
||||
|
||||
@@ -67,3 +75,69 @@ docker run \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
## Composer 脚本引用 `@php`
|
||||
|
||||
[Composer 脚本](https://getcomposer.org/doc/articles/scripts.md) 可能想要执行一个 PHP 二进制文件来完成一些任务,例如在 [Laravel 项目](laravel.md) 中运行 `@php artisan package:discover --ansi`。这 [目前失败](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) 的原因有两个:
|
||||
|
||||
- Composer 不知道如何调用 FrankenPHP 二进制文件;
|
||||
- Composer 可以在命令中使用 `-d` 标志添加 PHP 设置,而 FrankenPHP 目前尚不支持。
|
||||
|
||||
作为一种变通方法,我们可以在 `/usr/local/bin/php` 中创建一个 Shell 脚本,该脚本会去掉不支持的参数,然后调用 FrankenPHP:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
args=("$@")
|
||||
index=0
|
||||
for i in "$@"
|
||||
do
|
||||
if [ "$i" == "-d" ]; then
|
||||
unset 'args[$index]'
|
||||
unset 'args[$index+1]'
|
||||
fi
|
||||
index=$((index+1))
|
||||
done
|
||||
|
||||
/usr/local/bin/frankenphp php-cli ${args[@]}
|
||||
```
|
||||
|
||||
然后将环境变量 `PHP_BINARY` 设置为我们 `php` 脚本的路径,并运行 Composer:
|
||||
|
||||
```console
|
||||
export PHP_BINARY=/usr/local/bin/php
|
||||
composer install
|
||||
```
|
||||
|
||||
## 使用静态二进制文件排查 TLS/SSL 问题
|
||||
|
||||
在使用静态二进制文件时,您可能会遇到以下与TLS相关的错误,例如在使用STARTTLS发送电子邮件时:
|
||||
|
||||
```text
|
||||
Unable to connect with STARTTLS: stream_socket_enable_crypto(): SSL operation failed with code 5. OpenSSL Error messages:
|
||||
error:80000002:system library::No such file or directory
|
||||
error:80000002:system library::No such file or directory
|
||||
error:80000002:system library::No such file or directory
|
||||
error:0A000086:SSL routines::certificate verify failed
|
||||
```
|
||||
|
||||
由于静态二进制不捆绑 TLS 证书,因此您需要将 OpenSSL 指向本地 CA 证书安装。
|
||||
|
||||
检查 [`openssl_get_cert_locations()`](https://www.php.net/manual/en/function.openssl-get-cert-locations.php) 的输出,
|
||||
以找到 CA 证书必须安装的位置,并将它们存储在该位置。
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> Web 和命令行界面可能有不同的设置。
|
||||
> 确保在适当的上下文中运行 `openssl_get_cert_locations()`。
|
||||
|
||||
[从Mozilla提取的CA证书可以在curl网站上下载](https://curl.se/docs/caextract.html)。
|
||||
|
||||
或者,许多发行版,包括 Debian、Ubuntu 和 Alpine,提供名为 `ca-certificates` 的软件包,其中包含这些证书。
|
||||
|
||||
还可以使用 `SSL_CERT_FILE` 和 `SSL_CERT_DIR` 来提示 OpenSSL 在哪里查找 CA 证书:
|
||||
|
||||
```console
|
||||
# Set TLS certificates environment variables
|
||||
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
export SSL_CERT_DIR=/etc/ssl/certs
|
||||
```
|
||||
|
||||
@@ -16,24 +16,26 @@ docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
|
||||
|
||||
或者,你可以从本地机器上使用 FrankenPHP 运行 Laravel 项目:
|
||||
|
||||
1. [下载与您的系统相对应的二进制文件](https://github.com/dunglas/frankenphp/releases)
|
||||
1. [下载与你的系统相对应的二进制文件](https://github.com/php/frankenphp/releases)
|
||||
2. 将以下配置添加到 Laravel 项目根目录中名为 `Caddyfile` 的文件中:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp
|
||||
}
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp
|
||||
}
|
||||
|
||||
# 服务器的域名
|
||||
localhost {
|
||||
# 将 webroot 设置为 public/ 目录
|
||||
root * public/
|
||||
# 启用压缩(可选)
|
||||
encode zstd br gzip
|
||||
# 执行当前目录中的 PHP 文件并提供资产
|
||||
php_server
|
||||
}
|
||||
```
|
||||
# 服务器的域名
|
||||
localhost {
|
||||
# 将 webroot 设置为 public/ 目录
|
||||
root public/
|
||||
# 启用压缩(可选)
|
||||
encode zstd br gzip
|
||||
# 执行当前目录中的 PHP 文件并提供资源
|
||||
php_server {
|
||||
try_files {path} index.php
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 从 Laravel 项目的根目录启动 FrankenPHP:`frankenphp run`
|
||||
|
||||
@@ -45,7 +47,7 @@ Octane 可以通过 Composer 包管理器安装:
|
||||
composer require laravel/octane
|
||||
```
|
||||
|
||||
安装 Octane 后,您可以执行 `octane:install` Artisan 命令,该命令会将 Octane 的配置文件安装到您的应用程序中:
|
||||
安装 Octane 后,你可以执行 `octane:install` Artisan 命令,该命令会将 Octane 的配置文件安装到你的应用程序中:
|
||||
|
||||
```console
|
||||
php artisan octane:install --server=frankenphp
|
||||
@@ -59,16 +61,125 @@ php artisan octane:frankenphp
|
||||
|
||||
`octane:frankenphp` 命令可以采用以下选项:
|
||||
|
||||
* `--host`: 服务器应绑定到的 IP 地址(默认值: `127.0.0.1`)
|
||||
* `--port`: 服务器应可用的端口(默认值: `8000`)
|
||||
* `--admin-port`: 管理服务器应可用的端口(默认值: `2019`)
|
||||
* `--workers`: 应可用于处理请求的 worker 数(默认值: `auto`)
|
||||
* `--max-requests`: 在 worker 重启之前要处理的请求数(默认值: `500`)
|
||||
* `--caddyfile`: FrankenPHP `Caddyfile` 文件的路径
|
||||
* `--https`: 开启 HTTPS、HTTP/2 和 HTTP/3,自动生成和延长证书
|
||||
* `--http-redirect`: 启用 HTTP 到 HTTPS 重定向(仅在使用 `--https` 时启用)
|
||||
* `--watch`: 修改应用程序时自动重新加载服务器
|
||||
* `--poll`: 在监视时使用文件系统轮询,以便通过网络监视文件
|
||||
* `--log-level`: 在指定日志级别或高于指定日志级别的日志消息
|
||||
- `--host`: 服务器应绑定到的 IP 地址(默认值: `127.0.0.1`)
|
||||
- `--port`: 服务器应可用的端口(默认值: `8000`)
|
||||
- `--admin-port`: 管理服务器应可用的端口(默认值: `2019`)
|
||||
- `--workers`: 应可用于处理请求的 worker 数(默认值: `auto`)
|
||||
- `--max-requests`: 在 worker 重启之前要处理的请求数(默认值: `500`)
|
||||
- `--caddyfile`:FrankenPHP `Caddyfile` 文件的路径(默认: [Laravel Octane 中的存根 `Caddyfile`](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile))
|
||||
- `--https`: 开启 HTTPS、HTTP/2 和 HTTP/3,自动生成和延长证书
|
||||
- `--http-redirect`: 启用 HTTP 到 HTTPS 重定向(仅在使用 `--https` 时启用)
|
||||
- `--watch`: 修改应用程序时自动重新加载服务器
|
||||
- `--poll`: 在监视时使用文件系统轮询,以便通过网络监视文件
|
||||
- `--log-level`: 在指定日志级别或高于指定日志级别的日志消息
|
||||
|
||||
> [!TIP]
|
||||
> 要获取结构化的 JSON 日志(在使用日志分析解决方案时非常有用),请明确传递 `--log-level` 选项。
|
||||
|
||||
你可以了解更多关于 [Laravel Octane 官方文档](https://laravel.com/docs/octane)。
|
||||
|
||||
## Laravel 应用程序作为独立的可执行文件
|
||||
|
||||
使用[FrankenPHP 的应用嵌入功能](embed.md),可以将 Laravel 应用程序作为
|
||||
独立的二进制文件分发。
|
||||
|
||||
按照以下步骤将您的Laravel应用程序打包为Linux的独立二进制文件:
|
||||
|
||||
1. 在您的应用程序的存储库中创建一个名为 `static-build.Dockerfile` 的文件:
|
||||
|
||||
```dockerfile
|
||||
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
|
||||
# 如果你打算在 musl-libc 系统上运行该二进制文件,请使用 static-builder-musl
|
||||
|
||||
# 复制你的应用
|
||||
WORKDIR /go/src/app/dist/app
|
||||
COPY . .
|
||||
|
||||
# 删除测试和其他不必要的文件以节省空间
|
||||
# 或者,将这些文件添加到 .dockerignore 文件中
|
||||
RUN rm -Rf tests/
|
||||
|
||||
# 复制 .env 文件
|
||||
RUN cp .env.example .env
|
||||
# 将 APP_ENV 和 APP_DEBUG 更改为适合生产环境
|
||||
RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env
|
||||
|
||||
# 根据需要对您的 .env 文件进行其他更改
|
||||
|
||||
# 安装依赖项
|
||||
RUN composer install --ignore-platform-reqs --no-dev -a
|
||||
|
||||
# 构建静态二进制文件
|
||||
WORKDIR /go/src/app/
|
||||
RUN EMBED=dist/app/ ./build-static.sh
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> 一些 `.dockerignore` 文件
|
||||
> 将忽略 `vendor/` 目录和 `.env` 文件。在构建之前,请确保调整或删除 `.dockerignore` 文件。
|
||||
|
||||
2. 构建:
|
||||
|
||||
```console
|
||||
docker build -t static-laravel-app -f static-build.Dockerfile .
|
||||
```
|
||||
|
||||
3. 提取二进制:
|
||||
|
||||
```console
|
||||
docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp
|
||||
```
|
||||
|
||||
4. 填充缓存:
|
||||
|
||||
```console
|
||||
frankenphp php-cli artisan optimize
|
||||
```
|
||||
|
||||
5. 运行数据库迁移(如果有的话):
|
||||
|
||||
```console
|
||||
frankenphp php-cli artisan migrate
|
||||
```
|
||||
|
||||
6. 生成应用程序的密钥:
|
||||
|
||||
```console
|
||||
frankenphp php-cli artisan key:generate
|
||||
```
|
||||
|
||||
7. 启动服务器:
|
||||
|
||||
```console
|
||||
frankenphp php-server
|
||||
```
|
||||
|
||||
您的应用程序现在准备好了!
|
||||
|
||||
了解有关可用选项的更多信息,以及如何为其他操作系统构建二进制文件,请参见 [应用程序嵌入](embed.md)
|
||||
文档。
|
||||
|
||||
### 更改存储路径
|
||||
|
||||
默认情况下,Laravel 将上传的文件、缓存、日志等存储在应用程序的 `storage/` 目录中。
|
||||
这不适合嵌入式应用,因为每个新版本将被提取到不同的临时目录中。
|
||||
|
||||
设置 `LARAVEL_STORAGE_PATH` 环境变量(例如,在 `.env` 文件中)或调用 `Illuminate\Foundation\Application::useStoragePath()` 方法以使用临时目录之外的目录。
|
||||
|
||||
### 使用独立二进制文件运行 Octane
|
||||
|
||||
甚至可以将 Laravel Octane 应用打包为独立的二进制文件!
|
||||
|
||||
为此,[正确安装 Octane](#laravel-octane) 并遵循 [前一部分](#laravel-应用程序作为独立的可执行文件) 中描述的步骤。
|
||||
|
||||
然后,通过 Octane 在工作模式下启动 FrankenPHP,运行:
|
||||
|
||||
```console
|
||||
PATH="$PWD:$PATH" frankenphp php-cli artisan octane:frankenphp
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> 为了使命令有效,独立二进制文件**必须**命名为 `frankenphp`
|
||||
> 因为 Octane 需要一个名为 `frankenphp` 的程序在路径中可用。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 实时
|
||||
|
||||
FrankenPHP 带有一个内置的 Mercure Hub!
|
||||
FrankenPHP 配备了内置的 [Mercure](https://mercure.rocks) 中心!
|
||||
Mercure 允许将事件实时推送到所有连接的设备:它们将立即收到 JavaScript 事件。
|
||||
|
||||
无需 JS 库或 SDK!
|
||||
@@ -9,4 +9,7 @@ Mercure 允许将事件实时推送到所有连接的设备:它们将立即收
|
||||
|
||||
要启用 Mercure Hub,请按照 [Mercure 网站](https://mercure.rocks/docs/hub/config) 中的说明更新 `Caddyfile`。
|
||||
|
||||
要从您的代码中推送 Mercure 更新,我们推荐 [Symfony Mercure Component](https://symfony.com/components/Mercure)(不需要 Symfony 框架来使用)。
|
||||
Mercure hub 的路径是`/.well-known/mercure`.
|
||||
在 Docker 中运行 FrankenPHP 时,完整的发送 URL 将类似于 `http://php/.well-known/mercure` (其中 `php` 是运行 FrankenPHP 的容器名称)。
|
||||
|
||||
要从你的代码中推送 Mercure 更新,我们推荐 [Symfony Mercure Component](https://symfony.com/components/Mercure)(不需要 Symfony 框架来使用)。
|
||||
|
||||
17
docs/cn/metrics.md
Normal file
17
docs/cn/metrics.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# 指标
|
||||
|
||||
当启用 [Caddy 指标](https://caddyserver.com/docs/metrics) 时,FrankenPHP 公开以下指标:
|
||||
|
||||
- `frankenphp_total_threads`:PHP 线程的总数。
|
||||
- `frankenphp_busy_threads`:当前正在处理请求的 PHP 线程数(运行中的 worker 始终占用一个线程)。
|
||||
- `frankenphp_queue_depth`:常规排队请求的数量
|
||||
- `frankenphp_total_workers{worker="[worker_name]"}`:worker 的总数。
|
||||
- `frankenphp_busy_workers{worker="[worker_name]"}`:当前正在处理请求的 worker 数量。
|
||||
- `frankenphp_worker_request_time{worker="[worker_name]"}`:所有 worker 处理请求所花费的时间。
|
||||
- `frankenphp_worker_request_count{worker="[worker_name]"}`:所有 worker 处理的请求数量。
|
||||
- `frankenphp_ready_workers{worker="[worker_name]"}`:至少调用过一次 `frankenphp_handle_request` 的 worker 数量。
|
||||
- `frankenphp_worker_crashes{worker="[worker_name]"}`:worker 意外终止的次数。
|
||||
- `frankenphp_worker_restarts{worker="[worker_name]"}`:worker 被故意重启的次数。
|
||||
- `frankenphp_worker_queue_depth{worker="[worker_name]"}`:排队请求的数量。
|
||||
|
||||
对于 worker 指标,`[worker_name]` 占位符被 Caddyfile 中的 worker 名称替换,否则将使用 worker 文件的绝对路径。
|
||||
157
docs/cn/performance.md
Normal file
157
docs/cn/performance.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# 性能
|
||||
|
||||
默认情况下,FrankenPHP 尝试在性能和易用性之间提供良好的折衷。
|
||||
但是,通过使用适当的配置,可以大幅提高性能。
|
||||
|
||||
## 线程和 Worker 数量
|
||||
|
||||
默认情况下,FrankenPHP 启动的线程和 worker(在 worker 模式下)数量是可用 CPU 数量的 2 倍。
|
||||
|
||||
适当的值很大程度上取决于你的应用程序是如何编写的、它做什么以及你的硬件。
|
||||
我们强烈建议更改这些值。为了获得最佳的系统稳定性,建议 `num_threads` x `memory_limit` < `available_memory`。
|
||||
|
||||
要找到正确的值,最好运行模拟真实流量的负载测试。
|
||||
[k6](https://k6.io) 和 [Gatling](https://gatling.io) 是很好的工具。
|
||||
|
||||
要配置线程数,请使用 `php_server` 和 `php` 指令的 `num_threads` 选项。
|
||||
要更改 worker 数量,请使用 `frankenphp` 指令的 `worker` 部分的 `num` 选项。
|
||||
|
||||
### `max_threads`
|
||||
|
||||
虽然准确了解你的流量情况总是更好,但现实应用往往更加
|
||||
不可预测。`max_threads` [配置](config.md#caddyfile-config) 允许 FrankenPHP 在运行时自动生成额外线程,直到指定的限制。
|
||||
`max_threads` 可以帮助你确定需要多少线程来处理你的流量,并可以使服务器对延迟峰值更具弹性。
|
||||
如果设置为 `auto`,限制将基于你的 `php.ini` 中的 `memory_limit` 进行估算。如果无法这样做,
|
||||
`auto` 将默认为 2x `num_threads`。请记住,`auto` 可能会严重低估所需的线程数。
|
||||
`max_threads` 类似于 PHP FPM 的 [pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children)。主要区别是 FrankenPHP 使用线程而不是
|
||||
进程,并根据需要自动在不同的 worker 脚本和"经典模式"之间委派它们。
|
||||
|
||||
## Worker 模式
|
||||
|
||||
启用 [worker 模式](worker.md) 大大提高了性能,
|
||||
但你的应用必须适配以兼容此模式:
|
||||
你需要创建一个 worker 脚本并确保应用不会泄漏内存。
|
||||
|
||||
## 不要使用 musl
|
||||
|
||||
官方 Docker 镜像的 Alpine Linux 变体和我们提供的默认二进制文件使用 [musl libc](https://musl.libc.org)。
|
||||
|
||||
众所周知,当使用这个替代 C 库而不是传统的 GNU 库时,PHP [更慢](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381),
|
||||
特别是在以 ZTS 模式(线程安全)编译时,这是 FrankenPHP 所必需的。在大量线程环境中,差异可能很显著。
|
||||
|
||||
另外,[一些错误只在使用 musl 时发生](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl)。
|
||||
|
||||
在生产环境中,我们建议使用链接到 glibc 的 FrankenPHP。
|
||||
|
||||
这可以通过使用 Debian Docker 镜像(默认)、从我们的 [Releases](https://github.com/php/frankenphp/releases) 下载 -gnu 后缀二进制文件,或通过[从源代码编译 FrankenPHP](compile.md) 来实现。
|
||||
|
||||
或者,我们提供使用 [mimalloc 分配器](https://github.com/microsoft/mimalloc) 编译的静态 musl 二进制文件,这缓解了线程场景中的问题。
|
||||
|
||||
## Go 运行时配置
|
||||
|
||||
FrankenPHP 是用 Go 编写的。
|
||||
|
||||
一般来说,Go 运行时不需要任何特殊配置,但在某些情况下,
|
||||
特定的配置可以提高性能。
|
||||
|
||||
你可能想要将 `GODEBUG` 环境变量设置为 `cgocheck=0`(FrankenPHP Docker 镜像中的默认值)。
|
||||
|
||||
如果你在容器(Docker、Kubernetes、LXC...)中运行 FrankenPHP 并限制容器的可用内存,
|
||||
请将 `GOMEMLIMIT` 环境变量设置为可用内存量。
|
||||
|
||||
有关更多详细信息,[专门针对此主题的 Go 文档页面](https://pkg.go.dev/runtime#hdr-Environment_Variables) 是充分利用运行时的必读内容。
|
||||
|
||||
## `file_server`
|
||||
|
||||
默认情况下,`php_server` 指令自动设置文件服务器来
|
||||
提供存储在根目录中的静态文件(资产)。
|
||||
|
||||
此功能很方便,但有成本。
|
||||
要禁用它,请使用以下配置:
|
||||
|
||||
```caddyfile
|
||||
php_server {
|
||||
file_server off
|
||||
}
|
||||
```
|
||||
|
||||
## `try_files`
|
||||
|
||||
除了静态文件和 PHP 文件外,`php_server` 还会尝试提供你应用程序的索引
|
||||
和目录索引文件(`/path/` -> `/path/index.php`)。如果你不需要目录索引,
|
||||
你可以通过明确定义 `try_files` 来禁用它们,如下所示:
|
||||
|
||||
```caddyfile
|
||||
php_server {
|
||||
try_files {path} index.php
|
||||
root /root/to/your/app # 在这里明确添加根目录允许更好的缓存
|
||||
}
|
||||
```
|
||||
|
||||
这可以显著减少不必要的文件操作数量。
|
||||
|
||||
另一种具有 0 个不必要文件系统操作的方法是改用 `php` 指令并按路径将
|
||||
文件与 PHP 分开。如果你的整个应用程序由一个入口文件提供服务,这种方法效果很好。
|
||||
一个在 `/assets` 文件夹后面提供静态文件的示例[配置](config.md#caddyfile-config)可能如下所示:
|
||||
|
||||
```caddyfile
|
||||
route {
|
||||
@assets {
|
||||
path /assets/*
|
||||
}
|
||||
|
||||
# /assets 后面的所有内容都由文件服务器处理
|
||||
file_server @assets {
|
||||
root /root/to/your/app
|
||||
}
|
||||
|
||||
# 不在 /assets 中的所有内容都由你的索引或 worker PHP 文件处理
|
||||
rewrite index.php
|
||||
php {
|
||||
root /root/to/your/app # 在这里明确添加根目录允许更好的缓存
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 占位符
|
||||
|
||||
你可以在 `root` 和 `env` 指令中使用[占位符](https://caddyserver.com/docs/conventions#placeholders)。
|
||||
但是,这会阻止缓存这些值,并带来显著的性能成本。
|
||||
|
||||
如果可能,请避免在这些指令中使用占位符。
|
||||
|
||||
## `resolve_root_symlink`
|
||||
|
||||
默认情况下,如果文档根目录是符号链接,FrankenPHP 会自动解析它(这对于 PHP 正常工作是必要的)。
|
||||
如果文档根目录不是符号链接,你可以禁用此功能。
|
||||
|
||||
```caddyfile
|
||||
php_server {
|
||||
resolve_root_symlink false
|
||||
}
|
||||
```
|
||||
|
||||
如果 `root` 指令包含[占位符](https://caddyserver.com/docs/conventions#placeholders),这将提高性能。
|
||||
在其他情况下,收益将可以忽略不计。
|
||||
|
||||
## 日志
|
||||
|
||||
日志显然非常有用,但根据定义,
|
||||
它需要 I/O 操作和内存分配,这会大大降低性能。
|
||||
确保你[正确设置日志级别](https://caddyserver.com/docs/caddyfile/options#log),
|
||||
并且只记录必要的内容。
|
||||
|
||||
## PHP 性能
|
||||
|
||||
FrankenPHP 使用官方 PHP 解释器。
|
||||
所有常见的 PHP 相关性能优化都适用于 FrankenPHP。
|
||||
|
||||
特别是:
|
||||
|
||||
- 检查 [OPcache](https://www.php.net/manual/zh/book.opcache.php) 是否已安装、启用并正确配置
|
||||
- 启用 [Composer 自动加载器优化](https://getcomposer.org/doc/articles/autoloader-optimization.md)
|
||||
- 确保 `realpath` 缓存对于你的应用程序需求足够大
|
||||
- 使用[预加载](https://www.php.net/manual/zh/opcache.preloading.php)
|
||||
|
||||
有关更多详细信息,请阅读[专门的 Symfony 文档条目](https://symfony.com/doc/current/performance.html)
|
||||
(即使你不使用 Symfony,大多数提示也很有用)。
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
在本教程中,我们将学习如何使用 Docker Compose 在单个服务器上部署 PHP 应用程序。
|
||||
|
||||
如果您使用的是 Symfony,请阅读 Symfony Docker 项目(使用 FrankenPHP)的 [在生产环境中部署](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md) 文档条目。
|
||||
如果你使用的是 Symfony,请阅读 Symfony Docker 项目(使用 FrankenPHP)的 [在生产环境中部署](https://github.com/dunglas/symfony-docker/blob/main/docs/production.md) 文档条目。
|
||||
|
||||
如果您使用的是 API Platform(同样使用 FrankenPHP),请参阅 [框架的部署文档](https://api-platform.com/docs/deployment/)。
|
||||
如果你使用的是 API Platform(同样使用 FrankenPHP),请参阅 [框架的部署文档](https://api-platform.com/docs/deployment/)。
|
||||
|
||||
## 准备应用
|
||||
|
||||
@@ -13,11 +13,14 @@
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
|
||||
# 请将 "your-domain-name.example.com" 替换为您的域名
|
||||
# 请将 "your-domain-name.example.com" 替换为你的域名
|
||||
ENV SERVER_NAME=your-domain-name.example.com
|
||||
# 如果要禁用 HTTPS,请改用以下值:
|
||||
#ENV SERVER_NAME=:80
|
||||
|
||||
# 如果你的项目不使用 "public" 目录作为 web 根目录,你可以在这里设置:
|
||||
# ENV SERVER_ROOT=web/
|
||||
|
||||
# 启用 PHP 生产配置
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
@@ -30,8 +33,8 @@ COPY . /app/public
|
||||
有关更多详细信息和选项,请参阅 [构建自定义 Docker 镜像](docker.md)。
|
||||
要了解如何自定义配置,请安装 PHP 扩展和 Caddy 模块。
|
||||
|
||||
如果您的项目使用 Composer,
|
||||
请务必将其包含在 Docker 镜像中并安装您的依赖。
|
||||
如果你的项目使用 Composer,
|
||||
请务必将其包含在 Docker 镜像中并安装你的依赖。
|
||||
|
||||
然后,添加一个 `compose.yaml` 文件:
|
||||
|
||||
@@ -63,25 +66,25 @@ volumes:
|
||||
> (使用 FrankenPHP)作为使用多阶段镜像的更高级示例,
|
||||
> Composer、额外的 PHP 扩展等。
|
||||
|
||||
最后,如果您使用 Git,请提交这些文件并推送。
|
||||
最后,如果你使用 Git,请提交这些文件并推送。
|
||||
|
||||
## 准备服务器
|
||||
|
||||
若要在生产环境中部署应用程序,需要一台服务器。
|
||||
在本教程中,我们将使用 DigitalOcean 提供的虚拟机,但任何 Linux 服务器都可以工作。
|
||||
如果您已经有安装了 Docker 的 Linux 服务器,您可以直接跳到 [下一节](#配置域名)。
|
||||
如果你已经有安装了 Docker 的 Linux 服务器,你可以直接跳到 [下一节](#配置域名)。
|
||||
|
||||
否则,请使用 [此会员链接](https://m.do.co/c/5d8aabe3ab80) 获得 200 美元的免费信用额度,创建一个帐户,然后单击“Create a Droplet”。
|
||||
然后,单击“Choose an image”部分下的“Marketplace”选项卡,然后搜索名为“Docker”的应用程序。
|
||||
这将配置已安装最新版本的 Docker 和 Docker Compose 的 Ubuntu 服务器!
|
||||
|
||||
出于测试目的,最便宜的就足够了。
|
||||
对于实际的生产用途,您可能需要在“general purpose”部分中选择一个计划来满足您的需求。
|
||||
对于实际的生产用途,你可能需要在“general purpose”部分中选择一个计划来满足你的需求。
|
||||
|
||||

|
||||
|
||||
您可以保留其他设置的默认值,也可以根据需要进行调整。
|
||||
不要忘记添加您的 SSH 密钥或创建密码,然后点击“完成并创建”按钮。
|
||||
你可以保留其他设置的默认值,也可以根据需要进行调整。
|
||||
不要忘记添加你的 SSH 密钥或创建密码,然后点击“完成并创建”按钮。
|
||||
|
||||
然后,在 Droplet 预配时等待几秒钟。
|
||||
Droplet 准备就绪后,使用 SSH 进行连接:
|
||||
@@ -92,10 +95,10 @@ ssh root@<droplet-ip>
|
||||
|
||||
## 配置域名
|
||||
|
||||
在大多数情况下,您需要将域名与您的网站相关联。
|
||||
如果您还没有域名,则必须通过注册商购买。
|
||||
在大多数情况下,你需要将域名与你的网站相关联。
|
||||
如果你还没有域名,则必须通过注册商购买。
|
||||
|
||||
然后为您的域名创建类型为 `A` 的 DNS 记录,指向服务器的 IP 地址:
|
||||
然后为你的域名创建类型为 `A` 的 DNS 记录,指向服务器的 IP 地址:
|
||||
|
||||
```dns
|
||||
your-domain-name.example.com. IN A 207.154.233.113
|
||||
@@ -111,7 +114,7 @@ DigitalOcean 域服务示例(“Networking” > “Domains”):
|
||||
|
||||
## 部署
|
||||
|
||||
使用 `git clone`、`scp` 或任何其他可能适合您需要的工具在服务器上复制您的项目。
|
||||
使用 `git clone`、`scp` 或任何其他可能适合你需要的工具在服务器上复制你的项目。
|
||||
如果使用 GitHub,则可能需要使用 [部署密钥](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys)。
|
||||
部署密钥也 [由 GitLab 支持](https://docs.gitlab.com/ee/user/project/deploy_keys/)。
|
||||
|
||||
@@ -124,15 +127,15 @@ git clone git@github.com:<username>/<project-name>.git
|
||||
进入包含项目 (`<project-name>`) 的目录,并在生产模式下启动应用:
|
||||
|
||||
```console
|
||||
docker compose up -d --wait
|
||||
docker compose up --wait
|
||||
```
|
||||
|
||||
您的服务器已启动并运行,并且已自动为您生成 HTTPS 证书。
|
||||
你的服务器已启动并运行,并且已自动为你生成 HTTPS 证书。
|
||||
去 `https://your-domain-name.example.com` 享受吧!
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Docker 有一个缓存层,请确保每个部署都有正确的构建,或者使用 --no-cache 选项重新构建项目以避免缓存问题。
|
||||
> Docker 有一个缓存层,请确保每个部署都有正确的构建,或者使用 `--no-cache` 选项重新构建项目以避免缓存问题。
|
||||
|
||||
## 在多个节点上部署
|
||||
|
||||
|
||||
@@ -1,35 +1,63 @@
|
||||
# 创建静态构建
|
||||
|
||||
基于 [static-php-cli](https://github.com/crazywhalecc/static-php-cli) 项目(这个项目支持所有 SAPI,不仅仅是 `cli`),
|
||||
FrankenPHP 已支持创建静态二进制,无需安装本地 PHP。
|
||||
与其使用本地安装的PHP库,
|
||||
由于伟大的 [static-php-cli 项目](https://github.com/crazywhalecc/static-php-cli),创建一个静态或基本静态的 FrankenPHP 构建是可能的(尽管它的名字,这个项目支持所有的 SAPI,而不仅仅是 CLI)。
|
||||
|
||||
使用这种方法,我们可构建一个包含 PHP 解释器、Caddy Web 服务器和 FrankenPHP 的可移植二进制文件!
|
||||
|
||||
完全静态的本地可执行文件不需要任何依赖,并且可以在 [`scratch` Docker 镜像](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch) 上运行。
|
||||
然而,它们无法加载动态 PHP 扩展(例如 Xdebug),并且由于使用了 musl libc,有一些限制。
|
||||
|
||||
大多数静态二进制文件只需要 `glibc` 并且可以加载动态扩展。
|
||||
|
||||
在可能的情况下,我们建议使用基于glibc的、主要是静态构建的版本。
|
||||
|
||||
FrankenPHP 还支持 [将 PHP 应用程序嵌入到静态二进制文件中](embed.md)。
|
||||
|
||||
## Linux
|
||||
|
||||
我们提供了一个 Docker 镜像来构建 Linux 静态二进制文件:
|
||||
|
||||
### 基于musl的完全静态构建
|
||||
|
||||
对于一个在任何Linux发行版上运行且不需要依赖项的完全静态二进制文件,但不支持动态加载扩展:
|
||||
|
||||
```console
|
||||
docker buildx bake --load static-builder
|
||||
docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder
|
||||
docker buildx bake --load static-builder-musl
|
||||
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-musl
|
||||
```
|
||||
|
||||
生成的静态二进制文件名为 `frankenphp`,可在当前目录中找到。
|
||||
为了在高度并发的场景中获得更好的性能,请考虑使用 [mimalloc](https://github.com/microsoft/mimalloc) 分配器。
|
||||
|
||||
如果您想在没有 Docker 的情况下构建静态二进制文件,请查看 macOS 说明,它也适用于 Linux。
|
||||
```console
|
||||
docker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl
|
||||
```
|
||||
|
||||
### 基于glibc的,主要静态构建(支持动态扩展)
|
||||
|
||||
对于一个支持动态加载 PHP 扩展的二进制文件,同时又将所选扩展静态编译:
|
||||
|
||||
```console
|
||||
docker buildx bake --load static-builder-gnu
|
||||
docker cp $(docker create --name static-builder-gnu dunglas/frankenphp:static-builder-gnu):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-gnu
|
||||
```
|
||||
|
||||
该二进制文件支持所有glibc版本2.17及以上,但不支持基于musl的系统(如Alpine Linux)。
|
||||
|
||||
生成的主要是静态的(除了 `glibc`)二进制文件名为 `frankenphp`,并且可以在当前目录中找到。
|
||||
|
||||
如果你想在没有 Docker 的情况下构建静态二进制文件,请查看 macOS 说明,它也适用于 Linux。
|
||||
|
||||
### 自定义扩展
|
||||
|
||||
默认情况下,大多数流行的 PHP 扩展都会被编译。
|
||||
|
||||
若要减小二进制文件的大小并减少攻击面,可以选择使用 `PHP_EXTENSIONS` Docker 参数来自定义构建的扩展。
|
||||
为了减少二进制文件的大小和减少攻击面,您可以选择使用 `PHP_EXTENSIONS` Docker ARG 构建的扩展列表。
|
||||
|
||||
例如,运行以下命令以生成仅包含 `opcache,pdo_sqlite` 扩展的二进制:
|
||||
例如,运行以下命令仅构建 `opcache` 扩展:
|
||||
|
||||
```console
|
||||
docker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder
|
||||
docker buildx bake --load --set static-builder-musl.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder-musl
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -38,9 +66,9 @@ docker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_s
|
||||
```console
|
||||
docker buildx bake \
|
||||
--load \
|
||||
--set static-builder.args.PHP_EXTENSIONS=gd \
|
||||
--set static-builder.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \
|
||||
static-builder
|
||||
--set static-builder-musl.args.PHP_EXTENSIONS=gd \
|
||||
--set static-builder-musl.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \
|
||||
static-builder-musl
|
||||
```
|
||||
|
||||
### 额外的 Caddy 模块
|
||||
@@ -50,8 +78,8 @@ docker buildx bake \
|
||||
```console
|
||||
docker buildx bake \
|
||||
--load \
|
||||
--set static-builder.args.XCADDY_ARGS="--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy" \
|
||||
static-builder
|
||||
--set static-builder-musl.args.XCADDY_ARGS="--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy" \
|
||||
static-builder-musl
|
||||
```
|
||||
|
||||
在本例中,我们为 Caddy 添加了 [Souin](https://souin.io) HTTP 缓存模块,以及 [cbrotli](https://github.com/dunglas/caddy-cbrotli)、[Mercure](https://mercure.rocks) 和 [Vulcain](https://vulcain.rocks) 模块。
|
||||
@@ -68,7 +96,7 @@ docker buildx bake \
|
||||
如果遇到了 GitHub API 速率限制,请在 `GITHUB_TOKEN` 的环境变量中设置 GitHub Personal Access Token:
|
||||
|
||||
```console
|
||||
GITHUB_TOKEN="xxx" docker --load buildx bake static-builder
|
||||
GITHUB_TOKEN="xxx" docker --load buildx bake static-builder-musl
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -77,7 +105,7 @@ GITHUB_TOKEN="xxx" docker --load buildx bake static-builder
|
||||
运行以下脚本以创建适用于 macOS 的静态二进制文件(需要先安装 [Homebrew](https://brew.sh/)):
|
||||
|
||||
```console
|
||||
git clone https://github.com/dunglas/frankenphp
|
||||
git clone https://github.com/php/frankenphp
|
||||
cd frankenphp
|
||||
./build-static.sh
|
||||
```
|
||||
@@ -89,12 +117,45 @@ cd frankenphp
|
||||
以下环境变量可以传递给 `docker build` 和 `build-static.sh`
|
||||
脚本来自定义静态构建:
|
||||
|
||||
* `FRANKENPHP_VERSION`: 要使用的 FrankenPHP 版本
|
||||
* `PHP_VERSION`: 要使用的 PHP 版本
|
||||
* `PHP_EXTENSIONS`: 要构建的 PHP 扩展([支持的扩展列表](https://static-php.dev/zh/guide/extensions.html))
|
||||
* `PHP_EXTENSION_LIBS`: 要构建的额外库,为扩展添加额外的功能
|
||||
* `XCADDY_ARGS`:传递给 [xcaddy](https://github.com/caddyserver/xcaddy) 的参数,例如用于添加额外的 Caddy 模块
|
||||
* `EMBED`: 要嵌入二进制文件的 PHP 应用程序的路径
|
||||
* `CLEAN`: 设置后,libphp 及其所有依赖项都是重新构建的(不使用缓存)
|
||||
* `DEBUG_SYMBOLS`: 设置后,调试符号将被保留在二进制文件内
|
||||
* `RELEASE`: (仅限维护者)设置后,生成的二进制文件将上传到 GitHub 上
|
||||
- `FRANKENPHP_VERSION`: 要使用的 FrankenPHP 版本
|
||||
- `PHP_VERSION`: 要使用的 PHP 版本
|
||||
- `PHP_EXTENSIONS`: 要构建的 PHP 扩展([支持的扩展列表](https://static-php.dev/zh/guide/extensions.html))
|
||||
- `PHP_EXTENSION_LIBS`: 要构建的额外库,为扩展添加额外的功能
|
||||
- `XCADDY_ARGS`:传递给 [xcaddy](https://github.com/caddyserver/xcaddy) 的参数,例如用于添加额外的 Caddy 模块
|
||||
- `EMBED`: 要嵌入二进制文件的 PHP 应用程序的路径
|
||||
- `CLEAN`: 设置后,libphp 及其所有依赖项都是重新构建的(不使用缓存)
|
||||
- `NO_COMPRESS`: 不要使用UPX压缩生成的二进制文件
|
||||
- `DEBUG_SYMBOLS`: 设置后,调试符号将被保留在二进制文件内
|
||||
- `MIMALLOC`: (实验性,仅限Linux) 用[mimalloc](https://github.com/microsoft/mimalloc)替换musl的mallocng,以提高性能。我们仅建议在musl目标构建中使用此选项,对于glibc,建议禁用此选项,并在运行二进制文件时使用[`LD_PRELOAD`](https://microsoft.github.io/mimalloc/overrides.html)。
|
||||
- `RELEASE`: (仅限维护者)设置后,生成的二进制文件将上传到 GitHub 上
|
||||
|
||||
## 扩展
|
||||
|
||||
使用glibc或基于macOS的二进制文件,您可以动态加载PHP扩展。然而,这些扩展必须使用ZTS支持进行编译。
|
||||
由于大多数软件包管理器目前不提供其扩展的 ZTS 版本,因此您必须自己编译它们。
|
||||
|
||||
为此,您可以构建并运行 `static-builder-gnu` Docker 容器,远程进入它,并使用 `./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config` 编译扩展。
|
||||
|
||||
关于 [Xdebug 扩展](https://xdebug.org) 的示例步骤:
|
||||
|
||||
```console
|
||||
docker build -t gnu-ext -f static-builder-gnu.Dockerfile --build-arg FRANKENPHP_VERSION=1.0 .
|
||||
docker create --name static-builder-gnu -it gnu-ext /bin/sh
|
||||
docker start static-builder-gnu
|
||||
docker exec -it static-builder-gnu /bin/sh
|
||||
cd /go/src/app/dist/static-php-cli/buildroot/bin
|
||||
git clone https://github.com/xdebug/xdebug.git && cd xdebug
|
||||
source scl_source enable devtoolset-10
|
||||
../phpize
|
||||
./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config
|
||||
make
|
||||
exit
|
||||
docker cp static-builder-gnu:/go/src/app/dist/static-php-cli/buildroot/bin/xdebug/modules/xdebug.so xdebug-zts.so
|
||||
docker cp static-builder-gnu:/go/src/app/dist/frankenphp-linux-$(uname -m) ./frankenphp
|
||||
docker stop static-builder-gnu
|
||||
docker rm static-builder-gnu
|
||||
docker rmi gnu-ext
|
||||
```
|
||||
|
||||
这将在当前目录中创建 `frankenphp` 和 `xdebug-zts.so`。
|
||||
如果你将 `xdebug-zts.so` 移动到你的扩展目录中,添加 `zend_extension=xdebug-zts.so` 到你的 php.ini 并运行 FrankenPHP,它将加载 Xdebug。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 使用 FrankenPHP Workers
|
||||
|
||||
启动应用程序一次并将其保存在内存中。
|
||||
FrankenPHP 将在几毫秒内处理传入的请求。
|
||||
启动一次应用程序并将其保存在内存中。
|
||||
FrankenPHP 将在几毫秒内处理传入请求。
|
||||
|
||||
## 启动 Worker 脚本
|
||||
|
||||
@@ -17,24 +17,34 @@ docker run \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
### 独立二进制
|
||||
### 独立二进制文件
|
||||
|
||||
使用 `php-server` 命令的 `--worker` 选项, 执行命令使当前目录的内容使用 worker:
|
||||
使用 `php-server` 命令的 `--worker` 选项通过 worker 为当前目录的内容提供服务:
|
||||
|
||||
```console
|
||||
frankenphp php-server --worker /path/to/your/worker/script.php
|
||||
```
|
||||
|
||||
如果你的 PHP 应用程序已[嵌入到二进制文件中](embed.md),你可以在应用程序的根目录中添加自定义的 `Caddyfile`。
|
||||
它将被自动使用。
|
||||
|
||||
还可以使用 `--watch` 选项在[文件更改时重启 worker](config.md#watching-for-file-changes)。
|
||||
如果 `/path/to/your/app/` 目录或子目录中任何以 `.php` 结尾的文件被修改,以下命令将触发重启:
|
||||
|
||||
```console
|
||||
frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to/your/app/**/*.php"
|
||||
```
|
||||
|
||||
## Symfony Runtime
|
||||
|
||||
FrankenPHP 的 worker 模式由 [Symfony Runtime 组件](https://symfony.com/doc/current/components/runtime.html) 支持。
|
||||
要在 worker 中启动任何 Symfony 应用程序,请安装 [PHP Runtime](https://github.com/php-runtime/runtime) 的 FrankenPHP 软件包:
|
||||
FrankenPHP 的 worker 模式由 [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html) 支持。
|
||||
要在 worker 中启动任何 Symfony 应用程序,请安装 [PHP Runtime](https://github.com/php-runtime/runtime) 的 FrankenPHP 包:
|
||||
|
||||
```console
|
||||
composer require runtime/frankenphp-symfony
|
||||
```
|
||||
|
||||
通过定义 `APP_RUNTIME` 环境变量来启动你的应用服务器,以使用 FrankenPHP Symfony Runtime:
|
||||
通过定义 `APP_RUNTIME` 环境变量来使用 FrankenPHP Symfony Runtime 启动你的应用服务器:
|
||||
|
||||
```console
|
||||
docker run \
|
||||
@@ -47,45 +57,50 @@ docker run \
|
||||
|
||||
## Laravel Octane
|
||||
|
||||
请参阅 [文档](laravel.md#laravel-octane)。
|
||||
请参阅[专门的文档](laravel.md#laravel-octane)。
|
||||
|
||||
## 自定义应用程序
|
||||
|
||||
以下示例演示如何在不依赖第三方库的情况下创建自己的 worker 脚本:
|
||||
以下示例展示了如何创建自己的 worker 脚本而不依赖第三方库:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// public/index.php
|
||||
|
||||
// 防止在客户端连接中断时 worker 线程脚本终止
|
||||
// 防止客户端连接中断时 worker 脚本终止
|
||||
ignore_user_abort(true);
|
||||
|
||||
// 启动应用
|
||||
// 启动你的应用程序
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
$myApp = new \App\Kernel();
|
||||
$myApp->boot();
|
||||
|
||||
// 循环外的处理程序以获得更好的性能(减少工作量)
|
||||
// 在循环外的处理器以获得更好的性能(减少工作量)
|
||||
$handler = static function () use ($myApp) {
|
||||
// 收到请求时调用
|
||||
// 超全局变量 php://input
|
||||
// 当收到请求时调用,
|
||||
// 超全局变量、php://input 等都会被重置
|
||||
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
|
||||
};
|
||||
for ($nbRequests = 0, $running = true; isset($_SERVER['MAX_REQUESTS']) && ($nbRequests < ((int)$_SERVER['MAX_REQUESTS'])) && $running; ++$nbRequests) {
|
||||
$running = \frankenphp_handle_request($handler);
|
||||
|
||||
// 发送 HTTP 响应后执行某些操作
|
||||
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
|
||||
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
|
||||
$keepRunning = \frankenphp_handle_request($handler);
|
||||
|
||||
// 在发送 HTTP 响应后做一些事情
|
||||
$myApp->terminate();
|
||||
|
||||
// 调用垃圾回收器以减少在页面生成过程中触发垃圾回收器的几率
|
||||
// 调用垃圾收集器以减少在页面生成过程中触发垃圾收集的可能性
|
||||
gc_collect_cycles();
|
||||
|
||||
if (!$keepRunning) break;
|
||||
}
|
||||
// 结束清理
|
||||
|
||||
// 清理
|
||||
$myApp->shutdown();
|
||||
```
|
||||
|
||||
然后,启动应用并使用 `FRANKENPHP_CONFIG` 环境变量来配置你的 worker:
|
||||
然后,启动你的应用程序并使用 `FRANKENPHP_CONFIG` 环境变量配置你的 worker:
|
||||
|
||||
```console
|
||||
docker run \
|
||||
@@ -95,8 +110,8 @@ docker run \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
默认情况下,每个 CPU 启动一个 worker。
|
||||
您还可以配置要启动的 worker 数:
|
||||
默认情况下,每个 CPU 启动 2 个 worker。
|
||||
你也可以配置要启动的 worker 数量:
|
||||
|
||||
```console
|
||||
docker run \
|
||||
@@ -106,9 +121,59 @@ docker run \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
### 在一定数量的请求后重新启动 Worker
|
||||
### 在处理一定数量的请求后重启 Worker
|
||||
|
||||
由于 PHP 最初不是为长时间运行的进程而设计的,因此仍然有许多库和遗留代码会发生内存泄露。
|
||||
在 worker 模式下使用此类代码的解决方法是在处理一定数量的请求后重新启动 worker 程序脚本:
|
||||
由于 PHP 最初不是为长时间运行的进程而设计的,仍有许多库和传统代码会泄漏内存。
|
||||
在 worker 模式下使用此类代码的一个解决方法是在处理一定数量的请求后重启 worker 脚本:
|
||||
|
||||
前面的 worker 代码段允许通过设置名为 `MAX_REQUESTS` 的环境变量来配置要处理的最大请求数。
|
||||
前面的 worker 代码片段允许通过设置名为 `MAX_REQUESTS` 的环境变量来配置要处理的最大请求数。
|
||||
|
||||
### 手动重启 Workers
|
||||
|
||||
虽然可以在[文件更改时重启 workers](config.md#watching-for-file-changes),但也可以通过 [Caddy admin API](https://caddyserver.com/docs/api) 优雅地重启所有 workers。如果在你的 [Caddyfile](config.md#caddyfile-config) 中启用了 admin,你可以通过简单的 POST 请求 ping 重启端点,如下所示:
|
||||
|
||||
```console
|
||||
curl -X POST http://localhost:2019/frankenphp/workers/restart
|
||||
```
|
||||
|
||||
### Worker 故障
|
||||
|
||||
如果 worker 脚本因非零退出代码而崩溃,FrankenPHP 将使用指数退避策略重启它。
|
||||
如果 worker 脚本保持运行的时间超过上次退避 × 2,
|
||||
它将不会惩罚 worker 脚本并再次重启它。
|
||||
但是,如果 worker 脚本在短时间内继续以非零退出代码失败
|
||||
(例如,脚本中有拼写错误),FrankenPHP 将崩溃并出现错误:`too many consecutive failures`。
|
||||
|
||||
可以在你的 [Caddyfile](config.md#caddyfile-配置) 中使用 `max_consecutive_failures` 选项配置连续失败的次数:
|
||||
|
||||
```caddyfile
|
||||
frankenphp {
|
||||
worker {
|
||||
# ...
|
||||
max_consecutive_failures 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 超全局变量行为
|
||||
|
||||
[PHP 超全局变量](https://www.php.net/manual/zh/language.variables.superglobals.php)(`$_SERVER`、`$_ENV`、`$_GET`...)
|
||||
行为如下:
|
||||
|
||||
- 在第一次调用 `frankenphp_handle_request()` 之前,超全局变量包含绑定到 worker 脚本本身的值
|
||||
- 在调用 `frankenphp_handle_request()` 期间和之后,超全局变量包含从处理的 HTTP 请求生成的值,每次调用 `frankenphp_handle_request()` 都会更改超全局变量的值
|
||||
|
||||
要在回调内访问 worker 脚本的超全局变量,必须复制它们并将副本导入到回调的作用域中:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// 在第一次调用 frankenphp_handle_request() 之前复制 worker 的 $_SERVER 超全局变量
|
||||
$workerServer = $_SERVER;
|
||||
|
||||
$handler = static function () use ($workerServer) {
|
||||
var_dump($_SERVER); // 与请求绑定的 $_SERVER
|
||||
var_dump($workerServer); // worker 脚本的 $_SERVER
|
||||
};
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
69
docs/cn/x-sendfile.md
Normal file
69
docs/cn/x-sendfile.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# 高效服务大型静态文件 (`X-Sendfile`/`X-Accel-Redirect`)
|
||||
|
||||
通常,静态文件可以直接由 Web 服务器提供服务,
|
||||
但有时在发送它们之前需要执行一些 PHP 代码:
|
||||
访问控制、统计、自定义 HTTP 头...
|
||||
|
||||
不幸的是,与直接使用 Web 服务器相比,使用 PHP 服务大型静态文件效率低下
|
||||
(内存过载、性能降低...)。
|
||||
|
||||
FrankenPHP 让你在执行自定义 PHP 代码**之后**将静态文件的发送委托给 Web 服务器。
|
||||
|
||||
为此,你的 PHP 应用程序只需定义一个包含要服务的文件路径的自定义 HTTP 头。FrankenPHP 处理其余部分。
|
||||
|
||||
此功能在 Apache 中称为 **`X-Sendfile`**,在 NGINX 中称为 **`X-Accel-Redirect`**。
|
||||
|
||||
在以下示例中,我们假设项目的文档根目录是 `public/` 目录,
|
||||
并且我们想要使用 PHP 来服务存储在 `public/` 目录外的文件,
|
||||
来自名为 `private-files/` 的目录。
|
||||
|
||||
## 配置
|
||||
|
||||
首先,将以下配置添加到你的 `Caddyfile` 以启用此功能:
|
||||
|
||||
```patch
|
||||
root public/
|
||||
# ...
|
||||
|
||||
+ # Symfony、Laravel 和其他使用 Symfony HttpFoundation 组件的项目需要
|
||||
+ request_header X-Sendfile-Type x-accel-redirect
|
||||
+ request_header X-Accel-Mapping ../private-files=/private-files
|
||||
+
|
||||
+ intercept {
|
||||
+ @accel header X-Accel-Redirect *
|
||||
+ handle_response @accel {
|
||||
+ root private-files/
|
||||
+ rewrite * {resp.header.X-Accel-Redirect}
|
||||
+ method * GET
|
||||
+
|
||||
+ # 删除 PHP 设置的 X-Accel-Redirect 头以提高安全性
|
||||
+ header -X-Accel-Redirect
|
||||
+
|
||||
+ file_server
|
||||
+ }
|
||||
+ }
|
||||
|
||||
php_server
|
||||
```
|
||||
|
||||
## 纯 PHP
|
||||
|
||||
将相对文件路径(从 `private-files/`)设置为 `X-Accel-Redirect` 头的值:
|
||||
|
||||
```php
|
||||
header('X-Accel-Redirect: file.txt');
|
||||
```
|
||||
|
||||
## 使用 Symfony HttpFoundation 组件的项目(Symfony、Laravel、Drupal...)
|
||||
|
||||
Symfony HttpFoundation [原生支持此功能](https://symfony.com/doc/current/components/http_foundation.html#serving-files)。
|
||||
它将自动确定 `X-Accel-Redirect` 头的正确值并将其添加到响应中。
|
||||
|
||||
```php
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
BinaryFileResponse::trustXSendfileTypeHeader();
|
||||
$response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt');
|
||||
|
||||
// ...
|
||||
```
|
||||
@@ -3,12 +3,29 @@
|
||||
This document explains how to create a FrankenPHP binary that will load PHP as a dynamic library.
|
||||
This is the recommended method.
|
||||
|
||||
Alternatively, [static builds](static.md) can also be created.
|
||||
Alternatively, [fully and mostly static builds](static.md) can also be created.
|
||||
|
||||
## Install PHP
|
||||
|
||||
FrankenPHP is compatible with PHP 8.2 and superior.
|
||||
|
||||
### With Homebrew (Linux and Mac)
|
||||
|
||||
The easiest way to install a version of libphp compatible with FrankenPHP is to use the ZTS packages provided by [Homebrew PHP](https://github.com/shivammathur/homebrew-php).
|
||||
|
||||
First, if not already done, install [Homebrew](https://brew.sh).
|
||||
|
||||
Then, install the ZTS variant of PHP, Brotli (optional, for compression support) and watcher (optional, for file change detection):
|
||||
|
||||
```console
|
||||
brew install shivammathur/php/php-zts brotli watcher
|
||||
brew link --overwrite --force shivammathur/php/php-zts
|
||||
```
|
||||
|
||||
### By Compiling PHP
|
||||
|
||||
Alternatively, you can compile PHP from sources with the options needed by FrankenPHP by following these steps.
|
||||
|
||||
First, [get the PHP sources](https://www.php.net/downloads.php) and extract them:
|
||||
|
||||
```console
|
||||
@@ -19,7 +36,7 @@ cd php-*/
|
||||
Then, run the `configure` script with the options needed for your platform.
|
||||
The following `./configure` flags are mandatory, but you can add others, for example, to compile extensions or additional features.
|
||||
|
||||
### Linux
|
||||
#### Linux
|
||||
|
||||
```console
|
||||
./configure \
|
||||
@@ -29,13 +46,12 @@ The following `./configure` flags are mandatory, but you can add others, for exa
|
||||
--enable-zend-max-execution-timers
|
||||
```
|
||||
|
||||
### Mac
|
||||
#### Mac
|
||||
|
||||
Use the [Homebrew](https://brew.sh/) package manager to install
|
||||
`libiconv`, `bison`, `re2c` and `pkg-config`:
|
||||
Use the [Homebrew](https://brew.sh/) package manager to install the required and optional dependencies:
|
||||
|
||||
```console
|
||||
brew install libiconv bison brotli re2c pkg-config
|
||||
brew install libiconv bison brotli re2c pkg-config watcher
|
||||
echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
|
||||
```
|
||||
|
||||
@@ -43,16 +59,13 @@ Then run the configure script:
|
||||
|
||||
```console
|
||||
./configure \
|
||||
--enable-embed=static \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--disable-opcache-jit \
|
||||
--enable-static \
|
||||
--enable-shared=no \
|
||||
--with-iconv=/opt/homebrew/opt/libiconv/
|
||||
```
|
||||
|
||||
## Compile PHP
|
||||
#### Compile PHP
|
||||
|
||||
Finally, compile and install PHP:
|
||||
|
||||
@@ -66,24 +79,20 @@ sudo make install
|
||||
Some FrankenPHP features depend on optional system dependencies that must be installed.
|
||||
Alternatively, these features can be disabled by passing build tags to the Go compiler.
|
||||
|
||||
| Feature | Dependency | Build tag to disable it |
|
||||
|--------------------------------|-----------------------------------------------------------------------|-------------------------|
|
||||
| Brotli compression | [Brotli](https://github.com/google/brotli) | nobrotli |
|
||||
| Restart workers on file change | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
|
||||
| Feature | Dependency | Build tag to disable it |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------- |
|
||||
| Brotli compression | [Brotli](https://github.com/google/brotli) | nobrotli |
|
||||
| Restart workers on file change | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
|
||||
| [Mercure](mercure.md) | [Mercure Go library](https://pkg.go.dev/github.com/dunglas/mercure) (automatically installed, AGPL licensed) | nomercure |
|
||||
|
||||
## Compile the Go App
|
||||
|
||||
You can now build the final binary:
|
||||
|
||||
```console
|
||||
curl -L https://github.com/dunglas/frankenphp/archive/refs/heads/main.tar.gz | tar xz
|
||||
cd frankenphp-main/caddy/frankenphp
|
||||
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build -tags=nobadger,nomysql,nopgx
|
||||
```
|
||||
You can now build the final binary.
|
||||
|
||||
### Using xcaddy
|
||||
|
||||
Alternatively, use [xcaddy](https://github.com/caddyserver/xcaddy) to compile FrankenPHP with [custom Caddy modules](https://caddyserver.com/docs/modules/):
|
||||
The recommended way is to use [xcaddy](https://github.com/caddyserver/xcaddy) to compile FrankenPHP.
|
||||
`xcaddy` also allows to easily add [custom Caddy modules](https://caddyserver.com/docs/modules/) and FrankenPHP extensions:
|
||||
|
||||
```console
|
||||
CGO_ENABLED=1 \
|
||||
@@ -94,8 +103,13 @@ xcaddy build \
|
||||
--output frankenphp \
|
||||
--with github.com/dunglas/frankenphp/caddy \
|
||||
--with github.com/dunglas/mercure/caddy \
|
||||
--with github.com/dunglas/vulcain/caddy
|
||||
# Add extra Caddy modules here
|
||||
--with github.com/dunglas/vulcain/caddy \
|
||||
--with github.com/dunglas/caddy-cbrotli
|
||||
# Add extra Caddy modules and FrankenPHP extensions here
|
||||
# optionally, if you would like to compile from your frankenphp sources:
|
||||
# --with github.com/dunglas/frankenphp=$(pwd) \
|
||||
# --with github.com/dunglas/frankenphp/caddy=$(pwd)/caddy
|
||||
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
@@ -107,3 +121,13 @@ xcaddy build \
|
||||
> To do so, change the `XCADDY_GO_BUILD_FLAGS` environment variable to something like
|
||||
> `XCADDY_GO_BUILD_FLAGS=$'-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'`
|
||||
> (change the stack size value according to your app needs).
|
||||
|
||||
### Without xcaddy
|
||||
|
||||
Alternatively, it's possible to compile FrankenPHP without `xcaddy` by using the `go` command directly:
|
||||
|
||||
```console
|
||||
curl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz
|
||||
cd frankenphp-main/caddy/frankenphp
|
||||
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build -tags=nobadger,nomysql,nopgx
|
||||
```
|
||||
|
||||
195
docs/config.md
195
docs/config.md
@@ -1,17 +1,42 @@
|
||||
# Configuration
|
||||
|
||||
FrankenPHP, Caddy as well as the Mercure and Vulcain modules can be configured using [the formats supported by Caddy](https://caddyserver.com/docs/getting-started#your-first-config).
|
||||
FrankenPHP, Caddy as well as the [Mercure](mercure.md) and [Vulcain](https://vulcain.rocks) modules can be configured using [the formats supported by Caddy](https://caddyserver.com/docs/getting-started#your-first-config).
|
||||
|
||||
In [the Docker images](docker.md), the `Caddyfile` is located at `/etc/caddy/Caddyfile`.
|
||||
The static binary will look for the `Caddyfile` in the directory in which it is started.
|
||||
The most common format is the `Caddyfile`, which is a simple, human-readable text format.
|
||||
By default, FrankenPHP will look for a `Caddyfile` in the current directory.
|
||||
You can specify a custom path with the `-c` or `--config` option.
|
||||
|
||||
A minimal `Caddyfile` to serve a PHP application is shown below:
|
||||
|
||||
```caddyfile
|
||||
# The hostname to respond to
|
||||
localhost
|
||||
|
||||
# Optionaly, the directory to serve files from, otherwise defaults to the current directory
|
||||
#root public/
|
||||
php_server
|
||||
```
|
||||
|
||||
A more advanced `Caddyfile` enabling more features and providing convenient environment variables is provided [in the FrankenPHP repository](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile),
|
||||
and with Docker images.
|
||||
|
||||
PHP itself can be configured [using a `php.ini` file](https://www.php.net/manual/en/configuration.file.php).
|
||||
|
||||
By default, PHP supplied with Docker images and the one included in the static binary will look for a `php.ini` file in the directory where FrankenPHP is started and in `/usr/local/etc/php/`. They will also load all files ending in `.ini` from `/usr/local/etc/php/conf.d/`.
|
||||
Depending on your installation method, FrankenPHP and the PHP interpreter will look for configuration files in locations described below.
|
||||
|
||||
No `php.ini` file is present by default, you should copy an official template provided by the PHP project.
|
||||
## Docker
|
||||
|
||||
On Docker, the templates are provided in the images:
|
||||
FrankenPHP:
|
||||
|
||||
- `/etc/frankenphp/Caddyfile`: the main configuration file
|
||||
- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: additional configuration files that are loaded automatically
|
||||
|
||||
PHP:
|
||||
|
||||
- `php.ini`: `/usr/local/etc/php/php.ini` (no `php.ini` is provided by default)
|
||||
- additional configuration files: `/usr/local/etc/php/conf.d/*.ini`
|
||||
- PHP extensions: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`
|
||||
- You should copy an official template provided by the PHP project:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
@@ -23,20 +48,38 @@ RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
|
||||
RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
|
||||
```
|
||||
|
||||
If you don't use Docker, copy one of `php.ini-production` or `php.ini-development` provided [in the PHP sources](https://github.com/php/php-src/).
|
||||
## RPM and Debian packages
|
||||
|
||||
FrankenPHP:
|
||||
|
||||
- `/etc/frankenphp/Caddyfile`: the main configuration file
|
||||
- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: additional configuration files that are loaded automatically
|
||||
|
||||
PHP:
|
||||
|
||||
- `php.ini`: `/etc/php-zts/php.ini` (a `php.ini` file with production presets is provided by default)
|
||||
- additional configuration files: `/etc/php-zts/conf.d/*.ini`
|
||||
|
||||
## Static binary
|
||||
|
||||
FrankenPHP:
|
||||
|
||||
- In the current working directory: `Caddyfile`
|
||||
|
||||
PHP:
|
||||
|
||||
- `php.ini`: The directory in which `frankenphp run` or `frankenphp php-server` is executed, then `/etc/frankenphp/php.ini`
|
||||
- additional configuration files: `/etc/frankenphp/php.d/*.ini`
|
||||
- PHP extensions: cannot be loaded, bundle them in the binary itself
|
||||
- copy one of `php.ini-production` or `php.ini-development` provided [in the PHP sources](https://github.com/php/php-src/).
|
||||
|
||||
## Caddyfile Config
|
||||
|
||||
To register the FrankenPHP executor, the `frankenphp` [global option](https://caddyserver.com/docs/caddyfile/concepts#global-options) must be set, then the `php_server` or the `php` [HTTP directives](https://caddyserver.com/docs/caddyfile/concepts#directives) may be used within the site blocks to serve your PHP app.
|
||||
The `php_server` or the `php` [HTTP directives](https://caddyserver.com/docs/caddyfile/concepts#directives) may be used within the site blocks to serve your PHP app.
|
||||
|
||||
Minimal example:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
# Enable FrankenPHP
|
||||
frankenphp
|
||||
}
|
||||
|
||||
localhost {
|
||||
# Enable compression (optional)
|
||||
encode zstd br gzip
|
||||
@@ -45,19 +88,22 @@ localhost {
|
||||
}
|
||||
```
|
||||
|
||||
Optionally, the number of threads to create and [worker scripts](worker.md) to start with the server can be specified under the global option.
|
||||
You can also explicitly configure FrankenPHP using the [global option](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp`:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
num_threads <num_threads> # Sets the number of PHP threads to start. Default: 2x the number of available CPUs.
|
||||
max_threads <num_threads> # Limits the number of additional PHP threads that can be started at runtime. Default: num_threads. Can be set to 'auto'.
|
||||
max_wait_time <duration> # Sets the maximum time a request may wait for a free PHP thread before timing out. Default: disabled.
|
||||
php_ini <key> <value> # Set a php.ini directive. Can be used several times to set multiple directives.
|
||||
worker {
|
||||
file <path> # Sets the path to the worker script.
|
||||
num <num> # Sets the number of PHP threads to start, defaults to 2x the number of available CPUs.
|
||||
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
watch <path> # Sets the path to watch for file changes. Can be specified more than once for multiple paths.
|
||||
name <name> # Sets the name of the worker, used in logs and metrics. Default: absolute path of worker file
|
||||
max_consecutive_failures <num> # Sets the maximum number of consecutive failures before the worker is considered unhealthy, -1 means the worker will always restart. Default: 6.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,28 +126,29 @@ Alternatively, you may use the one-line short form of the `worker` option:
|
||||
You can also define multiple workers if you serve multiple apps on the same server:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker /path/to/app/public/index.php <num>
|
||||
worker /path/to/other/public/index.php <num>
|
||||
app.example.com {
|
||||
root /path/to/app/public
|
||||
php_server {
|
||||
root /path/to/app/public # allows for better caching
|
||||
worker index.php <num>
|
||||
}
|
||||
}
|
||||
|
||||
app.example.com {
|
||||
root * /path/to/app/public
|
||||
php_server
|
||||
}
|
||||
|
||||
other.example.com {
|
||||
root * /path/to/other/public
|
||||
php_server
|
||||
root /path/to/other/public
|
||||
php_server {
|
||||
root /path/to/other/public
|
||||
worker index.php <num>
|
||||
}
|
||||
}
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
Using the `php_server` directive is generally what you need,
|
||||
but if you need full control, you can use the lower level `php` directive:
|
||||
but if you need full control, you can use the lower-level `php` directive.
|
||||
The `php` directive passes all input to PHP, instead of first checking whether
|
||||
it's a PHP file or not. Read more about it in the [performance page](performance.md#try_files).
|
||||
|
||||
Using the `php_server` directive is equivalent to this configuration:
|
||||
|
||||
@@ -135,6 +182,15 @@ php_server [<matcher>] {
|
||||
resolve_root_symlink false # Disables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists (enabled by default).
|
||||
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
|
||||
file_server off # Disables the built-in file_server directive.
|
||||
worker { # Creates a worker specific to this server. Can be specified more than once for multiple workers.
|
||||
file <path> # Sets the path to the worker script, can be relative to the php_server root
|
||||
num <num> # Sets the number of PHP threads to start, defaults to 2x the number of available
|
||||
name <name> # Sets the name for the worker, used in logs and metrics. Default: absolute path of worker file. Always starts with m# when defined in a php_server block.
|
||||
watch <path> # Sets the path to watch for file changes. Can be specified more than once for multiple paths.
|
||||
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables. Environment variables for this worker are also inherited from the php_server parent, but can be overwritten here.
|
||||
match <path> # match the worker to a path pattern. Overrides try_files and can only be used in the php_server directive.
|
||||
}
|
||||
worker <other_file> <num> # Can also use the short form like in the global frankenphp block.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -176,48 +232,44 @@ where the FrankenPHP process was started. You can instead also specify one or mo
|
||||
}
|
||||
```
|
||||
|
||||
* The `**` pattern signifies recursive watching
|
||||
* Directories can also be relative (to where the FrankenPHP process is started from)
|
||||
* If you have multiple workers defined, all of them will be restarted when a file changes
|
||||
* Be wary about watching files that are created at runtime (like logs) since they might cause unwanted worker restarts.
|
||||
- The `**` pattern signifies recursive watching
|
||||
- Directories can also be relative (to where the FrankenPHP process is started from)
|
||||
- If you have multiple workers defined, all of them will be restarted when a file changes
|
||||
- Be wary about watching files that are created at runtime (like logs) since they might cause unwanted worker restarts.
|
||||
|
||||
The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher).
|
||||
|
||||
### Full Duplex (HTTP/1)
|
||||
## Matching the worker to a path
|
||||
|
||||
When using HTTP/1.x, it may be desirable to enable full-duplex mode to allow writing a response before the entire body
|
||||
has been read. (for example: WebSocket, Server-Sent Events, etc.)
|
||||
In traditional PHP applications, scripts are always placed in the public directory.
|
||||
This is also true for worker scripts, which are treated like any other PHP script.
|
||||
If you want to instead put the worker script outside the public directory, you can do so via the `match` directive.
|
||||
|
||||
This is an opt-in configuration that needs to be added to the global options in the `Caddyfile`:
|
||||
The `match` directive is an optimized alternative to `try_files` only available inside `php_server` and `php`.
|
||||
The following example will always serve a file in the public directory if present
|
||||
and otherwise forward the request to the worker matching the path pattern.
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
servers {
|
||||
enable_full_duplex
|
||||
}
|
||||
frankenphp {
|
||||
php_server {
|
||||
worker {
|
||||
file /path/to/worker.php # file can be outside of public path
|
||||
match /api/* # all requests starting with /api/ will be handled by this worker
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Enabling this option may cause old HTTP/1.x clients that don't support full-duplex to deadlock.
|
||||
> This can also be configured using the `CADDY_GLOBAL_OPTIONS` environment config:
|
||||
|
||||
```sh
|
||||
CADDY_GLOBAL_OPTIONS="servers {
|
||||
enable_full_duplex
|
||||
}"
|
||||
```
|
||||
|
||||
You can find more information about this setting in the [Caddy documentation](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The following environment variables can be used to inject Caddy directives in the `Caddyfile` without modifying it:
|
||||
|
||||
* `SERVER_NAME`: change [the addresses on which to listen](https://caddyserver.com/docs/caddyfile/concepts#addresses), the provided hostnames will also be used for the generated TLS certificate
|
||||
* `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options)
|
||||
* `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive
|
||||
- `SERVER_NAME`: change [the addresses on which to listen](https://caddyserver.com/docs/caddyfile/concepts#addresses), the provided hostnames will also be used for the generated TLS certificate
|
||||
- `SERVER_ROOT`: change the root directory of the site, defaults to `public/`
|
||||
- `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options)
|
||||
- `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive
|
||||
|
||||
As for FPM and CLI SAPIs, environment variables are exposed by default in the `$_SERVER` superglobal.
|
||||
|
||||
@@ -246,6 +298,43 @@ You can also change the PHP configuration using the `php_ini` directive in the `
|
||||
}
|
||||
```
|
||||
|
||||
### Disabling HTTPS
|
||||
|
||||
By default, FrankenPHP will automatically enable HTTPS using for all the hostnames, including `localhost`.
|
||||
If you want to disable HTTPS (for example in a development environment), you can set the `SERVER_NAME` environment variable to `http://` or `:80`:
|
||||
|
||||
Alternatively, you can use all other methods described in the [Caddy documentation](https://caddyserver.com/docs/automatic-https#activation).
|
||||
|
||||
If you want to use HTTPS with the `127.0.0.1` IP address instead of the `localhost` hostname, please read the [known issues](known-issues.md#using-https127001-with-docker) section.
|
||||
|
||||
### Full Duplex (HTTP/1)
|
||||
|
||||
When using HTTP/1.x, it may be desirable to enable full-duplex mode to allow writing a response before the entire body
|
||||
has been read. (for example: [Mercure](mercure.md), WebSocket, Server-Sent Events, etc.)
|
||||
|
||||
This is an opt-in configuration that needs to be added to the global options in the `Caddyfile`:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
servers {
|
||||
enable_full_duplex
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Enabling this option may cause old HTTP/1.x clients that don't support full-duplex to deadlock.
|
||||
> This can also be configured using the `CADDY_GLOBAL_OPTIONS` environment config:
|
||||
|
||||
```sh
|
||||
CADDY_GLOBAL_OPTIONS="servers {
|
||||
enable_full_duplex
|
||||
}"
|
||||
```
|
||||
|
||||
You can find more information about this setting in the [Caddy documentation](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).
|
||||
|
||||
## Enable the Debug Mode
|
||||
|
||||
When using the Docker image, set the `CADDY_GLOBAL_OPTIONS` environment variable to `debug` to enable the debug mode:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 87 KiB |
@@ -1,13 +1,15 @@
|
||||
# Building Custom Docker Image
|
||||
|
||||
[FrankenPHP Docker images](https://hub.docker.com/r/dunglas/frankenphp) are based on [official PHP images](https://hub.docker.com/_/php/). Debian and Alpine Linux variants are provided for popular architectures. Debian variants are recommended.
|
||||
[FrankenPHP Docker images](https://hub.docker.com/r/dunglas/frankenphp) are based on [official PHP images](https://hub.docker.com/_/php/).
|
||||
Debian and Alpine Linux variants are provided for popular architectures.
|
||||
Debian variants are recommended.
|
||||
|
||||
Variants for PHP 8.2, 8.3 and 8.4 are provided.
|
||||
Variants for PHP 8.2, 8.3, 8.4 and 8.5 are provided.
|
||||
|
||||
The tags follow this pattern: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`
|
||||
|
||||
* `<frankenphp-version>` and `<php-version>` are version numbers of FrankenPHP and PHP respectively, ranging from major (e.g. `1`), minor (e.g. `1.2`) to patch versions (e.g. `1.2.3`).
|
||||
* `<os>` is either `bookworm` (for Debian Bookworm) or `alpine` (for the latest stable version of Alpine).
|
||||
- `<frankenphp-version>` and `<php-version>` are version numbers of FrankenPHP and PHP respectively, ranging from major (e.g. `1`), minor (e.g. `1.2`) to patch versions (e.g. `1.2.3`).
|
||||
- `<os>` is either `trixie` (for Debian Trixie), `bookworm` (for Debian Bookworm), or `alpine` (for the latest stable version of Alpine).
|
||||
|
||||
[Browse tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
|
||||
|
||||
@@ -28,6 +30,11 @@ docker build -t my-php-app .
|
||||
docker run -it --rm --name my-running-app my-php-app
|
||||
```
|
||||
|
||||
## How to Tweak the Configuration
|
||||
|
||||
For convenience, [a default `Caddyfile`](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile) containing
|
||||
useful environment variables is provided in the image.
|
||||
|
||||
## How to Install More PHP Extensions
|
||||
|
||||
The [`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer) script is provided in the base image.
|
||||
@@ -156,8 +163,8 @@ RUN \
|
||||
useradd ${USER}; \
|
||||
# Add additional capability to bind to port 80 and 443
|
||||
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
|
||||
# Give write access to /data/caddy and /config/caddy
|
||||
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy
|
||||
# Give write access to /config/caddy and /data/caddy
|
||||
chown -R ${USER}:${USER} /config/caddy /data/caddy
|
||||
|
||||
USER ${USER}
|
||||
```
|
||||
@@ -180,8 +187,8 @@ RUN \
|
||||
useradd ${USER}; \
|
||||
# Remove default capability
|
||||
setcap -r /usr/local/bin/frankenphp; \
|
||||
# Give write access to /data/caddy and /config/caddy
|
||||
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy
|
||||
# Give write access to /config/caddy and /data/caddy
|
||||
chown -R ${USER}:${USER} /config/caddy /data/caddy
|
||||
|
||||
USER ${USER}
|
||||
```
|
||||
@@ -193,8 +200,8 @@ Example: `:8000`
|
||||
|
||||
The Docker images are built:
|
||||
|
||||
* when a new release is tagged
|
||||
* daily at 4 am UTC, if new versions of the official PHP images are available
|
||||
- when a new release is tagged
|
||||
- daily at 4 am UTC, if new versions of the official PHP images are available
|
||||
|
||||
## Development Versions
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ Before creating the self-contained binary be sure that your app is ready for emb
|
||||
|
||||
For instance, you likely want to:
|
||||
|
||||
* Install the production dependencies of the app
|
||||
* Dump the autoloader
|
||||
* Enable the production mode of your application (if any)
|
||||
* Strip unneeded files such as `.git` or tests to reduce the size of your final binary
|
||||
- Install the production dependencies of the app
|
||||
- Dump the autoloader
|
||||
- Enable the production mode of your application (if any)
|
||||
- Strip unneeded files such as `.git` or tests to reduce the size of your final binary
|
||||
|
||||
For instance, for a Symfony app, you can use the following commands:
|
||||
|
||||
@@ -53,34 +53,35 @@ The easiest way to create a Linux binary is to use the Docker-based builder we p
|
||||
|
||||
1. Create a file named `static-build.Dockerfile` in the repository of your app:
|
||||
|
||||
```dockerfile
|
||||
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
|
||||
```dockerfile
|
||||
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
|
||||
# If you intend to run the binary on musl-libc systems, use static-builder-musl instead
|
||||
|
||||
# Copy your app
|
||||
WORKDIR /go/src/app/dist/app
|
||||
COPY . .
|
||||
# Copy your app
|
||||
WORKDIR /go/src/app/dist/app
|
||||
COPY . .
|
||||
|
||||
# Build the static binary
|
||||
WORKDIR /go/src/app/
|
||||
RUN EMBED=dist/app/ ./build-static.sh
|
||||
```
|
||||
# Build the static binary
|
||||
WORKDIR /go/src/app/
|
||||
RUN EMBED=dist/app/ ./build-static.sh
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Some `.dockerignore` files (e.g. default [Symfony Docker `.dockerignore`](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))
|
||||
> will ignore the `vendor/` directory and `.env` files. Be sure to adjust or remove the `.dockerignore` file before the build.
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Some `.dockerignore` files (e.g. default [Symfony Docker `.dockerignore`](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))
|
||||
> will ignore the `vendor/` directory and `.env` files. Be sure to adjust or remove the `.dockerignore` file before the build.
|
||||
|
||||
2. Build:
|
||||
|
||||
```console
|
||||
docker build -t static-app -f static-build.Dockerfile .
|
||||
```
|
||||
```console
|
||||
docker build -t static-app -f static-build.Dockerfile .
|
||||
```
|
||||
|
||||
3. Extract the binary:
|
||||
|
||||
```console
|
||||
docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp
|
||||
```
|
||||
```console
|
||||
docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp
|
||||
```
|
||||
|
||||
The resulting binary is the file named `my-app` in the current directory.
|
||||
|
||||
@@ -89,7 +90,7 @@ The resulting binary is the file named `my-app` in the current directory.
|
||||
If you don't want to use Docker, or want to build a macOS binary, use the shell script we provide:
|
||||
|
||||
```console
|
||||
git clone https://github.com/dunglas/frankenphp
|
||||
git clone https://github.com/php/frankenphp
|
||||
cd frankenphp
|
||||
EMBED=/path/to/your/app ./build-static.sh
|
||||
```
|
||||
|
||||
893
docs/extensions.md
Normal file
893
docs/extensions.md
Normal file
@@ -0,0 +1,893 @@
|
||||
# Writing PHP Extensions in Go
|
||||
|
||||
With FrankenPHP, you can **write PHP extensions in Go**, which allows you to create **high-performance native functions** that can be called directly from PHP. Your applications can leverage any existing or new Go library, as well as the infamous concurrency model of **goroutines right from your PHP code**.
|
||||
|
||||
Writing PHP extensions is typically done in C, but it's also possible to write them in other languages with a bit of extra work. PHP extensions allow you to leverage the power of low-level languages to extend PHP's functionalities, for example, by adding native functions or optimizing specific operations.
|
||||
|
||||
Thanks to Caddy modules, you can write PHP extensions in Go and integrate them very quickly into FrankenPHP.
|
||||
|
||||
## Two Approaches
|
||||
|
||||
FrankenPHP provides two ways to create PHP extensions in Go:
|
||||
|
||||
1. **Using the Extension Generator** - The recommended approach that generates all necessary boilerplate for most use cases, allowing you to focus on writing your Go code
|
||||
2. **Manual Implementation** - Full control over the extension structure for advanced use cases
|
||||
|
||||
We'll start with the generator approach as it's the easiest way to get started, then show the manual implementation for those who need complete control.
|
||||
|
||||
## Using the Extension Generator
|
||||
|
||||
FrankenPHP is bundled with a tool that allows you **to create a PHP extension** only using Go. **No need to write C code** or use CGO directly: FrankenPHP also includes a **public types API** to help you write your extensions in Go without having to worry about **the type juggling between PHP/C and Go**.
|
||||
|
||||
> [!TIP]
|
||||
> If you want to understand how extensions can be written in Go from scratch, you can read the manual implementation section below demonstrating how to write a PHP extension in Go without using the generator.
|
||||
|
||||
Keep in mind that this tool is **not a full-fledged extension generator**. It is meant to help you write simple extensions in Go, but it does not provide the most advanced features of PHP extensions. If you need to write a more **complex and optimized** extension, you may need to write some C code or use CGO directly.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
As covered in the manual implementation section below as well, you need to [get the PHP sources](https://www.php.net/downloads.php) and create a new Go module.
|
||||
|
||||
#### Create a New Module and Get PHP Sources
|
||||
|
||||
The first step to writing a PHP extension in Go is to create a new Go module. You can use the following command for this:
|
||||
|
||||
```console
|
||||
go mod init example.com/example
|
||||
```
|
||||
|
||||
The second step is to [get the PHP sources](https://www.php.net/downloads.php) for the next steps. Once you have them, decompress them into the directory of your choice, not inside your Go module:
|
||||
|
||||
```console
|
||||
tar xf php-*
|
||||
```
|
||||
|
||||
### Writing the Extension
|
||||
|
||||
Everything is now setup to write your native function in Go. Create a new file named `stringext.go`. Our first function will take a string as an argument, the number of times to repeat it, a boolean to indicate whether to reverse the string, and return the resulting string. This should look like this:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
|
||||
func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(s))
|
||||
|
||||
result := strings.Repeat(str, int(count))
|
||||
if reverse {
|
||||
runes := []rune(result)
|
||||
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
|
||||
runes[i], runes[j] = runes[j], runes[i]
|
||||
}
|
||||
result = string(runes)
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(result, false)
|
||||
}
|
||||
```
|
||||
|
||||
There are two important things to note here:
|
||||
|
||||
- A directive comment `//export_php:function` defines the function signature in PHP. This is how the generator knows how to generate the PHP function with the right parameters and return type;
|
||||
- The function must return an `unsafe.Pointer`. FrankenPHP provides an API to help you with type juggling between C and Go.
|
||||
|
||||
While the first point speaks for itself, the second may be harder to apprehend. Let's take a deeper dive to type juggling in the next section.
|
||||
|
||||
### Type Juggling
|
||||
|
||||
While some variable types have the same memory representation between C/PHP and Go, some types require more logic to be directly used. This is maybe the hardest part when it comes to writing extensions because it requires understanding internals of the Zend Engine and how variables are stored internally in PHP.
|
||||
This table summarizes what you need to know:
|
||||
|
||||
| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support |
|
||||
|--------------------|-------------------------------|-------------------|-----------------------------------|------------------------------------|-----------------------|
|
||||
| `int` | `int64` | ✅ | - | - | ✅ |
|
||||
| `?int` | `*int64` | ✅ | - | - | ✅ |
|
||||
| `float` | `float64` | ✅ | - | - | ✅ |
|
||||
| `?float` | `*float64` | ✅ | - | - | ✅ |
|
||||
| `bool` | `bool` | ✅ | - | - | ✅ |
|
||||
| `?bool` | `*bool` | ✅ | - | - | ✅ |
|
||||
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
|
||||
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
|
||||
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
|
||||
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
|
||||
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
|
||||
| `callable` | `*C.zval` | ❌ | - | frankenphp.CallPHPCallable() | ❌ |
|
||||
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> This table is not exhaustive yet and will be completed as the FrankenPHP types API gets more complete.
|
||||
>
|
||||
> For class methods specifically, primitive types and arrays are currently supported. Objects cannot be used as method parameters or return types yet.
|
||||
|
||||
If you refer to the code snippet of the previous section, you can see that helpers are used to convert the first parameter and the return value. The second and third parameter of our `repeat_this()` function don't need to be converted as memory representation of the underlying types are the same for both C and Go.
|
||||
|
||||
#### Working with Arrays
|
||||
|
||||
FrankenPHP provides native support for PHP arrays through `frankenphp.AssociativeArray` or direct conversion to a map or slice.
|
||||
|
||||
`AssociativeArray` represents a [hash map](https://en.wikipedia.org/wiki/Hash_table) composed of a `Map: map[string]any`field and an optional `Order: []string` field (unlike PHP "associative arrays", Go maps aren't ordered).
|
||||
|
||||
If order or association are not needed, it's also possible to directly convert to a slice `[]any` or unordered map `map[string]any`.
|
||||
|
||||
**Creating and manipulating arrays in Go:**
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
// export_php:function process_data_ordered(array $input): array
|
||||
func process_data_ordered_map(arr *C.zend_array) unsafe.Pointer {
|
||||
// Convert PHP associative array to Go while keeping the order
|
||||
associativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
// loop over the entries in order
|
||||
for _, key := range associativeArray.Order {
|
||||
value, _ = associativeArray.Map[key]
|
||||
// do something with key and value
|
||||
}
|
||||
|
||||
// return an ordered array
|
||||
// if 'Order' is not empty, only the key-value pairs in 'Order' will be respected
|
||||
return frankenphp.PHPAssociativeArray[string](frankenphp.AssociativeArray[string]{
|
||||
Map: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
Order: []string{"key1", "key2"},
|
||||
})
|
||||
}
|
||||
|
||||
// export_php:function process_data_unordered(array $input): array
|
||||
func process_data_unordered_map(arr *C.zend_array) unsafe.Pointer {
|
||||
// Convert PHP associative array to a Go map without keeping the order
|
||||
// ignoring the order will be more performant
|
||||
goMap, err := frankenphp.GoMap[any](unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
// loop over the entries in no specific order
|
||||
for key, value := range goMap {
|
||||
// do something with key and value
|
||||
}
|
||||
|
||||
// return an unordered array
|
||||
return frankenphp.PHPMap(map[string]string {
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
})
|
||||
}
|
||||
|
||||
// export_php:function process_data_packed(array $input): array
|
||||
func process_data_packed(arr *C.zend_array) unsafe.Pointer {
|
||||
// Convert PHP packed array to Go
|
||||
goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
// loop over the slice in order
|
||||
for index, value := range goSlice {
|
||||
// do something with index and value
|
||||
}
|
||||
|
||||
// return a packed array
|
||||
return frankenphp.PHPPackedArray([]string{"value1", "value2", "value3"})
|
||||
}
|
||||
```
|
||||
|
||||
**Key features of array conversion:**
|
||||
|
||||
- **Ordered key-value pairs** - Option to keep the order of the associative array
|
||||
- **Optimized for multiple cases** - Option to ditch the order for better performance or convert straight to a slice
|
||||
- **Automatic list detection** - When converting to PHP, automatically detects if array should be a packed list or hashmap
|
||||
- **Nested Arrays** - Arrays can be nested and will convert all support types automatically (`int64`,`float64`,`string`,`bool`,`nil`,`AssociativeArray`,`map[string]any`,`[]any`)
|
||||
- **Objects are not supported** - Currently, only scalar types and arrays can be used as values. Providing an object will result in a `null` value in the PHP array.
|
||||
|
||||
##### Available methods: Packed and Associative
|
||||
|
||||
- `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to an ordered PHP array with key-value pairs
|
||||
- `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convert a map to an unordered PHP array with key-value pairs
|
||||
- `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convert a slice to a PHP packed array with indexed values only
|
||||
- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to an ordered Go `AssociativeArray` (map with order)
|
||||
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered Go map
|
||||
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a Go slice
|
||||
- `frankenphp.IsPacked(zval *C.zend_array) bool` - Check if a PHP array is packed (indexed only) or associative (key-value pairs)
|
||||
|
||||
### Working with Callables
|
||||
|
||||
FrankenPHP provides a way to work with PHP callables using the `frankenphp.CallPHPCallable` helper. This allows you to call PHP functions or methods from Go code.
|
||||
|
||||
To showcase this, let's create our own `array_map()` function that takes a callable and an array, applies the callable to each element of the array, and returns a new array with the results:
|
||||
|
||||
```go
|
||||
// export_php:function my_array_map(array $data, callable $callback): array
|
||||
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
|
||||
goSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result := make([]any, len(goSlice))
|
||||
|
||||
for index, value := range goSlice {
|
||||
result[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
|
||||
}
|
||||
|
||||
return frankenphp.PHPPackedArray(result)
|
||||
}
|
||||
```
|
||||
|
||||
Notice how we use `frankenphp.CallPHPCallable()` to call the PHP callable passed as a parameter. This function takes a pointer to the callable and an array of arguments, and it returns the result of the callable execution. You can use the callable syntax you're used to:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
|
||||
// $result will be [2, 4, 6]
|
||||
|
||||
$result = my_array_map(['hello', 'world'], 'strtoupper');
|
||||
// $result will be ['HELLO', 'WORLD']
|
||||
```
|
||||
|
||||
### Declaring a Native PHP Class
|
||||
|
||||
The generator supports declaring **opaque classes** as Go structs, which can be used to create PHP objects. You can use the `//export_php:class` directive comment to define a PHP class. For example:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
```
|
||||
|
||||
#### What are Opaque Classes?
|
||||
|
||||
**Opaque classes** are classes where the internal structure (properties) is hidden from PHP code. This means:
|
||||
|
||||
- **No direct property access**: You cannot read or write properties directly from PHP (`$user->name` won't work)
|
||||
- **Method-only interface** - All interactions must go through methods you define
|
||||
- **Better encapsulation** - Internal data structure is completely controlled by Go code
|
||||
- **Type safety** - No risk of PHP code corrupting internal state with wrong types
|
||||
- **Cleaner API** - Forces to design a proper public interface
|
||||
|
||||
This approach provides better encapsulation and prevents PHP code from accidentally corrupting the internal state of your Go objects. All interactions with the object must go through the methods you explicitly define.
|
||||
|
||||
#### Adding Methods to Classes
|
||||
|
||||
Since properties are not directly accessible, you **must define methods** to interact with your opaque classes. Use the `//export_php:method` directive to define behavior:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
//export_php:method User::getName(): string
|
||||
func (us *UserStruct) GetUserName() unsafe.Pointer {
|
||||
return frankenphp.PHPString(us.Name, false)
|
||||
}
|
||||
|
||||
//export_php:method User::setAge(int $age): void
|
||||
func (us *UserStruct) SetUserAge(age int64) {
|
||||
us.Age = int(age)
|
||||
}
|
||||
|
||||
//export_php:method User::getAge(): int
|
||||
func (us *UserStruct) GetUserAge() int64 {
|
||||
return int64(us.Age)
|
||||
}
|
||||
|
||||
//export_php:method User::setNamePrefix(string $prefix = "User"): void
|
||||
func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
|
||||
us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + ": " + us.Name
|
||||
}
|
||||
```
|
||||
|
||||
#### Nullable Parameters
|
||||
|
||||
The generator supports nullable parameters using the `?` prefix in PHP signatures. When a parameter is nullable, it becomes a pointer in your Go function, allowing you to check if the value was `null` in PHP:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void
|
||||
func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {
|
||||
// Check if name was provided (not null)
|
||||
if name != nil {
|
||||
us.Name = frankenphp.GoString(unsafe.Pointer(name))
|
||||
}
|
||||
|
||||
// Check if age was provided (not null)
|
||||
if age != nil {
|
||||
us.Age = int(*age)
|
||||
}
|
||||
|
||||
// Check if active was provided (not null)
|
||||
if active != nil {
|
||||
us.Active = *active
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key points about nullable parameters:**
|
||||
|
||||
- **Nullable primitive types** (`?int`, `?float`, `?bool`) become pointers (`*int64`, `*float64`, `*bool`) in Go
|
||||
- **Nullable strings** (`?string`) remain as `*C.zend_string` but can be `nil`
|
||||
- **Check for `nil`** before dereferencing pointer values
|
||||
- **PHP `null` becomes Go `nil`** - when PHP passes `null`, your Go function receives a `nil` pointer
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> Currently, class methods have the following limitations. **Objects are not supported** as parameter types or return types. **Arrays are fully supported** for both parameters and return types. Supported types: `string`, `int`, `float`, `bool`, `array`, and `void` (for return type). **Nullable parameter types are fully supported** for all scalar types (`?string`, `?int`, `?float`, `?bool`).
|
||||
|
||||
After generating the extension, you will be allowed to use the class and its methods in PHP. Note that you **cannot access properties directly**:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
$user = new User();
|
||||
|
||||
// ✅ This works - using methods
|
||||
$user->setAge(25);
|
||||
echo $user->getName(); // Output: (empty, default value)
|
||||
echo $user->getAge(); // Output: 25
|
||||
$user->setNamePrefix("Employee");
|
||||
|
||||
// ✅ This also works - nullable parameters
|
||||
$user->updateInfo("John", 30, true); // All parameters provided
|
||||
$user->updateInfo("Jane", null, false); // Age is null
|
||||
$user->updateInfo(null, 25, null); // Name and active are null
|
||||
|
||||
// ❌ This will NOT work - direct property access
|
||||
// echo $user->name; // Error: Cannot access private property
|
||||
// $user->age = 30; // Error: Cannot access private property
|
||||
```
|
||||
|
||||
This design ensures that your Go code has complete control over how the object's state is accessed and modified, providing better encapsulation and type safety.
|
||||
|
||||
### Declaring Constants
|
||||
|
||||
The generator supports exporting Go constants to PHP using two directives: `//export_php:const` for global constants and `//export_php:classconst` for class constants. This allows you to share configuration values, status codes, and other constants between Go and PHP code.
|
||||
|
||||
#### Global Constants
|
||||
|
||||
Use the `//export_php:const` directive to create global PHP constants:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
//export_php:const
|
||||
const MAX_CONNECTIONS = 100
|
||||
|
||||
//export_php:const
|
||||
const API_VERSION = "1.2.3"
|
||||
|
||||
//export_php:const
|
||||
const STATUS_OK = iota
|
||||
|
||||
//export_php:const
|
||||
const STATUS_ERROR = iota
|
||||
```
|
||||
|
||||
#### Class Constants
|
||||
|
||||
Use the `//export_php:classconst ClassName` directive to create constants that belong to a specific PHP class:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
//export_php:classconst User
|
||||
const STATUS_ACTIVE = 1
|
||||
|
||||
//export_php:classconst User
|
||||
const STATUS_INACTIVE = 0
|
||||
|
||||
//export_php:classconst User
|
||||
const ROLE_ADMIN = "admin"
|
||||
|
||||
//export_php:classconst Order
|
||||
const STATE_PENDING = iota
|
||||
|
||||
//export_php:classconst Order
|
||||
const STATE_PROCESSING = iota
|
||||
|
||||
//export_php:classconst Order
|
||||
const STATE_COMPLETED = iota
|
||||
```
|
||||
|
||||
Class constants are accessible using the class name scope in PHP:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// Global constants
|
||||
echo MAX_CONNECTIONS; // 100
|
||||
echo API_VERSION; // "1.2.3"
|
||||
|
||||
// Class constants
|
||||
echo User::STATUS_ACTIVE; // 1
|
||||
echo User::ROLE_ADMIN; // "admin"
|
||||
echo Order::STATE_PENDING; // 0
|
||||
```
|
||||
|
||||
The directive supports various value types including strings, integers, booleans, floats, and iota constants. When using `iota`, the generator automatically assigns sequential values (0, 1, 2, etc.). Global constants become available in your PHP code as global constants, while class constants are scoped to their respective classes using the public visibility. When using integers, different possible notation (binary, hex, octal) are supported and dumped as is in the PHP stub file.
|
||||
|
||||
You can use constants just like you are used to in the Go code. For example, let's take the `repeat_this()` function we declared earlier and change the last argument to an integer:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:const
|
||||
const STR_REVERSE = iota
|
||||
|
||||
//export_php:const
|
||||
const STR_NORMAL = iota
|
||||
|
||||
//export_php:classconst StringProcessor
|
||||
const MODE_LOWERCASE = 1
|
||||
|
||||
//export_php:classconst StringProcessor
|
||||
const MODE_UPPERCASE = 2
|
||||
|
||||
//export_php:function repeat_this(string $str, int $count, int $mode): string
|
||||
func repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(s))
|
||||
|
||||
result := strings.Repeat(str, int(count))
|
||||
if mode == STR_REVERSE {
|
||||
// reverse the string
|
||||
}
|
||||
|
||||
if mode == STR_NORMAL {
|
||||
// no-op, just to showcase the constant
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(result, false)
|
||||
}
|
||||
|
||||
//export_php:class StringProcessor
|
||||
type StringProcessorStruct struct {
|
||||
// internal fields
|
||||
}
|
||||
|
||||
//export_php:method StringProcessor::process(string $input, int $mode): string
|
||||
func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(input))
|
||||
|
||||
switch mode {
|
||||
case MODE_LOWERCASE:
|
||||
str = strings.ToLower(str)
|
||||
case MODE_UPPERCASE:
|
||||
str = strings.ToUpper(str)
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(str, false)
|
||||
}
|
||||
```
|
||||
|
||||
### Using Namespaces
|
||||
|
||||
The generator supports organizing your PHP extension's functions, classes, and constants under a namespace using the `//export_php:namespace` directive. This helps avoid naming conflicts and provides better organization for your extension's API.
|
||||
|
||||
#### Declaring a Namespace
|
||||
|
||||
Use the `//export_php:namespace` directive at the top of your Go file to place all exported symbols under a specific namespace:
|
||||
|
||||
```go
|
||||
//export_php:namespace My\Extension
|
||||
package example
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:function hello(): string
|
||||
func hello() string {
|
||||
return "Hello from My\\Extension namespace!"
|
||||
}
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
// internal fields
|
||||
}
|
||||
|
||||
//export_php:method User::getName(): string
|
||||
func (u *UserStruct) GetName() unsafe.Pointer {
|
||||
return frankenphp.PHPString("John Doe", false)
|
||||
}
|
||||
|
||||
//export_php:const
|
||||
const STATUS_ACTIVE = 1
|
||||
```
|
||||
|
||||
#### Using Namespaced Extension in PHP
|
||||
|
||||
When a namespace is declared, all functions, classes, and constants are placed under that namespace in PHP:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
echo My\Extension\hello(); // "Hello from My\Extension namespace!"
|
||||
|
||||
$user = new My\Extension\User();
|
||||
echo $user->getName(); // "John Doe"
|
||||
|
||||
echo My\Extension\STATUS_ACTIVE; // 1
|
||||
```
|
||||
|
||||
#### Important Notes
|
||||
|
||||
- Only **one** namespace directive is allowed per file. If multiple namespace directives are found, the generator will return an error.
|
||||
- The namespace applies to **all** exported symbols in the file: functions, classes, methods, and constants.
|
||||
- Namespace names follow PHP namespace conventions using backslashes (`\`) as separators.
|
||||
- If no namespace is declared, symbols are exported to the global namespace as usual.
|
||||
|
||||
### Generating the Extension
|
||||
|
||||
This is where the magic happens, and your extension can now be generated. You can run the generator with the following command:
|
||||
|
||||
```console
|
||||
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Don't forget to set the `GEN_STUB_SCRIPT` environment variable to the path of the `gen_stub.php` file in the PHP sources you downloaded earlier. This is the same `gen_stub.php` script mentioned in the manual implementation section.
|
||||
|
||||
If everything went well, a new directory named `build` should have been created. This directory contains the generated files for your extension, including the `my_extension.go` file with the generated PHP function stubs.
|
||||
|
||||
### Integrating the Generated Extension into FrankenPHP
|
||||
|
||||
Our extension is now ready to be compiled and integrated into FrankenPHP. To do this, refer to the FrankenPHP [compilation documentation](compile.md) to learn how to compile FrankenPHP. Add the module using the `--with` flag, pointing to the path of your module:
|
||||
|
||||
```console
|
||||
CGO_ENABLED=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
||||
CGO_CFLAGS=$(php-config --includes) \
|
||||
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
||||
xcaddy build \
|
||||
--output frankenphp \
|
||||
--with github.com/my-account/my-module/build
|
||||
```
|
||||
|
||||
Note that you point to the `/build` subdirectory that was created during the generation step. However, this is not mandatory: you can also copy the generated files to your module directory and point to it directly.
|
||||
|
||||
### Testing Your Generated Extension
|
||||
|
||||
You can create a PHP file to test the functions and classes you've created. For example, create an `index.php` file with the following content:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// Using global constants
|
||||
var_dump(repeat_this('Hello World', 5, STR_REVERSE));
|
||||
|
||||
// Using class constants
|
||||
$processor = new StringProcessor();
|
||||
echo $processor->process('Hello World', StringProcessor::MODE_LOWERCASE); // "hello world"
|
||||
echo $processor->process('Hello World', StringProcessor::MODE_UPPERCASE); // "HELLO WORLD"
|
||||
```
|
||||
|
||||
Once you've integrated your extension into FrankenPHP as demonstrated in the previous section, you can run this test file using `./frankenphp php-server`, and you should see your extension working.
|
||||
|
||||
## Manual Implementation
|
||||
|
||||
If you want to understand how extensions work or need full control over your extension, you can write them manually. This approach gives you complete control but requires more boilerplate code.
|
||||
|
||||
### Basic Function
|
||||
|
||||
We'll see how to write a simple PHP extension in Go that defines a new native function. This function will be called from PHP and will trigger a goroutine that logs a message in Caddy's logs. This function doesn't take any parameters and returns nothing.
|
||||
|
||||
#### Define the Go Function
|
||||
|
||||
In your module, you need to define a new native function that will be called from PHP. To do this, create a file with the name you want, for example, `extension.go`, and add the following code:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include "extension.h"
|
||||
import "C"
|
||||
import (
|
||||
"log/slog"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
|
||||
}
|
||||
|
||||
//export go_print_something
|
||||
func go_print_something() {
|
||||
go func() {
|
||||
slog.Info("Hello from a goroutine!")
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
The `frankenphp.RegisterExtension()` function simplifies the extension registration process by handling the internal PHP registration logic. The `go_print_something` function uses the `//export` directive to indicate that it will be accessible in the C code we will write, thanks to CGO.
|
||||
|
||||
In this example, our new function will trigger a goroutine that logs a message in Caddy's logs.
|
||||
|
||||
#### Define the PHP Function
|
||||
|
||||
To allow PHP to call our function, we need to define a corresponding PHP function. For this, we will create a stub file, for example, `extension.stub.php`, which will contain the following code:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @generate-class-entries */
|
||||
|
||||
function go_print(): void {}
|
||||
```
|
||||
|
||||
This file defines the signature of the `go_print()` function, which will be called from PHP. The `@generate-class-entries` directive allows PHP to automatically generate function entries for our extension.
|
||||
|
||||
This is not done manually but using a script provided in the PHP sources (make sure to adjust the path to the `gen_stub.php` script based on where your PHP sources are located):
|
||||
|
||||
```bash
|
||||
php ../php-src/build/gen_stub.php extension.stub.php
|
||||
```
|
||||
|
||||
This script will generate a file named `extension_arginfo.h` that contains the necessary information for PHP to know how to define and call our function.
|
||||
|
||||
#### Write the Bridge Between Go and C
|
||||
|
||||
Now, we need to write the bridge between Go and C. Create a file named `extension.h` in your module directory with the following content:
|
||||
|
||||
```c
|
||||
#ifndef _EXTENSION_H
|
||||
#define _EXTENSION_H
|
||||
|
||||
#include <php.h>
|
||||
|
||||
extern zend_module_entry ext_module_entry;
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
Next, create a file named `extension.c` that will perform the following steps:
|
||||
|
||||
- Include PHP headers;
|
||||
- Declare our new native PHP function `go_print()`;
|
||||
- Declare the extension metadata.
|
||||
|
||||
Let's start by including the required headers:
|
||||
|
||||
```c
|
||||
#include <php.h>
|
||||
#include "extension.h"
|
||||
#include "extension_arginfo.h"
|
||||
|
||||
// Contains symbols exported by Go
|
||||
#include "_cgo_export.h"
|
||||
```
|
||||
|
||||
We then define our PHP function as a native language function:
|
||||
|
||||
```c
|
||||
PHP_FUNCTION(go_print)
|
||||
{
|
||||
ZEND_PARSE_PARAMETERS_NONE();
|
||||
|
||||
go_print_something();
|
||||
}
|
||||
|
||||
zend_module_entry ext_module_entry = {
|
||||
STANDARD_MODULE_HEADER,
|
||||
"ext_go",
|
||||
ext_functions, /* Functions */
|
||||
NULL, /* MINIT */
|
||||
NULL, /* MSHUTDOWN */
|
||||
NULL, /* RINIT */
|
||||
NULL, /* RSHUTDOWN */
|
||||
NULL, /* MINFO */
|
||||
"0.1.1",
|
||||
STANDARD_MODULE_PROPERTIES
|
||||
};
|
||||
```
|
||||
|
||||
In this case, our function takes no parameters and returns nothing. It simply calls the Go function we defined earlier, exported using the `//export` directive.
|
||||
|
||||
Finally, we define the extension's metadata in a `zend_module_entry` structure, such as its name, version, and properties. This information is necessary for PHP to recognize and load our extension. Note that `ext_functions` is an array of pointers to the PHP functions we defined, and it was automatically generated by the `gen_stub.php` script in the `extension_arginfo.h` file.
|
||||
|
||||
The extension registration is automatically handled by FrankenPHP's `RegisterExtension()` function that we call in our Go code.
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
Now that we know how to create a basic PHP extension in Go, let's complexify our example. We will now create a PHP function that takes a string as a parameter and returns its uppercase version.
|
||||
|
||||
#### Define the PHP Function Stub
|
||||
|
||||
To define the new PHP function, we will modify our `extension.stub.php` file to include the new function signature:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @generate-class-entries */
|
||||
|
||||
/**
|
||||
* Converts a string to uppercase.
|
||||
*
|
||||
* @param string $string The string to convert.
|
||||
* @return string The uppercase version of the string.
|
||||
*/
|
||||
function go_upper(string $string): string {}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Don't neglect the documentation of your functions! You are likely to share your extension stubs with other developers to document how to use your extension and which features are available.
|
||||
|
||||
By regenerating the stub file with the `gen_stub.php` script, the `extension_arginfo.h` file should look like this:
|
||||
|
||||
```c
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)
|
||||
ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
ZEND_FUNCTION(go_upper);
|
||||
|
||||
static const zend_function_entry ext_functions[] = {
|
||||
ZEND_FE(go_upper, arginfo_go_upper)
|
||||
ZEND_FE_END
|
||||
};
|
||||
```
|
||||
|
||||
We can see that the `go_upper` function is defined with a parameter of type `string` and a return type of `string`.
|
||||
|
||||
#### Type Juggling Between Go and PHP/C
|
||||
|
||||
Your Go function cannot directly accept a PHP string as a parameter. You need to convert it to a Go string. Fortunately, FrankenPHP provides helper functions to handle the conversion between PHP strings and Go strings, similar to what we saw in the generator approach.
|
||||
|
||||
The header file remains simple:
|
||||
|
||||
```c
|
||||
#ifndef _EXTENSION_H
|
||||
#define _EXTENSION_H
|
||||
|
||||
#include <php.h>
|
||||
|
||||
extern zend_module_entry ext_module_entry;
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
We can now write the bridge between Go and C in our `extension.c` file. We will pass the PHP string directly to our Go function:
|
||||
|
||||
```c
|
||||
PHP_FUNCTION(go_upper)
|
||||
{
|
||||
zend_string *str;
|
||||
|
||||
ZEND_PARSE_PARAMETERS_START(1, 1)
|
||||
Z_PARAM_STR(str)
|
||||
ZEND_PARSE_PARAMETERS_END();
|
||||
|
||||
zend_string *result = go_upper(str);
|
||||
RETVAL_STR(result);
|
||||
}
|
||||
```
|
||||
|
||||
You can learn more about the `ZEND_PARSE_PARAMETERS_START` and parameters parsing in the dedicated page of [the PHP Internals Book](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters). Here, we tell PHP that our function takes one mandatory parameter of type `string` as a `zend_string`. We then pass this string directly to our Go function and return the result using `RETVAL_STR`.
|
||||
|
||||
There's only one thing left to do: implement the `go_upper` function in Go.
|
||||
|
||||
#### Implement the Go Function
|
||||
|
||||
Our Go function will take a `*C.zend_string` as a parameter, convert it to a Go string using FrankenPHP's helper function, process it, and return the result as a new `*C.zend_string`. The helper functions handle all the memory management and conversion complexity for us.
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
"strings"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export go_upper
|
||||
func go_upper(s *C.zend_string) *C.zend_string {
|
||||
str := frankenphp.GoString(unsafe.Pointer(s))
|
||||
|
||||
upper := strings.ToUpper(str)
|
||||
|
||||
return (*C.zend_string)(frankenphp.PHPString(upper, false))
|
||||
}
|
||||
```
|
||||
|
||||
This approach is much cleaner and safer than manual memory management.
|
||||
FrankenPHP's helper functions handle the conversion between PHP's `zend_string` format and Go strings automatically.
|
||||
The `false` parameter in `PHPString()` indicates that we want to create a new non-persistent string (freed at the end of the request).
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> In this example, we don't perform any error handling, but you should always check that pointers are not `nil` and that the data is valid before using it in your Go functions.
|
||||
|
||||
### Integrating the Extension into FrankenPHP
|
||||
|
||||
Our extension is now ready to be compiled and integrated into FrankenPHP. To do this, refer to the FrankenPHP [compilation documentation](compile.md) to learn how to compile FrankenPHP. Add the module using the `--with` flag, pointing to the path of your module:
|
||||
|
||||
```console
|
||||
CGO_ENABLED=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
||||
CGO_CFLAGS=$(php-config --includes) \
|
||||
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
||||
xcaddy build \
|
||||
--output frankenphp \
|
||||
--with github.com/my-account/my-module
|
||||
```
|
||||
|
||||
That's it! Your extension is now integrated into FrankenPHP and can be used in your PHP code.
|
||||
|
||||
### Testing Your Extension
|
||||
|
||||
After integrating your extension into FrankenPHP, you can create an `index.php` file with examples for the functions you've implemented:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// Test basic function
|
||||
go_print();
|
||||
|
||||
// Test advanced function
|
||||
echo go_upper("hello world") . "\n";
|
||||
```
|
||||
|
||||
You can now run FrankenPHP with this file using `./frankenphp php-server`, and you should see your extension working.
|
||||
@@ -11,9 +11,13 @@ docker build -t frankenphp-dev -f dev.Dockerfile .
|
||||
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev
|
||||
```
|
||||
|
||||
L'image contient les outils de développement habituels (Go, GDB, Valgrind, Neovim...).
|
||||
L'image contient les outils de développement habituels (Go, GDB, Valgrind, Neovim...) et utilise les emplacements de configuration PHP suivants
|
||||
|
||||
Si la version de Docker est inférieure à 23.0, la construction échoue à cause d'un [problème de pattern](https://github.com/moby/moby/pull/42676) dans `.dockerignore`. Ajoutez les répertoires à `.dockerignore`.
|
||||
- php.ini: `/etc/frankenphp/php.ini` Un fichier php.ini avec des préréglages de développement est fourni par défaut.
|
||||
- fichiers de configuration supplémentaires: `/etc/frankenphp/php.d/*.ini`
|
||||
- extensions php: `/usr/lib/frankenphp/modules/`
|
||||
|
||||
Si votre version de Docker est inférieure à 23.0, la construction échouera à cause d'un [problème de pattern](https://github.com/moby/moby/pull/42676) dans `.dockerignore`. Ajoutez les répertoires à `.dockerignore`.
|
||||
|
||||
```patch
|
||||
!testdata/*.php
|
||||
@@ -38,7 +42,7 @@ Construire Caddy avec le module FrankenPHP :
|
||||
|
||||
```console
|
||||
cd caddy/frankenphp/
|
||||
go build
|
||||
go build -tags watcher,brotli,nobadger,nomysql,nopgx
|
||||
cd ../../
|
||||
```
|
||||
|
||||
@@ -49,10 +53,14 @@ cd testdata/
|
||||
../caddy/frankenphp/frankenphp run
|
||||
```
|
||||
|
||||
Le serveur est configuré pour écouter à l'adresse `127.0.0.1:8080`:
|
||||
Le serveur est configuré pour écouter à l'adresse `127.0.0.1:80`:
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Si vous utilisez Docker, vous devrez soit lier le port 80 du conteneur, soit exécuter depuis l'intérieur du conteneur.
|
||||
|
||||
```console
|
||||
curl -vk https://localhost/phpinfo.php
|
||||
curl -vk http://127.0.0.1/phpinfo.php
|
||||
```
|
||||
|
||||
## Serveur de test minimal
|
||||
@@ -104,69 +112,92 @@ Construire à partir de zéro les images FrankenPHP pour arm64 & amd64 et les po
|
||||
docker buildx bake -f docker-bake.hcl --pull --no-cache --push
|
||||
```
|
||||
|
||||
## Déboguer les erreurs de segmentation avec les builds statiques
|
||||
|
||||
1. Téléchargez la version de débogage du binaire FrankenPHP depuis GitHub ou créez votre propre build statique incluant des symboles de débogage :
|
||||
|
||||
```console
|
||||
docker buildx bake \
|
||||
--load \
|
||||
--set static-builder.args.DEBUG_SYMBOLS=1 \
|
||||
--set "static-builder.platform=linux/amd64" \
|
||||
static-builder
|
||||
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
|
||||
```
|
||||
|
||||
2. Remplacez votre version actuelle de `frankenphp` par l'exécutable de débogage de FrankenPHP.
|
||||
3. Démarrez FrankenPHP comme d'habitude (alternativement, vous pouvez directement démarrer FrankenPHP avec GDB : `gdb --args frankenphp run`).
|
||||
4. Attachez-vous au processus avec GDB :
|
||||
|
||||
```console
|
||||
gdb -p `pidof frankenphp`
|
||||
```
|
||||
|
||||
5. Si nécessaire, tapez `continue` dans le shell GDB
|
||||
6. Faites planter FrankenPHP.
|
||||
7. Tapez `bt` dans le shell GDB
|
||||
8. Copiez la sortie
|
||||
|
||||
## Déboguer les erreurs de segmentation dans GitHub Actions
|
||||
|
||||
1. Ouvrir `.github/workflows/tests.yml`
|
||||
2. Activer les symboles de débogage de la bibliothèque PHP
|
||||
|
||||
```patch
|
||||
- uses: shivammathur/setup-php@v2
|
||||
# ...
|
||||
env:
|
||||
phpts: ts
|
||||
+ debug: true
|
||||
```
|
||||
```patch
|
||||
- uses: shivammathur/setup-php@v2
|
||||
# ...
|
||||
env:
|
||||
phpts: ts
|
||||
+ debug: true
|
||||
```
|
||||
|
||||
3. Activer `tmate` pour se connecter au conteneur
|
||||
|
||||
```patch
|
||||
-
|
||||
name: Set CGO flags
|
||||
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
|
||||
+ -
|
||||
+ run: |
|
||||
+ sudo apt install gdb
|
||||
+ mkdir -p /home/runner/.config/gdb/
|
||||
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
|
||||
+ -
|
||||
+ uses: mxschmitt/action-tmate@v3
|
||||
```
|
||||
```patch
|
||||
- name: Set CGO flags
|
||||
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
|
||||
+ - run: |
|
||||
+ sudo apt install gdb
|
||||
+ mkdir -p /home/runner/.config/gdb/
|
||||
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
|
||||
+ - uses: mxschmitt/action-tmate@v3
|
||||
```
|
||||
|
||||
4. Se connecter au conteneur
|
||||
5. Ouvrir `frankenphp.go`
|
||||
6. Activer `cgosymbolizer`
|
||||
|
||||
```patch
|
||||
- //_ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
+ _ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
```
|
||||
```patch
|
||||
- //_ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
+ _ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
```
|
||||
|
||||
7. Télécharger le module : `go get`
|
||||
8. Dans le conteneur, vous pouvez utiliser GDB et similaires :
|
||||
|
||||
```console
|
||||
go test -tags watcher -c -ldflags=-w
|
||||
gdb --args frankenphp.test -test.run ^MyTest$
|
||||
```
|
||||
```console
|
||||
go test -tags watcher -c -ldflags=-w
|
||||
gdb --args frankenphp.test -test.run ^MyTest$
|
||||
```
|
||||
|
||||
9. Quand le bug est corrigé, annulez tous les changements
|
||||
9. Quand le bug est corrigé, annulez tous les changements.
|
||||
|
||||
## Ressources Diverses pour le Développement
|
||||
|
||||
* [Intégration de PHP dans uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
|
||||
* [Intégration de PHP dans NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
|
||||
* [Intégration de PHP dans Go (go-php)](https://github.com/deuill/go-php)
|
||||
* [Intégration de PHP dans Go (GoEmPHP)](https://github.com/mikespook/goemphp)
|
||||
* [Intégration de PHP dans C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
|
||||
* [Extending and Embedding PHP par Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
|
||||
* [Qu'est-ce que TSRMLS_CC, au juste ?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
|
||||
* [Intégration de PHP sur Mac](https://gist.github.com/jonnywang/61427ffc0e8dde74fff40f479d147db4)
|
||||
* [Bindings SDL](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)
|
||||
- [Intégration de PHP dans uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
|
||||
- [Intégration de PHP dans NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
|
||||
- [Intégration de PHP dans Go (go-php)](https://github.com/deuill/go-php)
|
||||
- [Intégration de PHP dans Go (GoEmPHP)](https://github.com/mikespook/goemphp)
|
||||
- [Intégration de PHP dans C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
|
||||
- [Extending and Embedding PHP par Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
|
||||
- [Qu'est-ce que TSRMLS_CC, au juste ?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
|
||||
- [Intégration de PHP sur Mac](https://gist.github.com/jonnywang/61427ffc0e8dde74fff40f479d147db4)
|
||||
- [Bindings SDL](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)
|
||||
|
||||
## Ressources Liées à Docker
|
||||
|
||||
* [Définition du fichier Bake](https://docs.docker.com/build/customize/bake/file-definition/)
|
||||
* [docker buildx build](https://docs.docker.com/engine/reference/commandline/buildx_build/)
|
||||
- [Définition du fichier Bake](https://docs.docker.com/build/customize/bake/file-definition/)
|
||||
- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)
|
||||
|
||||
## Commande utile
|
||||
|
||||
@@ -174,3 +205,16 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push
|
||||
apk add strace util-linux gdb
|
||||
strace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1
|
||||
```
|
||||
|
||||
## Traduire la documentation
|
||||
|
||||
Pour traduire la documentation et le site dans une nouvelle langue, procédez comme suit :
|
||||
|
||||
1. Créez un nouveau répertoire nommé avec le code ISO à 2 caractères de la langue dans le répertoire `docs/` de ce dépôt
|
||||
2. Copiez tous les fichiers `.md` à la racine du répertoire `docs/` dans le nouveau répertoire (utilisez toujours la version anglaise comme source de traduction, car elle est toujours à jour).
|
||||
3. Copiez les fichiers `README.md` et `CONTRIBUTING.md` du répertoire racine vers le nouveau répertoire.
|
||||
4. Traduisez le contenu des fichiers, mais ne changez pas les noms de fichiers, ne traduisez pas non plus les chaînes commençant par `> [!` (c'est un balisage spécial pour GitHub).
|
||||
5. Créez une Pull Request avec les traductions
|
||||
6. Dans le [référentiel du site](https://github.com/dunglas/frankenphp-website/tree/main), copiez et traduisez les fichiers de traduction dans les répertoires `content/`, `data/` et `i18n/`.
|
||||
7. Traduire les valeurs dans le fichier YAML créé.
|
||||
8. Ouvrir une Pull Request sur le dépôt du site.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
FrankenPHP est un serveur d'applications moderne pour PHP construit à partir du serveur web [Caddy](https://caddyserver.com/).
|
||||
|
||||
FrankenPHP donne des super-pouvoirs à vos applications PHP grâce à ses fonctionnalités à la pointe : [*Early Hints*](early-hints.md), [mode worker](worker.md), [fonctionnalités en temps réel](mercure.md), HTTPS automatique, prise en charge de HTTP/2 et HTTP/3...
|
||||
FrankenPHP donne des super-pouvoirs à vos applications PHP grâce à ses fonctionnalités à la pointe : [_Early Hints_](early-hints.md), [mode worker](worker.md), [fonctionnalités en temps réel](mercure.md), HTTPS automatique, prise en charge de HTTP/2 et HTTP/3...
|
||||
|
||||
FrankenPHP fonctionne avec n'importe quelle application PHP et rend vos projets Laravel et Symfony plus rapides que jamais grâce à leurs intégrations officielles avec le mode worker.
|
||||
|
||||
@@ -16,44 +16,71 @@ Découvrez plus de détails sur ce serveur d’application dans le replay de cet
|
||||
|
||||
## Pour Commencer
|
||||
|
||||
### Binaire autonome
|
||||
Sur Windows, utilisez [WSL](https://learn.microsoft.com/windows/wsl/) pour exécuter FrankenPHP.
|
||||
|
||||
Si vous préférez ne pas utiliser Docker, nous fournissons des binaires autonomes de FrankenPHP pour Linux et macOS
|
||||
contenant [PHP 8.4](https://www.php.net/releases/8.4/fr.php) et la plupart des extensions PHP populaires.
|
||||
### Script d'installation
|
||||
|
||||
Sous Windows, utilisez [WSL](https://learn.microsoft.com/windows/wsl/) pour exécuter FrankenPHP.
|
||||
|
||||
[Téléchargez FrankenPHP](https://github.com/dunglas/frankenphp/releases) ou copiez cette ligne dans votre terminal pour installer automatiquement la version appropriée à votre plateforme :
|
||||
Vous pouvez copier cette ligne dans votre terminal pour installer automatiquement
|
||||
une version adaptée à votre plateforme :
|
||||
|
||||
```console
|
||||
curl https://frankenphp.dev/install.sh | sh
|
||||
mv frankenphp /usr/local/bin/
|
||||
```
|
||||
|
||||
Pour servir le contenu du répertoire courant, exécutez :
|
||||
### Binaire autonome
|
||||
|
||||
Nous fournissons des binaires statiques de FrankenPHP pour le développement, pour Linux et macOS,
|
||||
contenant [PHP 8.4](https://www.php.net/releases/8.4/fr.php) et la plupart des extensions PHP populaires.
|
||||
|
||||
[Télécharger FrankenPHP](https://github.com/php/frankenphp/releases)
|
||||
|
||||
**Installation d'extensions :** Les extensions les plus courantes sont incluses. Il n'est pas possible d'en installer davantage.
|
||||
|
||||
### Paquets rpm
|
||||
|
||||
Nos mainteneurs proposent des paquets rpm pour tous les systèmes utilisant `dnf`. Pour installer, exécutez :
|
||||
|
||||
```console
|
||||
frankenphp php-server
|
||||
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
|
||||
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 disponibles
|
||||
sudo dnf install frankenphp
|
||||
```
|
||||
|
||||
Vous pouvez également exécuter des scripts en ligne de commande avec :
|
||||
**Installation d'extensions :** `sudo dnf install php-zts-<extension>`
|
||||
|
||||
Pour les extensions non disponibles par défaut, utilisez [PIE](https://github.com/php/pie) :
|
||||
|
||||
```console
|
||||
frankenphp php-cli /path/to/your/script.php
|
||||
sudo dnf install pie-zts
|
||||
sudo pie-zts install asgrim/example-pie-extension
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> En production, préférez utiliser [les images Docker](#docker), [le paquet Brew](#homebrew)
|
||||
> ou [compiler FrankenPHP à partir des sources](https://frankenphp.dev/docs/fr/compile/).
|
||||
> Le binaire autonome est fourni à des fins de développement et de test.
|
||||
### Paquets deb
|
||||
|
||||
Nos mainteneurs proposent des paquets deb pour tous les systèmes utilisant `apt`. Pour installer, exécutez :
|
||||
|
||||
```console
|
||||
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" | sudo tee /etc/apt/sources.list.d/static-php.list && \
|
||||
sudo apt update
|
||||
sudo apt install frankenphp
|
||||
```
|
||||
|
||||
**Installation d'extensions :** `sudo apt install php-zts-<extension>`
|
||||
|
||||
Pour les extensions non disponibles par défaut, utilisez [PIE](https://github.com/php/pie) :
|
||||
|
||||
```console
|
||||
sudo apt install pie-zts
|
||||
sudo pie-zts install asgrim/example-pie-extension
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
Des [images Docker](https://frankenphp.dev/docs/fr/docker/) sont également disponibles :
|
||||
|
||||
```console
|
||||
docker run -v $PWD:/app/public \
|
||||
docker run -v .:/app/public \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
@@ -75,38 +102,58 @@ Pour l'installer :
|
||||
brew install dunglas/frankenphp/frankenphp
|
||||
```
|
||||
|
||||
**Installation d'extensions :** Utilisez [PIE](https://github.com/php/pie).
|
||||
|
||||
### Utilisation
|
||||
|
||||
Pour servir le contenu du répertoire courant, exécutez :
|
||||
|
||||
```console
|
||||
frankenphp php-server
|
||||
```
|
||||
|
||||
Vous pouvez également exécuter des scripts en ligne de commande avec :
|
||||
|
||||
```console
|
||||
frankenphp php-cli /path/to/your/script.php
|
||||
```
|
||||
|
||||
Pour les paquets deb et rpm, vous pouvez aussi démarrer le service systemd :
|
||||
|
||||
```console
|
||||
sudo systemctl start frankenphp
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
* [Le mode worker](worker.md)
|
||||
* [Le support des Early Hints (code de statut HTTP 103)](early-hints.md)
|
||||
* [Temps réel](mercure.md)
|
||||
* [Configuration](config.md)
|
||||
* [Images Docker](docker.md)
|
||||
* [Déploiement en production](production.md)
|
||||
* [Optimisation des performances](performance.md)
|
||||
* [Créer des applications PHP **standalone**, auto-exécutables](embed.md)
|
||||
* [Créer un build statique](static.md)
|
||||
* [Compiler depuis les sources](compile.md)
|
||||
* [Intégration Laravel](laravel.md)
|
||||
* [Problèmes connus](known-issues.md)
|
||||
* [Application de démo (Symfony) et benchmarks](https://github.com/dunglas/frankenphp-demo)
|
||||
* [Documentation de la bibliothèque Go](https://pkg.go.dev/github.com/dunglas/frankenphp)
|
||||
* [Contribuer et débugger](CONTRIBUTING.md)
|
||||
- [Le mode classique](classic.md)
|
||||
- [Le mode worker](worker.md)
|
||||
- [Le support des Early Hints (code de statut HTTP 103)](early-hints.md)
|
||||
- [Temps réel](mercure.md)
|
||||
- [Servir efficacement les fichiers statiques volumineux](x-sendfile.md)
|
||||
- [Configuration](config.md)
|
||||
- [Écrire des extensions PHP en Go](extensions.md)
|
||||
- [Images Docker](docker.md)
|
||||
- [Déploiement en production](production.md)
|
||||
- [Optimisation des performances](performance.md)
|
||||
- [Créer des applications PHP **standalone**, auto-exécutables](embed.md)
|
||||
- [Créer un build statique](static.md)
|
||||
- [Compiler depuis les sources](compile.md)
|
||||
- [Surveillance de FrankenPHP](metrics.md)
|
||||
- [Intégration Laravel](laravel.md)
|
||||
- [Problèmes connus](known-issues.md)
|
||||
- [Application de démo (Symfony) et benchmarks](https://github.com/dunglas/frankenphp-demo)
|
||||
- [Documentation de la bibliothèque Go](https://pkg.go.dev/github.com/dunglas/frankenphp)
|
||||
- [Contribuer et débugger](CONTRIBUTING.md)
|
||||
|
||||
## Exemples et squelettes
|
||||
|
||||
* [Symfony](https://github.com/dunglas/symfony-docker)
|
||||
* [API Platform](https://api-platform.com/docs/distribution/)
|
||||
* [Laravel](laravel.md)
|
||||
* [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
|
||||
* [WordPress](https://github.com/StephenMiracle/frankenwp)
|
||||
* [Drupal](https://github.com/dunglas/frankenphp-drupal)
|
||||
* [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
|
||||
* [TYPO3](https://github.com/ochorocho/franken-typo3)
|
||||
* [Magento2](https://github.com/ekino/frankenphp-magento2)
|
||||
- [Symfony](https://github.com/dunglas/symfony-docker)
|
||||
- [API Platform](https://api-platform.com/docs/distribution/)
|
||||
- [Laravel](laravel.md)
|
||||
- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
|
||||
- [WordPress](https://github.com/StephenMiracle/frankenwp)
|
||||
- [Drupal](https://github.com/dunglas/frankenphp-drupal)
|
||||
- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
|
||||
- [TYPO3](https://github.com/ochorocho/franken-typo3)
|
||||
- [Magento2](https://github.com/ekino/frankenphp-magento2)
|
||||
|
||||
11
docs/fr/classic.md
Normal file
11
docs/fr/classic.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Utilisation du mode classique
|
||||
|
||||
Sans aucune configuration additionnelle, FrankenPHP fonctionne en mode classique. Dans ce mode, FrankenPHP fonctionne comme un serveur PHP traditionnel, en servant directement les fichiers PHP. Cela en fait un remplaçant parfait à PHP-FPM ou Apache avec mod_php.
|
||||
|
||||
Comme Caddy, FrankenPHP accepte un nombre illimité de connexions et utilise un [nombre fixe de threads](config.md#configuration-du-caddyfile) pour les servir. Le nombre de connexions acceptées et en attente n'est limité que par les ressources système disponibles.
|
||||
Le pool de threads PHP fonctionne avec un nombre fixe de threads initialisés au démarrage, comparable au mode statique de PHP-FPM. Il est également possible de laisser les threads [s'adapter automatiquement à l'exécution](performance.md#max_threads), comme dans le mode dynamique de PHP-FPM.
|
||||
|
||||
Les connexions en file d'attente attendront indéfiniment jusqu'à ce qu'un thread PHP soit disponible pour les servir. Pour éviter cela, vous pouvez utiliser la [configuration](config.md#configuration-du-caddyfile) `max_wait_time` dans la configuration globale de FrankenPHP pour limiter la durée pendant laquelle une requête peut attendre un thread PHP libre avant d'être rejetée.
|
||||
En outre, vous pouvez définir un [délai d'écriture dans Caddy](https://caddyserver.com/docs/caddyfile/options#timeouts) raisonnable.
|
||||
|
||||
Chaque instance de Caddy n'utilisera qu'un seul pool de threads FrankenPHP, qui sera partagé par tous les blocs `php_server`.
|
||||
@@ -9,6 +9,23 @@ Alternativement, il est aussi possible de [créer des builds statiques](static.m
|
||||
|
||||
FrankenPHP est compatible avec PHP 8.2 et versions ultérieures.
|
||||
|
||||
### Avec Homebrew (Linux et Mac)
|
||||
|
||||
La manière la plus simple d'installer une version de libphp compatible avec FrankenPHP est d'utiliser les paquets ZTS fournis par [Homebrew PHP](https://github.com/shivammathur/homebrew-php).
|
||||
|
||||
Tout d'abord, si ce n'est déjà fait, installez [Homebrew](https://brew.sh).
|
||||
|
||||
Ensuite, installez la variante ZTS de PHP, Brotli (facultatif, pour la prise en charge de la compression) et watcher (facultatif, pour la détection des modifications de fichiers) :
|
||||
|
||||
```console
|
||||
brew install shivammathur/php/php-zts brotli watcher
|
||||
brew link --overwrite --force shivammathur/php/php-zts
|
||||
```
|
||||
|
||||
### En compilant PHP
|
||||
|
||||
Vous pouvez également compiler PHP à partir des sources avec les options requises par FrankenPHP en suivant ces étapes.
|
||||
|
||||
Tout d'abord, [téléchargez les sources de PHP](https://www.php.net/downloads.php) et extrayez-les :
|
||||
|
||||
```console
|
||||
@@ -32,10 +49,10 @@ Les options de configuration suivantes sont nécessaires pour la compilation, ma
|
||||
|
||||
### Mac
|
||||
|
||||
Utilisez le gestionnaire de paquets [Homebrew](https://brew.sh/) pour installer `libiconv`, `bison`, `re2c` et `pkg-config` :
|
||||
Utilisez le gestionnaire de paquets [Homebrew](https://brew.sh/) pour installer les dépendances obligatoires et optionnelles :
|
||||
|
||||
```console
|
||||
brew install libiconv bison re2c pkg-config
|
||||
brew install libiconv bison brotli re2c pkg-config watcher
|
||||
echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
|
||||
```
|
||||
|
||||
@@ -43,16 +60,14 @@ Puis exécutez le script de configuration :
|
||||
|
||||
```console
|
||||
./configure \
|
||||
--enable-embed=static \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--disable-opcache-jit \
|
||||
--enable-static \
|
||||
--enable-shared=no \
|
||||
--with-iconv=/opt/homebrew/opt/libiconv/
|
||||
```
|
||||
|
||||
## Compilez PHP
|
||||
### Compilez PHP
|
||||
|
||||
Finalement, compilez et installez PHP :
|
||||
|
||||
@@ -61,30 +76,35 @@ make -j"$(getconf _NPROCESSORS_ONLN)"
|
||||
sudo make install
|
||||
```
|
||||
|
||||
## Installez les dépendances optionnelles
|
||||
|
||||
Certaines fonctionnalités de FrankenPHP nécessitent des dépendances optionnelles qui doivent être installées.
|
||||
Ces fonctionnalités peuvent également être désactivées en passant des tags de compilation au compilateur Go.
|
||||
|
||||
| Fonctionnalité | Dépendance | Tag de compilation pour la désactiver |
|
||||
| ------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------- |
|
||||
| Compression Brotli | [Brotli](https://github.com/google/brotli) | nobrotli |
|
||||
| Redémarrage des workers en cas de changement de fichier | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
|
||||
|
||||
## Compiler l'application Go
|
||||
|
||||
Vous pouvez maintenant compiler FrankenPHP :
|
||||
|
||||
```console
|
||||
curl -L https://github.com/dunglas/frankenphp/archive/refs/heads/main.tar.gz | tar xz
|
||||
cd frankenphp-main/caddy/frankenphp
|
||||
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build
|
||||
```
|
||||
|
||||
### Utiliser xcaddy
|
||||
|
||||
Alternativement, vous pouvez utiliser [xcaddy](https://github.com/caddyserver/xcaddy) pour compiler FrankenPHP avec [des modules Caddy additionnels](https://caddyserver.com/docs/modules/) :
|
||||
La méthode recommandée consiste à utiliser [xcaddy](https://github.com/caddyserver/xcaddy) pour compiler FrankenPHP.
|
||||
`xcaddy` permet également d'ajouter facilement des [modules Caddy personnalisés](https://caddyserver.com/docs/modules/) et des extensions FrankenPHP :
|
||||
|
||||
```console
|
||||
CGO_ENABLED=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'" \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
||||
CGO_CFLAGS=$(php-config --includes) \
|
||||
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
||||
xcaddy build \
|
||||
--output frankenphp \
|
||||
--with github.com/dunglas/frankenphp/caddy \
|
||||
--with github.com/dunglas/caddy-cbrotli \
|
||||
--with github.com/dunglas/mercure/caddy \
|
||||
--with github.com/dunglas/vulcain/caddy
|
||||
# Add extra Caddy modules here
|
||||
# Ajoutez les modules Caddy supplémentaires et les extensions FrankenPHP ici
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
@@ -96,3 +116,13 @@ xcaddy build \
|
||||
> Pour ce faire, modifiez la variable d'environnement `XCADDY_GO_BUILD_FLAGS` en quelque chose comme
|
||||
> `XCADDY_GO_BUILD_FLAGS=$'-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'`
|
||||
> (modifiez la valeur de la taille de la pile selon les besoins de votre application).
|
||||
|
||||
### Sans xcaddy
|
||||
|
||||
Il est également possible de compiler FrankenPHP sans `xcaddy` en utilisant directement la commande `go` :
|
||||
|
||||
```console
|
||||
curl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz
|
||||
cd frankenphp-main/caddy/frankenphp
|
||||
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build -tags=nobadger,nomysql,nopgx
|
||||
```
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
FrankenPHP, Caddy ainsi que les modules Mercure et Vulcain peuvent être configurés en utilisant [les formats pris en charge par Caddy](https://caddyserver.com/docs/getting-started#your-first-config).
|
||||
|
||||
Dans [les images Docker](docker.md), le `Caddyfile` est situé dans `/etc/caddy/Caddyfile`.
|
||||
Dans [les images Docker](docker.md), le `Caddyfile` est situé dans `/etc/frankenphp/Caddyfile`.
|
||||
Le binaire statique cherchera le `Caddyfile` dans le répertoire dans lequel il est démarré.
|
||||
|
||||
PHP lui-même peut être configuré [en utilisant un fichier `php.ini`](https://www.php.net/manual/fr/configuration.file.php).
|
||||
|
||||
Par défaut, le PHP fourni avec les images Docker et celui inclus dans le binaire statique cherchera un fichier `php.ini` dans le répertoire dans lequel FrankenPHP est démarré et dans `/usr/local/etc/php/`. Ils chargeront également tous les fichiers se terminant par `.ini` dans `/usr/local/etc/php/conf.d/`.
|
||||
L'interpréteur PHP cherchera dans les emplacements suivants :
|
||||
|
||||
Aucun fichier `php.ini` n'est présent par défaut, vous devriez copier un modèle officiel fourni par le projet PHP.
|
||||
Sur Docker, les modèles sont fournis dans les images :
|
||||
Docker :
|
||||
|
||||
- php.ini : `/usr/local/etc/php/php.ini` Aucun php.ini n'est fourni par défaut.
|
||||
- fichiers de configuration supplémentaires : `/usr/local/etc/php/conf.d/*.ini`
|
||||
- extensions php : `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`
|
||||
- Vous devriez copier un modèle officiel fourni par le projet PHP :
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
@@ -21,40 +26,53 @@ RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
|
||||
RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
|
||||
```
|
||||
|
||||
Si vous n'utilisez pas Docker, copiez l'un des fichiers `php.ini-production` ou `php.ini-development` fournis [dans les sources de PHP](https://github.com/php/php-src/).
|
||||
Installation de FrankenPHP (.rpm ou .deb) :
|
||||
|
||||
- php.ini : `/etc/frankenphp/php.ini` Un fichier php.ini avec des préréglages de production est fourni par défaut.
|
||||
- fichiers de configuration supplémentaires : `/etc/frankenphp/php.d/*.ini`
|
||||
- extensions php : `/usr/lib/frankenphp/modules/`
|
||||
|
||||
Binaire statique :
|
||||
|
||||
- php.ini : Le répertoire dans lequel `frankenphp run` ou `frankenphp php-server` est exécuté, puis `/etc/frankenphp/php.ini`
|
||||
- fichiers de configuration supplémentaires : `/etc/frankenphp/php.d/*.ini`
|
||||
- extensions php : ne peuvent pas être chargées
|
||||
- copiez l'un des fichiers `php.ini-production` ou `php.ini-development` fournis [dans les sources de PHP](https://github.com/php/php-src/).
|
||||
|
||||
## Configuration du Caddyfile
|
||||
|
||||
Pour enregistrer l'exécutable de FrankenPHP, l'[option globale](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp` doit être définie, puis les [directives HTTP](https://caddyserver.com/docs/caddyfile/concepts#directives) `php_server` ou `php` peuvent être utilisées dans les blocs de site pour servir votre application PHP.
|
||||
Les [directives HTTP](https://caddyserver.com/docs/caddyfile/concepts#directives) `php_server` ou `php` peuvent être utilisées dans les blocs de site pour servir votre application PHP.
|
||||
|
||||
Exemple minimal :
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
# Activer FrankenPHP
|
||||
frankenphp
|
||||
}
|
||||
|
||||
localhost {
|
||||
# Activer la compression (optionnel)
|
||||
encode zstd br gzip
|
||||
# Exécuter les fichiers PHP dans le répertoire courant et servir les assets
|
||||
php_server
|
||||
# Activer la compression (optionnel)
|
||||
encode zstd br gzip
|
||||
# Exécuter les fichiers PHP dans le répertoire courant et servir les assets
|
||||
php_server
|
||||
}
|
||||
```
|
||||
|
||||
En option, le nombre de threads à créer et les [workers](worker.md) à démarrer avec le serveur peuvent être spécifiés sous l'option globale.
|
||||
Vous pouvez également configurer explicitement FrankenPHP en utilisant l'option globale :
|
||||
L'[option globale](https://caddyserver.com/docs/caddyfile/concepts#global-options) `frankenphp` peut être utilisée pour configurer FrankenPHP.
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
num_threads <num_threads> # Définit le nombre de threads PHP à démarrer. Par défaut : 2x le nombre de CPUs disponibles.
|
||||
worker {
|
||||
file <path> # Définit le chemin vers le script worker.
|
||||
num <num> # Définit le nombre de threads PHP à démarrer, par défaut 2x le nombre de CPUs disponibles.
|
||||
env <key> <value> # Définit une variable d'environnement supplémentaire avec la valeur donnée. Peut être spécifié plusieurs fois pour régler plusieurs variables d'environnement.
|
||||
}
|
||||
}
|
||||
frankenphp {
|
||||
num_threads <num_threads> # Définit le nombre de threads PHP à démarrer. Par défaut : 2x le nombre de CPUs disponibles.
|
||||
max_threads <num_threads> # Limite le nombre de threads PHP supplémentaires qui peuvent être démarrés au moment de l'exécution. Valeur par défaut : num_threads. Peut être mis à 'auto'.
|
||||
max_wait_time <duration> # Définit le temps maximum pendant lequel une requête peut attendre un thread PHP libre avant d'être interrompue. Valeur par défaut : désactivé.
|
||||
php_ini <key> <value> Définit une directive php.ini. Peut être utilisé plusieurs fois pour définir plusieurs directives.
|
||||
worker {
|
||||
file <path> # Définit le chemin vers le script worker.
|
||||
num <num> # Définit le nombre de threads PHP à démarrer, par défaut 2x le nombre de CPUs disponibles.
|
||||
env <key> <value> # Définit une variable d'environnement supplémentaire avec la valeur donnée. Peut être spécifié plusieurs fois pour régler plusieurs variables d'environnement.
|
||||
watch <path> # Définit le chemin d'accès à surveiller pour les modifications de fichiers. Peut être spécifié plusieurs fois pour plusieurs chemins.
|
||||
name <name> # Définit le nom du worker, utilisé dans les journaux et les métriques. Défaut : chemin absolu du fichier du worker
|
||||
max_consecutive_failures <num> # Définit le nombre maximum d'échecs consécutifs avant que le worker ne soit considéré comme défaillant, -1 signifie que le worker redémarre toujours. Par défaut : 6.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ...
|
||||
@@ -64,9 +82,9 @@ Vous pouvez également utiliser la forme courte de l'option worker en une seule
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker <file> <num>
|
||||
}
|
||||
frankenphp {
|
||||
worker <file> <num>
|
||||
}
|
||||
}
|
||||
|
||||
# ...
|
||||
@@ -75,21 +93,20 @@ Vous pouvez également utiliser la forme courte de l'option worker en une seule
|
||||
Vous pouvez aussi définir plusieurs workers si vous servez plusieurs applications sur le même serveur :
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker /path/to/app/public/index.php <num>
|
||||
worker /path/to/other/public/index.php <num>
|
||||
}
|
||||
}
|
||||
|
||||
app.example.com {
|
||||
root * /path/to/app/public
|
||||
php_server
|
||||
root /path/to/app/public
|
||||
php_server {
|
||||
root /path/to/app/public # permet une meilleure mise en cache
|
||||
worker index.php <num>
|
||||
}
|
||||
}
|
||||
|
||||
other.example.com {
|
||||
root * /path/to/other/public
|
||||
php_server
|
||||
root /path/to/other/public
|
||||
php_server {
|
||||
root /path/to/other/public
|
||||
worker index.php <num>
|
||||
}
|
||||
}
|
||||
|
||||
# ...
|
||||
@@ -97,27 +114,29 @@ other.example.com {
|
||||
|
||||
L'utilisation de la directive `php_server` est généralement suffisante,
|
||||
mais si vous avez besoin d'un contrôle total, vous pouvez utiliser la directive `php`, qui permet un plus grand niveau de finesse dans la configuration.
|
||||
La directive `php` transmet toutes les entrées à PHP, au lieu de vérifier d'abord si
|
||||
c'est un fichier PHP ou pas. En savoir plus à ce sujet dans la [page performances](performance.md#try_files).
|
||||
|
||||
Utiliser la directive `php_server` est équivalent à cette configuration :
|
||||
|
||||
```caddyfile
|
||||
route {
|
||||
# Ajoute un slash final pour les requêtes de répertoire
|
||||
@canonicalPath {
|
||||
file {path}/index.php
|
||||
not path */
|
||||
}
|
||||
redir @canonicalPath {path}/ 308
|
||||
# Si le fichier demandé n'existe pas, essayer les fichiers index
|
||||
@indexFiles file {
|
||||
try_files {path} {path}/index.php index.php
|
||||
split_path .php
|
||||
}
|
||||
rewrite @indexFiles {http.matchers.file.relative}
|
||||
# FrankenPHP!
|
||||
@phpFiles path *.php
|
||||
php @phpFiles
|
||||
file_server
|
||||
# Ajoute un slash final pour les requêtes de répertoire
|
||||
@canonicalPath {
|
||||
file {path}/index.php
|
||||
not path */
|
||||
}
|
||||
redir @canonicalPath {path}/ 308
|
||||
# Si le fichier demandé n'existe pas, essayer les fichiers index
|
||||
@indexFiles file {
|
||||
try_files {path} {path}/index.php index.php
|
||||
split_path .php
|
||||
}
|
||||
rewrite @indexFiles {http.matchers.file.relative}
|
||||
# FrankenPHP!
|
||||
@phpFiles path *.php
|
||||
php @phpFiles
|
||||
file_server
|
||||
}
|
||||
```
|
||||
|
||||
@@ -125,20 +144,103 @@ Les directives `php_server` et `php` disposent des options suivantes :
|
||||
|
||||
```caddyfile
|
||||
php_server [<matcher>] {
|
||||
root <directory> # Définit le dossier racine du le site. Par défaut : valeur de la directive `root` parente.
|
||||
split_path <delim...> # Définit les sous-chaînes pour diviser l'URI en deux parties. La première sous-chaîne correspondante sera utilisée pour séparer le "path info" du chemin. La première partie est suffixée avec la sous-chaîne correspondante et sera considérée comme le nom réel de la ressource (script CGI). La seconde partie sera définie comme PATH_INFO pour utilisation par le script. Par défaut : `.php`
|
||||
resolve_root_symlink false # Désactive la résolution du répertoire `root` vers sa valeur réelle en évaluant un lien symbolique, s'il existe (activé par défaut).
|
||||
env <key> <value> # Définit une variable d'environnement supplémentaire avec la valeur donnée. Peut être spécifié plusieurs fois pour plusieurs variables d'environnement.
|
||||
root <directory> # Définit le dossier racine du le site. Par défaut : valeur de la directive `root` parente.
|
||||
split_path <delim...> # Définit les sous-chaînes pour diviser l'URI en deux parties. La première sous-chaîne correspondante sera utilisée pour séparer le "path info" du chemin. La première partie est suffixée avec la sous-chaîne correspondante et sera considérée comme le nom réel de la ressource (script CGI). La seconde partie sera définie comme PATH_INFO pour utilisation par le script. Par défaut : `.php`
|
||||
resolve_root_symlink false # Désactive la résolution du répertoire `root` vers sa valeur réelle en évaluant un lien symbolique, s'il existe (activé par défaut).
|
||||
env <key> <value> # Définit une variable d'environnement supplémentaire avec la valeur donnée. Peut être spécifié plusieurs fois pour plusieurs variables d'environnement.
|
||||
file_server off # Désactive la directive file_server intégrée.
|
||||
worker { # Crée un worker spécifique à ce serveur. Peut être spécifié plusieurs fois pour plusieurs workers.
|
||||
file <path> # Définit le chemin vers le script worker, peut être relatif à la racine du php_server
|
||||
num <num> # Définit le nombre de threads PHP à démarrer, par défaut 2x le nombre de CPUs disponibles
|
||||
name <name> # Définit le nom du worker, utilisé dans les journaux et les métriques. Défaut : chemin absolu du fichier du worker. Commence toujours par m# lorsqu'il est défini dans un bloc php_server.
|
||||
watch <path> # Définit le chemin d'accès à surveiller pour les modifications de fichiers. Peut être spécifié plusieurs fois pour plusieurs chemins.
|
||||
env <key> <value> # Définit une variable d'environnement supplémentaire avec la valeur donnée. Peut être spécifié plusieurs fois pour plusieurs variables d'environnement. Les variables d'environnement pour ce worker sont également héritées du parent php_server, mais peuvent être écrasées ici.
|
||||
}
|
||||
worker <other_file> <num> # Peut également utiliser la forme courte comme dans le bloc frankenphp global.
|
||||
}
|
||||
```
|
||||
|
||||
### Surveillance des modifications de fichier
|
||||
|
||||
Vu que les workers ne démarrent votre application qu'une seule fois et la gardent en mémoire, toute modification
|
||||
apportée à vos fichiers PHP ne sera pas répercutée immédiatement.
|
||||
|
||||
Les workers peuvent être redémarrés en cas de changement de fichier via la directive `watch`.
|
||||
Ceci est utile pour les environnements de développement.
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker {
|
||||
file /path/to/app/public/worker.php
|
||||
watch
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Si le répertoire `watch` n'est pas précisé, il se rabattra sur `./**/*.{php,yaml,yml,twig,env}`,
|
||||
qui surveille tous les fichiers `.php`, `.yaml`, `.yml`, `.twig` et `.env` dans le répertoire et les sous-répertoires
|
||||
où le processus FrankenPHP a été lancé. Vous pouvez également spécifier un ou plusieurs répertoires via une commande
|
||||
[motif de nom de fichier shell](https://pkg.go.dev/path/filepath#Match) :
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker {
|
||||
file /path/to/app/public/worker.php
|
||||
watch /path/to/app # surveille tous les fichiers dans tous les sous-répertoires de /path/to/app
|
||||
watch /path/to/app/*.php # surveille les fichiers se terminant par .php dans /path/to/app
|
||||
watch /path/to/app/**/*.php # surveille les fichiers PHP dans /path/to/app et les sous-répertoires
|
||||
watch /path/to/app/**/*.{php,twig} # surveille les fichiers PHP et Twig dans /path/to/app et les sous-répertoires
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Le motif `**` signifie une surveillance récursive.
|
||||
- Les répertoires peuvent également être relatifs (depuis l'endroit où le processus FrankenPHP est démarré).
|
||||
- Si vous avez défini plusieurs workers, ils seront tous redémarrés lorsqu'un fichier est modifié.
|
||||
- Méfiez-vous des fichiers créés au moment de l'exécution (comme les logs) car ils peuvent provoquer des redémarrages intempestifs du worker.
|
||||
|
||||
La surveillance des fichiers est basé sur [e-dant/watcher](https://github.com/e-dant/watcher).
|
||||
|
||||
### Full Duplex (HTTP/1)
|
||||
|
||||
Lors de l'utilisation de HTTP/1.x, il peut être souhaitable d'activer le mode full-duplex pour permettre l'écriture d'une réponse avant que le corps entier
|
||||
n'ait été lu. (par exemple : WebSocket, événements envoyés par le serveur, etc.)
|
||||
|
||||
Il s'agit d'une configuration optionnelle qui doit être ajoutée aux options globales dans le fichier `Caddyfile` :
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
servers {
|
||||
enable_full_duplex
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> L'activation de cette option peut entraîner un blocage (deadlock) des anciens clients HTTP/1.x qui ne supportent pas le full-duplex.
|
||||
> Cela peut aussi être configuré en utilisant la variable d'environnement `CADDY_GLOBAL_OPTIONS` :
|
||||
|
||||
```sh
|
||||
CADDY_GLOBAL_OPTIONS="servers {
|
||||
enable_full_duplex
|
||||
}"
|
||||
```
|
||||
|
||||
Vous trouverez plus d'informations sur ce paramètre dans la [documentation Caddy](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex).
|
||||
|
||||
## Variables d'environnement
|
||||
|
||||
Les variables d'environnement suivantes peuvent être utilisées pour insérer des directives Caddy dans le `Caddyfile` sans le modifier :
|
||||
|
||||
* `SERVER_NAME` : change [les adresses sur lesquelles écouter](https://caddyserver.com/docs/caddyfile/concepts#addresses), les noms d'hôte fournis seront également utilisés pour le certificat TLS généré
|
||||
* `CADDY_GLOBAL_OPTIONS` : injecte [des options globales](https://caddyserver.com/docs/caddyfile/options)
|
||||
* `FRANKENPHP_CONFIG` : insère la configuration sous la directive `frankenphp`
|
||||
- `SERVER_NAME` : change [les adresses sur lesquelles écouter](https://caddyserver.com/docs/caddyfile/concepts#addresses), les noms d'hôte fournis seront également utilisés pour le certificat TLS généré
|
||||
- `SERVER_ROOT` : change le répertoire racine du site, par défaut `public/`
|
||||
- `CADDY_GLOBAL_OPTIONS` : injecte [des options globales](https://caddyserver.com/docs/caddyfile/options)
|
||||
- `FRANKENPHP_CONFIG` : insère la configuration sous la directive `frankenphp`
|
||||
|
||||
Comme pour les SAPI FPM et CLI, les variables d'environnement ne sont exposées par défaut dans la superglobale `$_SERVER`.
|
||||
|
||||
@@ -146,9 +248,27 @@ La valeur `S` de [la directive `variables_order` de PHP](https://www.php.net/man
|
||||
|
||||
## Configuration PHP
|
||||
|
||||
Pour charger [des fichiers de configuration PHP supplémentaires](https://www.php.net/manual/fr/configuration.file.php#configuration.file.scan), la variable d'environnement `PHP_INI_SCAN_DIR` peut être utilisée.
|
||||
Pour charger [des fichiers de configuration PHP supplémentaires](https://www.php.net/manual/fr/configuration.file.php#configuration.file.scan),
|
||||
la variable d'environnement `PHP_INI_SCAN_DIR` peut être utilisée.
|
||||
Lorsqu'elle est définie, PHP chargera tous les fichiers avec l'extension `.ini` présents dans les répertoires donnés.
|
||||
|
||||
Vous pouvez également modifier la configuration de PHP en utilisant la directive `php_ini` dans le fichier `Caddyfile` :
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
php_ini memory_limit 256M
|
||||
|
||||
# or
|
||||
|
||||
php_ini {
|
||||
memory_limit 256M
|
||||
max_execution_time 15
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Activer le mode debug
|
||||
|
||||
Lors de l'utilisation de l'image Docker, définissez la variable d'environnement `CADDY_GLOBAL_OPTIONS` sur `debug` pour activer le mode debug :
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
|
||||
Les images Docker de [FrankenPHP](https://hub.docker.com/r/dunglas/frankenphp) sont basées sur les [images PHP officielles](https://hub.docker.com/_/php/). Des variantes Debian et Alpine Linux sont fournies pour les architectures populaires. Les variantes Debian sont recommandées.
|
||||
|
||||
Des variantes pour PHP 8.2, 8.3 et 8.4 sont disponibles. [Parcourir les tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
|
||||
Des variantes pour PHP 8.2, 8.3, 8.4 et 8.5 sont disponibles. [Parcourir les tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
|
||||
|
||||
Les tags suivent le pattern suivant: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`
|
||||
|
||||
- `<frankenphp-version>` et `<php-version>` sont repsectivement les numéros de version de FrankenPHP et PHP, allant de majeur (e.g. `1`), mineur (e.g. `1.2`) à des versions correctives (e.g. `1.2.3`).
|
||||
- `<os>` est soit `trixie` (pour Debian Trixie), `bookworm` (pour Debian Bookworm) ou `alpine` (pour la dernière version stable d'Alpine).
|
||||
|
||||
[Parcourir les tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
|
||||
|
||||
## Comment utiliser les images
|
||||
|
||||
@@ -167,12 +174,12 @@ FROM dunglas/frankenphp
|
||||
|
||||
ARG USER=appuser
|
||||
|
||||
RUN
|
||||
RUN \
|
||||
# Utiliser "adduser -D ${USER}" pour les distros basées sur Alpine
|
||||
useradd ${USER};
|
||||
# Supprimer la capacité par défaut
|
||||
useradd ${USER}; \
|
||||
# Supprimer la capacité par défaut \
|
||||
setcap -r /usr/local/bin/frankenphp; \
|
||||
# Donner un accès en écriture à /data/caddy et /config/caddy
|
||||
# Donner un accès en écriture à /data/caddy et /config/caddy \
|
||||
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy
|
||||
|
||||
USER ${USER}
|
||||
@@ -185,8 +192,8 @@ Exemple `:8000`
|
||||
|
||||
Les images Docker sont construites :
|
||||
|
||||
* lorsqu'une nouvelle version est taguée
|
||||
* tous les jours à 4h UTC, si de nouvelles versions des images officielles PHP sont disponibles
|
||||
- lorsqu'une nouvelle version est taguée
|
||||
- tous les jours à 4h UTC, si de nouvelles versions des images officielles PHP sont disponibles
|
||||
|
||||
## Versions de développement
|
||||
|
||||
|
||||
@@ -6,16 +6,18 @@ Grâce à cette fonctionnalité, les applications PHP peuvent être distribuées
|
||||
|
||||
Pour en savoir plus sur cette fonctionnalité, consultez [la présentation faite par Kévin à la SymfonyCon 2023](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/).
|
||||
|
||||
Pour embarquer des applications Laravel, [lisez ce point spécifique de la documentation](laravel.md#les-applications-laravel-en-tant-que-binaires-autonomes).
|
||||
|
||||
## Préparer votre application
|
||||
|
||||
Avant de créer le binaire autonome, assurez-vous que votre application est prête à être intégrée.
|
||||
|
||||
Vous devrez probablement :
|
||||
|
||||
* Installer les dépendances de production de l'application
|
||||
* Dumper l'autoloader
|
||||
* Activer le mode production de votre application (si disponible)
|
||||
* Supprimer les fichiers inutiles tels que `.git` ou les tests pour réduire la taille de votre binaire final
|
||||
- Installer les dépendances de production de l'application
|
||||
- Dumper l'autoloader
|
||||
- Activer le mode production de votre application (si disponible)
|
||||
- Supprimer les fichiers inutiles tels que `.git` ou les tests pour réduire la taille de votre binaire final
|
||||
|
||||
Par exemple, pour une application Symfony, lancez les commandes suivantes :
|
||||
|
||||
@@ -29,7 +31,8 @@ cd $TMPDIR/my-prepared-app
|
||||
echo APP_ENV=prod > .env.local
|
||||
echo APP_DEBUG=0 >> .env.local
|
||||
|
||||
# Supprimer les tests
|
||||
# Supprimer les tests et autres fichiers inutiles pour économiser de l'espace
|
||||
# Alternativement, ajoutez ces fichiers avec l'attribut export-ignore dans votre fichier .gitattributes
|
||||
rm -Rf tests/
|
||||
|
||||
# Installer les dépendances
|
||||
@@ -52,36 +55,34 @@ La manière la plus simple de créer un binaire Linux est d'utiliser le builder
|
||||
|
||||
1. Créez un fichier nommé `static-build.Dockerfile` dans le répertoire de votre application préparée :
|
||||
|
||||
```dockerfile
|
||||
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
|
||||
```dockerfile
|
||||
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
|
||||
# Si vous envisagez d'exécuter le binaire sur des systèmes musl-libc, utilisez plutôt static-builder-musl
|
||||
|
||||
# Copy your app
|
||||
WORKDIR /go/src/app/dist/app
|
||||
COPY . .
|
||||
# Copy your app
|
||||
WORKDIR /go/src/app/dist/app
|
||||
COPY . .
|
||||
|
||||
# Build the static binary, be sure to select only the PHP extensions you want
|
||||
WORKDIR /go/src/app/
|
||||
RUN EMBED=dist/app/ \
|
||||
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
|
||||
./build-static.sh
|
||||
```
|
||||
WORKDIR /go/src/app/
|
||||
RUN EMBED=dist/app/ ./build-static.sh
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Certains fichiers `.dockerignore` (par exemple celui fourni par défaut par [Symfony Docker](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))
|
||||
> empêchent la copie du dossier `vendor/` et des fichiers `.env`. Assurez-vous d'ajuster ou de supprimer le fichier `.dockerignore` avant le build.
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Certains fichiers `.dockerignore` (par exemple celui fourni par défaut par [Symfony Docker](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))
|
||||
> empêchent la copie du dossier `vendor/` et des fichiers `.env`. Assurez-vous d'ajuster ou de supprimer le fichier `.dockerignore` avant le build.
|
||||
|
||||
2. Construisez:
|
||||
|
||||
```console
|
||||
docker build -t static-app -f static-build.Dockerfile .
|
||||
```
|
||||
```console
|
||||
docker build -t static-app -f static-build.Dockerfile .
|
||||
```
|
||||
|
||||
3. Extrayez le binaire :
|
||||
|
||||
```console
|
||||
docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp
|
||||
```
|
||||
```console
|
||||
docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp
|
||||
```
|
||||
|
||||
Le binaire généré sera nommé `my-app` dans le répertoire courant.
|
||||
|
||||
@@ -90,11 +91,9 @@ Le binaire généré sera nommé `my-app` dans le répertoire courant.
|
||||
Si vous ne souhaitez pas utiliser Docker, ou souhaitez construire un binaire macOS, utilisez le script shell que nous fournissons :
|
||||
|
||||
```console
|
||||
git clone https://github.com/dunglas/frankenphp
|
||||
git clone https://github.com/php/frankenphp
|
||||
cd frankenphp
|
||||
EMBED=/path/to/your/app \
|
||||
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
|
||||
./build-static.sh
|
||||
EMBED=/path/to/your/app ./build-static.sh
|
||||
```
|
||||
|
||||
Le binaire obtenu est le fichier nommé `frankenphp-<os>-<arch>` dans le répertoire `dist/`.
|
||||
@@ -127,6 +126,19 @@ Vous pouvez également exécuter les scripts CLI PHP incorporés dans votre bina
|
||||
./my-app php-cli bin/console
|
||||
```
|
||||
|
||||
## Extensions PHP
|
||||
|
||||
Par défaut, le script construira les extensions requises par le fichier `composer.json` de votre projet, s'il y en a.
|
||||
Si le fichier `composer.json` n'existe pas, les extensions par défaut sont construites, comme documenté dans [Créer un binaire statique](static.md).
|
||||
|
||||
Pour personnaliser les extensions, utilisez la variable d'environnement `PHP_EXTENSIONS`.
|
||||
|
||||
```console
|
||||
EMBED=/path/to/your/app \
|
||||
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
|
||||
./build-static.sh
|
||||
```
|
||||
|
||||
## Personnaliser la compilation
|
||||
|
||||
[Consultez la documentation sur la compilation statique](static.md) pour voir comment personnaliser le binaire (extensions, version PHP...).
|
||||
|
||||
886
docs/fr/extensions.md
Normal file
886
docs/fr/extensions.md
Normal file
@@ -0,0 +1,886 @@
|
||||
# Écrire des extensions PHP en Go
|
||||
|
||||
Avec FrankenPHP, vous pouvez **écrire des extensions PHP en Go**, ce qui vous permet de créer des **fonctions natives haute performance** qui peuvent être appelées directement depuis PHP. Vos applications peuvent tirer parti de toute bibliothèque Go existante ou nouvelle, ainsi que du célèbre modèle de concurrence des **goroutines directement depuis votre code PHP**.
|
||||
|
||||
L'écriture d'extensions PHP se fait généralement en C, mais il est également possible de les écrire dans d'autres langages avec un peu de travail supplémentaire. Les extensions PHP permettent de tirer parti de la puissance des langages de bas niveau pour étendre les fonctionnalités de PHP, par exemple, en ajoutant des fonctions natives ou en optimisant des opérations spécifiques.
|
||||
|
||||
Grâce aux modules Caddy, vous pouvez écrire des extensions PHP en Go et les intégrer très rapidement dans FrankenPHP.
|
||||
|
||||
## Deux Approches
|
||||
|
||||
FrankenPHP offre deux façons de créer des extensions PHP en Go :
|
||||
|
||||
1. **Utilisation du Générateur d'Extensions** - L'approche recommandée qui génère tout le code standard nécessaire pour la plupart des cas d'usage, vous permettant de vous concentrer sur l'écriture de votre code Go
|
||||
2. **Implémentation Manuelle** - Contrôle total sur la structure de l'extension pour les cas d'usage avancés
|
||||
|
||||
Nous commencerons par l'approche du générateur, car c'est le moyen le plus facile de commencer, puis nous montrerons l'implémentation manuelle pour ceux qui ont besoin d'un contrôle complet.
|
||||
|
||||
## Utilisation du Générateur d'Extensions
|
||||
|
||||
FrankenPHP est livré avec un outil qui vous permet de **créer une extension PHP** en utilisant uniquement Go. **Pas besoin d'écrire du code C** ou d'utiliser CGO directement : FrankenPHP inclut également une **API de types publique** pour vous aider à écrire vos extensions en Go sans avoir à vous soucier du **jonglage de types entre PHP/C et Go**.
|
||||
|
||||
> [!TIP]
|
||||
> Si vous voulez comprendre comment les extensions peuvent être écrites en Go à partir de zéro, vous pouvez lire la section d'implémentation manuelle ci-dessous démontrant comment écrire une extension PHP en Go sans utiliser le générateur.
|
||||
|
||||
Gardez à l'esprit que cet outil n'est **pas un générateur d'extensions complet**. Il est destiné à vous aider à écrire des extensions simples en Go, mais il ne fournit pas les fonctionnalités les plus avancées des extensions PHP. Si vous devez écrire une extension plus **complexe et optimisée**, vous devrez peut-être écrire du code C ou utiliser CGO directement.
|
||||
|
||||
### Prérequis
|
||||
|
||||
Comme aussi couvert dans la section d'implémentation manuelle ci-dessous, vous devez [obtenir les sources PHP](https://www.php.net/downloads.php) et créer un nouveau module Go.
|
||||
|
||||
#### Créer un Nouveau Module et Obtenir les Sources PHP
|
||||
|
||||
La première étape pour écrire une extension PHP en Go est de créer un nouveau module Go. Vous pouvez utiliser la commande suivante pour cela :
|
||||
|
||||
```console
|
||||
go mod init github.com/my-account/my-module
|
||||
```
|
||||
|
||||
La seconde étape est [l'obtention des sources PHP](https://www.php.net/downloads.php) pour les étapes suivantes. Une fois que vous les avez, décompressez-les dans le répertoire de votre choix, mais pas à l'intérieur de votre module Go :
|
||||
|
||||
```console
|
||||
tar xf php-*
|
||||
```
|
||||
|
||||
### Écrire l'Extension
|
||||
|
||||
Tout est maintenant configuré pour écrire votre fonction native en Go. Créez un nouveau fichier nommé `stringext.go`. Notre première fonction prendra une chaîne comme argument, le nombre de fois à la répéter, un booléen pour indiquer s'il faut inverser la chaîne, et retournera la chaîne résultante. Cela devrait ressembler à ceci :
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
|
||||
func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(s))
|
||||
|
||||
result := strings.Repeat(str, int(count))
|
||||
if reverse {
|
||||
runes := []rune(result)
|
||||
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
|
||||
runes[i], runes[j] = runes[j], runes[i]
|
||||
}
|
||||
result = string(runes)
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(result, false)
|
||||
}
|
||||
```
|
||||
|
||||
Il y a deux choses importantes à noter ici :
|
||||
|
||||
- Une directive `//export_php:function` définit la signature de la fonction en PHP. C'est ainsi que le générateur sait comment générer la fonction PHP avec les bons paramètres et le bon type de retour ;
|
||||
- La fonction doit retourner un `unsafe.Pointer`. FrankenPHP fournit une API pour vous aider avec le jonglage de types entre C et Go.
|
||||
|
||||
Alors que le premier point parle de lui-même, le second peut être plus difficile à appréhender. Plongeons plus profondément dans le jonglage de types dans la section suivante.
|
||||
|
||||
### Jonglage de Types
|
||||
|
||||
Bien que certains types de variables aient la même représentation mémoire entre C/PHP et Go, certains types nécessitent plus de logique pour être directement utilisés. C'est peut-être la partie la plus difficile quand il s'agit d'écrire des extensions car cela nécessite de comprendre les fonctionnements internes du moteur Zend et comment les variables sont stockées dans le moteur de PHP. Ce tableau résume ce que vous devez savoir :
|
||||
|
||||
| Type PHP | Type Go | Conversion directe | Assistant C vers Go | Assistant Go vers C | Support des Méthodes de Classe |
|
||||
| ------------------ | ----------------------------- | ------------------ | --------------------------------- | ---------------------------------- | ------------------------------ |
|
||||
| `int` | `int64` | ✅ | - | - | ✅ |
|
||||
| `?int` | `*int64` | ✅ | - | - | ✅ |
|
||||
| `float` | `float64` | ✅ | - | - | ✅ |
|
||||
| `?float` | `*float64` | ✅ | - | - | ✅ |
|
||||
| `bool` | `bool` | ✅ | - | - | ✅ |
|
||||
| `?bool` | `*bool` | ✅ | - | - | ✅ |
|
||||
| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
|
||||
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
|
||||
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
|
||||
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
|
||||
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
|
||||
| `object` | `struct` | ❌ | _Pas encore implémenté_ | _Pas encore implémenté_ | ❌ |
|
||||
|
||||
> [!NOTE]
|
||||
> Ce tableau n'est pas encore exhaustif et sera complété au fur et à mesure que l'API de types FrankenPHP deviendra plus complète.
|
||||
>
|
||||
> Pour les méthodes de classe spécifiquement, les types primitifs et les tableaux sont supportés. Les objets ne peuvent pas encore être utilisés comme paramètres de méthode ou types de retour.
|
||||
|
||||
Si vous vous référez à l'extrait de code de la section précédente, vous pouvez voir que des assistants sont utilisés pour convertir le premier paramètre et la valeur de retour. Les deuxième et troisième paramètres de notre fonction `repeat_this()` n'ont pas besoin d'être convertis car la représentation mémoire des types sous-jacents est la même pour C et Go.
|
||||
|
||||
#### Travailler avec les Tableaux
|
||||
|
||||
FrankenPHP fournit un support natif pour les tableaux PHP à travers `frankenphp.AssociativeArray` ou une conversion directe vers une map ou un slice.
|
||||
|
||||
`AssociativeArray` représente une [hash map](https://fr.wikipedia.org/wiki/Table_de_hachage) composée d'un champ `Map: map[string]any` et d'un champ optionnel `Order: []string` (contrairement aux "tableaux associatifs" PHP, les maps Go ne sont pas ordonnées).
|
||||
|
||||
Si l'ordre ou l'association ne sont pas nécessaires, il est également possible de convertir directement vers un slice `[]any` ou une map non ordonnée `map[string]any`.
|
||||
|
||||
**Créer et manipuler des tableaux en Go :**
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
// export_php:function process_data_ordered(array $input): array
|
||||
func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
|
||||
// Convertir le tableau associatif PHP vers Go en conservant l'ordre
|
||||
associativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
// gérer l'erreur
|
||||
}
|
||||
|
||||
// parcourir les entrées dans l'ordre
|
||||
for _, key := range associativeArray.Order {
|
||||
value, _ = associativeArray.Map[key]
|
||||
// faire quelque chose avec key et value
|
||||
}
|
||||
|
||||
// retourner un tableau ordonné
|
||||
// si 'Order' n'est pas vide, seules les paires clé-valeur dans 'Order' seront respectées
|
||||
return frankenphp.PHPAssociativeArray[string](frankenphp.AssociativeArray[string]{
|
||||
Map: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
Order: []string{"key1", "key2"},
|
||||
})
|
||||
}
|
||||
|
||||
// export_php:function process_data_unordered(array $input): array
|
||||
func process_data_unordered_map(arr *C.zval) unsafe.Pointer {
|
||||
// Convertir le tableau associatif PHP vers une map Go sans conserver l'ordre
|
||||
// ignorer l'ordre sera plus performant
|
||||
goMap, err := frankenphp.GoMap[any](unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
// gérer l'erreur
|
||||
}
|
||||
|
||||
// parcourir les entrées sans ordre spécifique
|
||||
for key, value := range goMap {
|
||||
// faire quelque chose avec key et value
|
||||
}
|
||||
|
||||
// retourner un tableau non ordonné
|
||||
return frankenphp.PHPMap(map[string]string {
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
})
|
||||
}
|
||||
|
||||
// export_php:function process_data_packed(array $input): array
|
||||
func process_data_packed(arr *C.zval) unsafe.Pointer {
|
||||
// Convertir le tableau packed PHP vers Go
|
||||
goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
// gérer l'erreur
|
||||
}
|
||||
|
||||
// parcourir le slice dans l'ordre
|
||||
for index, value := range goSlice {
|
||||
// faire quelque chose avec index et value
|
||||
}
|
||||
|
||||
// retourner un tableau packed
|
||||
return frankenphp.PHPPackedArray([]string{"value1", "value2", "value3"})
|
||||
}
|
||||
```
|
||||
|
||||
**Fonctionnalités clés de la conversion de tableaux :**
|
||||
|
||||
- **Paires clé-valeur ordonnées** - Option pour conserver l'ordre du tableau associatif
|
||||
- **Optimisé pour plusieurs cas** - Option de ne pas conserver l'ordre pour de meilleures performances ou conversion directe vers un slice
|
||||
- **Détection automatique de liste** - Lors de la conversion vers PHP, détecte automatiquement si le tableau doit être une liste packed ou un hashmap
|
||||
- **Tableaux imbriqués** - Les tableaux peuvent être imbriqués et convertiront automatiquement tous les types supportés (`int64`, `float64`, `string`, `bool`, `nil`, `AssociativeArray`, `map[string]any`, `[]any`)
|
||||
- **Les objets ne sont pas supportés** - Actuellement, seuls les types scalaires et les tableaux peuvent être utilisés comme valeurs. Fournir un objet résultera en une valeur `null` dans le tableau PHP.
|
||||
|
||||
##### Méthodes disponibles : Packed et Associatif
|
||||
|
||||
- `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convertir vers un tableau PHP ordonné avec des paires clé-valeur
|
||||
- `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convertir une map vers un tableau PHP non ordonné avec des paires clé-valeur
|
||||
- `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convertir un slice vers un tableau PHP packed avec uniquement des valeurs indexées
|
||||
- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convertir un tableau PHP vers un `AssociativeArray` Go ordonné (map avec ordre)
|
||||
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convertir un tableau PHP vers une map Go non ordonnée
|
||||
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convertir un tableau PHP vers un slice Go
|
||||
- `frankenphp.IsPacked(zval *C.zend_array) bool` - Vérifie si le tableau PHP est une liste ou un tableau associatif
|
||||
|
||||
### Travailler avec des Callables
|
||||
|
||||
FrankenPHP propose un moyen de travailler avec les _callables_ PHP grâce au helper `frankenphp.CallPHPCallable()`. Cela permet d'appeler des fonctions ou des méthodes PHP depuis du code Go.
|
||||
|
||||
Pour illustrer cela, créons notre propre fonction `array_map()` qui prend un _callable_ et un tableau, applique le _callable_ à chaque élément du tableau, et retourne un nouveau tableau avec les résultats :
|
||||
|
||||
```go
|
||||
// export_php:function my_array_map(array $data, callable $callback): array
|
||||
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
|
||||
goSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result := make([]any, len(goSlice))
|
||||
|
||||
for index, value := range goSlice {
|
||||
result[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
|
||||
}
|
||||
|
||||
return frankenphp.PHPPackedArray(result)
|
||||
}
|
||||
```
|
||||
|
||||
Remarquez comment nous utilisons `frankenphp.CallPHPCallable()` pour appeler le _callable_ PHP passé en paramètre. Cette fonction prend un pointeur vers le _callable_ et un tableau d'arguments, et elle retourne le résultat de l'exécution du _callable_. Vous pouvez utiliser la syntaxe habituelle des _callables_ :
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
|
||||
// $result vaudra [2, 4, 6]
|
||||
|
||||
$result = my_array_map(['hello', 'world'], 'strtoupper');
|
||||
// $result vaudra ['HELLO', 'WORLD']
|
||||
```
|
||||
|
||||
### Déclarer une Classe PHP Native
|
||||
|
||||
Le générateur prend en charge la déclaration de **classes opaques** comme structures Go, qui peuvent être utilisées pour créer des objets PHP. Vous pouvez utiliser la directive `//export_php:class` pour définir une classe PHP. Par exemple :
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
```
|
||||
|
||||
#### Que sont les Classes Opaques ?
|
||||
|
||||
Les **classes opaques** sont des classes avec lesquelles la structure interne (comprendre : les propriétés) est cachée du code PHP. Cela signifie :
|
||||
|
||||
- **Pas d'accès direct aux propriétés** : Vous ne pouvez pas lire ou écrire des propriétés directement depuis PHP (`$user->name` ne fonctionnera pas)
|
||||
- **Interface uniquement par méthodes** - Toutes les interactions doivent passer par les méthodes que vous définissez
|
||||
- **Meilleure encapsulation** - La structure de données interne est complètement contrôlée par le code Go
|
||||
- **Sécurité de type** - Aucun risque que le code PHP corrompe l'état interne avec de mauvais types
|
||||
- **API plus propre** - Force à concevoir une interface publique appropriée
|
||||
|
||||
Cette approche fournit une meilleure encapsulation et empêche le code PHP de corrompre accidentellement l'état interne de vos objets Go. Toutes les interactions avec l'objet doivent passer par les méthodes que vous définissez explicitement.
|
||||
|
||||
#### Ajouter des Méthodes aux Classes
|
||||
|
||||
Puisque les propriétés ne sont pas directement accessibles, vous **devez définir des méthodes** pour interagir avec vos classes opaques. Utilisez la directive `//export_php:method` pour définir cela :
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
//export_php:method User::getName(): string
|
||||
func (us *UserStruct) GetUserName() unsafe.Pointer {
|
||||
return frankenphp.PHPString(us.Name, false)
|
||||
}
|
||||
|
||||
//export_php:method User::setAge(int $age): void
|
||||
func (us *UserStruct) SetUserAge(age int64) {
|
||||
us.Age = int(age)
|
||||
}
|
||||
|
||||
//export_php:method User::getAge(): int
|
||||
func (us *UserStruct) GetUserAge() int64 {
|
||||
return int64(us.Age)
|
||||
}
|
||||
|
||||
//export_php:method User::setNamePrefix(string $prefix = "User"): void
|
||||
func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
|
||||
us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + ": " + us.Name
|
||||
}
|
||||
```
|
||||
|
||||
#### Paramètres Nullables
|
||||
|
||||
Le générateur prend en charge les paramètres nullables en utilisant le préfixe `?` dans les signatures PHP. Quand un paramètre est nullable, il devient un pointeur dans votre fonction Go, vous permettant de vérifier si la valeur était `null` en PHP :
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void
|
||||
func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {
|
||||
// Vérifier si name a été fourni (pas null)
|
||||
if name != nil {
|
||||
us.Name = frankenphp.GoString(unsafe.Pointer(name))
|
||||
}
|
||||
|
||||
// Vérifier si age a été fourni (pas null)
|
||||
if age != nil {
|
||||
us.Age = int(*age)
|
||||
}
|
||||
|
||||
// Vérifier si active a été fourni (pas null)
|
||||
if active != nil {
|
||||
us.Active = *active
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Points clés sur les paramètres nullables :**
|
||||
|
||||
- **Types primitifs nullables** (`?int`, `?float`, `?bool`) deviennent des pointeurs (`*int64`, `*float64`, `*bool`) en Go
|
||||
- **Chaînes nullables** (`?string`) restent comme `*C.zend_string` mais peuvent être `nil`
|
||||
- **Vérifiez `nil`** avant de déréférencer les valeurs de pointeur
|
||||
- **PHP `null` devient Go `nil`** - quand PHP passe `null`, votre fonction Go reçoit un pointeur `nil`
|
||||
|
||||
> [!WARNING]
|
||||
> Actuellement, les méthodes de classe ont les limitations suivantes. **Les objets ne sont pas supportés** comme types de paramètres ou types de retour. **Les tableaux sont entièrement supportés** pour les paramètres et types de retour. Types supportés : `string`, `int`, `float`, `bool`, `array`, et `void` (pour le type de retour). **Les types de paramètres nullables sont entièrement supportés** pour tous les types scalaires (`?string`, `?int`, `?float`, `?bool`).
|
||||
|
||||
Après avoir généré l'extension, vous serez autorisé à utiliser la classe et ses méthodes en PHP. Notez que vous **ne pouvez pas accéder aux propriétés directement** :
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
$user = new User();
|
||||
|
||||
// ✅ Fonctionne - utilisation des méthodes
|
||||
$user->setAge(25);
|
||||
echo $user->getName(); // Output : (vide, valeur par défaut)
|
||||
echo $user->getAge(); // Output : 25
|
||||
$user->setNamePrefix("Employee");
|
||||
|
||||
// ✅ Fonctionne aussi - paramètres nullables
|
||||
$user->updateInfo("John", 30, true); // Tous les paramètres fournis
|
||||
$user->updateInfo("Jane", null, false); // L'âge est null
|
||||
$user->updateInfo(null, 25, null); // Le nom et actif sont null
|
||||
|
||||
// ❌ Ne fonctionnera PAS - accès direct aux propriétés
|
||||
// echo $user->name; // Erreur : Impossible d'accéder à la propriété privée
|
||||
// $user->age = 30; // Erreur : Impossible d'accéder à la propriété privée
|
||||
```
|
||||
|
||||
Cette conception garantit que votre code Go a un contrôle complet sur la façon dont l'état de l'objet est accédé et modifié, fournissant une meilleure encapsulation et sécurité de type.
|
||||
|
||||
### Déclarer des Constantes
|
||||
|
||||
Le générateur prend en charge l'exportation de constantes Go vers PHP en utilisant deux directives : `//export_php:const` pour les constantes globales et `//export_php:classconst` pour les constantes de classe. Cela vous permet de partager des valeurs de configuration, des codes de statut et d'autres constantes entre le code Go et PHP.
|
||||
|
||||
#### Constantes Globales
|
||||
|
||||
Utilisez la directive `//export_php:const` pour créer des constantes PHP globales :
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
//export_php:const
|
||||
const MAX_CONNECTIONS = 100
|
||||
|
||||
//export_php:const
|
||||
const API_VERSION = "1.2.3"
|
||||
|
||||
//export_php:const
|
||||
const STATUS_OK = iota
|
||||
|
||||
//export_php:const
|
||||
const STATUS_ERROR = iota
|
||||
```
|
||||
|
||||
#### Constantes de Classe
|
||||
|
||||
Utilisez la directive `//export_php:classconst ClassName` pour créer des constantes qui appartiennent à une classe PHP spécifique :
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
//export_php:classconst User
|
||||
const STATUS_ACTIVE = 1
|
||||
|
||||
//export_php:classconst User
|
||||
const STATUS_INACTIVE = 0
|
||||
|
||||
//export_php:classconst User
|
||||
const ROLE_ADMIN = "admin"
|
||||
|
||||
//export_php:classconst Order
|
||||
const STATE_PENDING = iota
|
||||
|
||||
//export_php:classconst Order
|
||||
const STATE_PROCESSING = iota
|
||||
|
||||
//export_php:classconst Order
|
||||
const STATE_COMPLETED = iota
|
||||
```
|
||||
|
||||
Les constantes de classe sont accessibles en utilisant la portée du nom de classe en PHP :
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// Constantes globales
|
||||
echo MAX_CONNECTIONS; // 100
|
||||
echo API_VERSION; // "1.2.3"
|
||||
|
||||
// Constantes de classe
|
||||
echo User::STATUS_ACTIVE; // 1
|
||||
echo User::ROLE_ADMIN; // "admin"
|
||||
echo Order::STATE_PENDING; // 0
|
||||
```
|
||||
|
||||
La directive prend en charge divers types de valeurs incluant les chaînes, entiers, booléens, flottants et constantes iota. Lors de l'utilisation de `iota`, le générateur assigne automatiquement des valeurs séquentielles (0, 1, 2, etc.). Les constantes globales deviennent disponibles dans votre code PHP comme constantes globales, tandis que les constantes de classe sont déclarées dans leurs classes respectives avec la visibilité publique. Lors de l'utilisation d'entiers, différentes notations possibles (binaire, hex, octale) sont supportées et dumpées telles quelles dans le fichier stub PHP.
|
||||
|
||||
Vous pouvez utiliser les constantes comme vous êtes habitué dans le code Go. Par exemple, prenons la fonction `repeat_this()` que nous avons déclarée plus tôt et changeons le dernier argument en entier :
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:const
|
||||
const STR_REVERSE = iota
|
||||
|
||||
//export_php:const
|
||||
const STR_NORMAL = iota
|
||||
|
||||
//export_php:classconst StringProcessor
|
||||
const MODE_LOWERCASE = 1
|
||||
|
||||
//export_php:classconst StringProcessor
|
||||
const MODE_UPPERCASE = 2
|
||||
|
||||
//export_php:function repeat_this(string $str, int $count, int $mode): string
|
||||
func repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(s))
|
||||
|
||||
result := strings.Repeat(str, int(count))
|
||||
if mode == STR_REVERSE {
|
||||
// inverser la chaîne
|
||||
}
|
||||
|
||||
if mode == STR_NORMAL {
|
||||
// no-op, juste pour montrer la constante
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(result, false)
|
||||
}
|
||||
|
||||
//export_php:class StringProcessor
|
||||
type StringProcessorStruct struct {
|
||||
// champs internes
|
||||
}
|
||||
|
||||
//export_php:method StringProcessor::process(string $input, int $mode): string
|
||||
func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(input))
|
||||
|
||||
switch mode {
|
||||
case MODE_LOWERCASE:
|
||||
str = strings.ToLower(str)
|
||||
case MODE_UPPERCASE:
|
||||
str = strings.ToUpper(str)
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(str, false)
|
||||
}
|
||||
```
|
||||
|
||||
### Utilisation des Espaces de Noms
|
||||
|
||||
Le générateur prend en charge l'organisation des fonctions, classes et constantes de votre extension PHP sous un espace de noms (namespace) en utilisant la directive `//export_php:namespace`. Cela aide à éviter les conflits de noms et fournit une meilleure organisation pour l'API de votre extension.
|
||||
|
||||
#### Déclarer un Espace de Noms
|
||||
|
||||
Utilisez la directive `//export_php:namespace` en haut de votre fichier Go pour placer tous les symboles exportés sous un espace de noms spécifique :
|
||||
|
||||
```go
|
||||
//export_php:namespace My\Extension
|
||||
package example
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:function hello(): string
|
||||
func hello() string {
|
||||
return "Bonjour depuis l'espace de noms My\\Extension !"
|
||||
}
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
// champs internes
|
||||
}
|
||||
|
||||
//export_php:method User::getName(): string
|
||||
func (u *UserStruct) GetName() unsafe.Pointer {
|
||||
return frankenphp.PHPString("Jean Dupont", false)
|
||||
}
|
||||
|
||||
//export_php:const
|
||||
const STATUS_ACTIVE = 1
|
||||
```
|
||||
|
||||
#### Utilisation de l'Extension avec Espace de Noms en PHP
|
||||
|
||||
Quand un espace de noms est déclaré, toutes les fonctions, classes et constantes sont placées sous cet espace de noms en PHP :
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
echo My\Extension\hello(); // "Bonjour depuis l'espace de noms My\Extension !"
|
||||
|
||||
$user = new My\Extension\User();
|
||||
echo $user->getName(); // "Jean Dupont"
|
||||
|
||||
echo My\Extension\STATUS_ACTIVE; // 1
|
||||
```
|
||||
|
||||
#### Notes Importantes
|
||||
|
||||
- Seule **une** directive d'espace de noms est autorisée par fichier. Si plusieurs directives d'espace de noms sont trouvées, le générateur retournera une erreur.
|
||||
- L'espace de noms s'applique à **tous** les symboles exportés dans le fichier : fonctions, classes, méthodes et constantes.
|
||||
- Les noms d'espaces de noms suivent les conventions des espaces de noms PHP en utilisant les barres obliques inverses (`\`) comme séparateurs.
|
||||
- Si aucun espace de noms n'est déclaré, les symboles sont exportés vers l'espace de noms global comme d'habitude.
|
||||
|
||||
### Générer l'Extension
|
||||
|
||||
C'est là que la magie opère, et votre extension peut maintenant être générée. Vous pouvez exécuter le générateur avec la commande suivante :
|
||||
|
||||
```console
|
||||
GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> N'oubliez pas de définir la variable d'environnement `GEN_STUB_SCRIPT` sur le chemin du fichier `gen_stub.php` dans les sources PHP que vous avez téléchargées plus tôt. C'est le même script `gen_stub.php` mentionné dans la section d'implémentation manuelle.
|
||||
|
||||
Si tout s'est bien passé, un nouveau répertoire nommé `build` devrait avoir été créé. Ce répertoire contient les fichiers générés pour votre extension, incluant le fichier `my_extension.go` avec les stubs de fonction PHP générés.
|
||||
|
||||
### Intégrer l'Extension Générée dans FrankenPHP
|
||||
|
||||
Notre extension est maintenant prête à être compilée et intégrée dans FrankenPHP. Pour ce faire, référez-vous à la [documentation de compilation](compile.md) de FrankenPHP pour apprendre comment compiler FrankenPHP. Ajoutez le module en utilisant le flag `--with`, pointant vers le chemin de votre module :
|
||||
|
||||
```console
|
||||
CGO_ENABLED=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
||||
CGO_CFLAGS=$(php-config --includes) \
|
||||
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
||||
xcaddy build \
|
||||
--output frankenphp \
|
||||
--with github.com/my-account/my-module/build
|
||||
```
|
||||
|
||||
Notez que vous pointez vers le sous-répertoire `/build` qui a été créé pendant l'étape de génération. Cependant, ce n'est pas obligatoire : vous pouvez aussi copier les fichiers générés dans le répertoire de votre module et pointer directement vers lui.
|
||||
|
||||
### Tester Votre Extension Générée
|
||||
|
||||
Vous pouvez créer un fichier PHP pour tester les fonctions et classes que vous avez créées. Par exemple, créez un fichier `index.php` avec le contenu suivant :
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// Utilisation des constantes globales
|
||||
var_dump(repeat_this('Hello World', 5, STR_REVERSE));
|
||||
|
||||
// Utilisation des constantes de classe
|
||||
$processor = new StringProcessor();
|
||||
echo $processor->process('Hello World', StringProcessor::MODE_LOWERCASE); // "hello world"
|
||||
echo $processor->process('Hello World', StringProcessor::MODE_UPPERCASE); // "HELLO WORLD"
|
||||
```
|
||||
|
||||
Une fois que vous avez intégré votre extension dans FrankenPHP comme indiqué dans la section précédente, vous pouvez exécuter ce fichier de test en utilisant `./frankenphp php-server`, et vous devriez voir votre extension fonctionner.
|
||||
|
||||
## Implémentation Manuelle
|
||||
|
||||
Si vous voulez comprendre comment les extensions fonctionnent ou avez besoin d'un contrôle total sur votre extension, vous pouvez les écrire manuellement. Cette approche vous donne un contrôle complet mais nécessite plus de code intermédiaire.
|
||||
|
||||
### Fonction de Base
|
||||
|
||||
Nous allons voir comment écrire une extension PHP simple en Go qui définit une nouvelle fonction native. Cette fonction sera appelée depuis PHP et déclenchera une goroutine qui enregistrera un message dans les logs de Caddy. Cette fonction ne prend aucun paramètre et ne retourne rien.
|
||||
|
||||
#### Définir la Fonction Go
|
||||
|
||||
Dans votre module, vous devez définir une nouvelle fonction native qui sera appelée depuis PHP. Pour ce faire, créez un fichier avec le nom que vous voulez, par exemple, `extension.go`, et ajoutez le code suivant :
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include "extension.h"
|
||||
import "C"
|
||||
import (
|
||||
"log/slog"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
|
||||
}
|
||||
|
||||
//export go_print_something
|
||||
func go_print_something() {
|
||||
go func() {
|
||||
slog.Info("Hello from a goroutine!")
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
La fonction `frankenphp.RegisterExtension()` simplifie le processus d'enregistrement d'extension en gérant la logique interne de PHP. La fonction `go_print_something` utilise la directive `//export` pour indiquer qu'elle sera accessible dans le code C que nous écrirons, grâce à CGO.
|
||||
|
||||
Dans cet exemple, notre nouvelle fonction déclenchera une goroutine qui enregistrera un message dans les logs de Caddy.
|
||||
|
||||
#### Définir la Fonction PHP
|
||||
|
||||
Pour permettre à PHP d'appeler notre fonction, nous devons définir une fonction PHP correspondante. Pour cela, nous créerons un fichier stub, par exemple, `extension.stub.php`, qui contiendra le code suivant :
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @generate-class-entries */
|
||||
|
||||
function go_print(): void {}
|
||||
```
|
||||
|
||||
Ce fichier définit la signature de la fonction `go_print()`, qui sera appelée depuis PHP. La directive `@generate-class-entries` permet à PHP de générer automatiquement les entrées de fonction pour notre extension.
|
||||
|
||||
Ceci n'est pas fait manuellement mais en utilisant un script fourni dans les sources PHP (assurez-vous d'ajuster le chemin vers le script `gen_stub.php` selon l'emplacement de vos sources PHP) :
|
||||
|
||||
```bash
|
||||
php ../php-src/build/gen_stub.php extension.stub.php
|
||||
```
|
||||
|
||||
Ce script générera un fichier nommé `extension_arginfo.h` qui contient les informations nécessaires pour que PHP sache comment définir et appeler notre fonction.
|
||||
|
||||
#### Écrire le Pont entre Go et C
|
||||
|
||||
Maintenant, nous devons écrire le pont entre Go et C. Créez un fichier nommé `extension.h` dans le répertoire de votre module avec le contenu suivant :
|
||||
|
||||
```c
|
||||
#ifndef _EXTENSION_H
|
||||
#define _EXTENSION_H
|
||||
|
||||
#include <php.h>
|
||||
|
||||
extern zend_module_entry ext_module_entry;
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
Ensuite, créez un fichier nommé `extension.c` qui effectuera les étapes suivantes :
|
||||
|
||||
- Inclure les en-têtes PHP ;
|
||||
- Déclarer notre nouvelle fonction PHP native `go_print()` ;
|
||||
- Déclarer les métadonnées de l'extension.
|
||||
|
||||
Commençons par inclure les en-têtes requis :
|
||||
|
||||
```c
|
||||
#include <php.h>
|
||||
#include "extension.h"
|
||||
#include "extension_arginfo.h"
|
||||
|
||||
// Contient les symboles exportés par Go
|
||||
#include "_cgo_export.h"
|
||||
```
|
||||
|
||||
Nous définissons ensuite notre fonction PHP comme une fonction de langage natif :
|
||||
|
||||
```c
|
||||
PHP_FUNCTION(go_print)
|
||||
{
|
||||
ZEND_PARSE_PARAMETERS_NONE();
|
||||
|
||||
go_print_something();
|
||||
}
|
||||
|
||||
zend_module_entry ext_module_entry = {
|
||||
STANDARD_MODULE_HEADER,
|
||||
"ext_go",
|
||||
ext_functions, /* Functions */
|
||||
NULL, /* MINIT */
|
||||
NULL, /* MSHUTDOWN */
|
||||
NULL, /* RINIT */
|
||||
NULL, /* RSHUTDOWN */
|
||||
NULL, /* MINFO */
|
||||
"0.1.1",
|
||||
STANDARD_MODULE_PROPERTIES
|
||||
};
|
||||
```
|
||||
|
||||
Dans ce cas, notre fonction ne prend aucun paramètre et ne retourne rien. Elle appelle simplement la fonction Go que nous avons définie plus tôt, exportée en utilisant la directive `//export`.
|
||||
|
||||
Enfin, nous définissons les métadonnées de l'extension dans une structure `zend_module_entry`, telles que son nom, sa version et ses propriétés. Cette information est nécessaire pour que PHP reconnaisse et charge notre extension. Notez que `ext_functions` est un tableau de pointeurs vers les fonctions PHP que nous avons définies, et il a été automatiquement généré par le script `gen_stub.php` dans le fichier `extension_arginfo.h`.
|
||||
|
||||
L'enregistrement de l'extension est automatiquement géré par la fonction `RegisterExtension()` de FrankenPHP que nous appelons dans notre code Go.
|
||||
|
||||
### Usage Avancé
|
||||
|
||||
Maintenant que nous savons comment créer une extension PHP de base en Go, complexifions notre exemple. Nous allons maintenant créer une fonction PHP qui prend une chaîne comme paramètre et retourne sa version en majuscules.
|
||||
|
||||
#### Définir le Stub de Fonction PHP
|
||||
|
||||
Pour définir la nouvelle fonction PHP, nous modifierons notre fichier `extension.stub.php` pour inclure la nouvelle signature de fonction :
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @generate-class-entries */
|
||||
|
||||
/**
|
||||
* Convertit une chaîne en majuscules.
|
||||
*
|
||||
* @param string $string La chaîne à convertir.
|
||||
* @return string La version en majuscules de la chaîne.
|
||||
*/
|
||||
function go_upper(string $string): string {}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Ne négligez pas la documentation de vos fonctions ! Vous êtes susceptible de partager vos stubs d'extension avec d'autres développeurs pour documenter comment utiliser votre extension et quelles fonctionnalités sont disponibles.
|
||||
|
||||
En régénérant le fichier stub avec le script `gen_stub.php`, le fichier `extension_arginfo.h` devrait ressembler à ceci :
|
||||
|
||||
```c
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0)
|
||||
ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
ZEND_FUNCTION(go_upper);
|
||||
|
||||
static const zend_function_entry ext_functions[] = {
|
||||
ZEND_FE(go_upper, arginfo_go_upper)
|
||||
ZEND_FE_END
|
||||
};
|
||||
```
|
||||
|
||||
Nous pouvons voir que la fonction `go_upper` est définie avec un paramètre de type `string` et un type de retour `string`.
|
||||
|
||||
#### Jonglerie de Types entre Go et PHP/C
|
||||
|
||||
Votre fonction Go ne peut pas accepter directement une chaîne PHP comme paramètre. Vous devez la convertir en chaîne Go. Heureusement, FrankenPHP fournit des fonctions d'aide pour gérer la conversion entre les chaînes PHP et les chaînes Go, similaire à ce que nous avons vu dans l'approche du générateur.
|
||||
|
||||
Le fichier d'en-tête reste simple :
|
||||
|
||||
```c
|
||||
#ifndef _EXTENSION_H
|
||||
#define _EXTENSION_H
|
||||
|
||||
#include <php.h>
|
||||
|
||||
extern zend_module_entry ext_module_entry;
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
Nous pouvons maintenant écrire le pont entre Go et C dans notre fichier `extension.c`. Nous passerons la chaîne PHP directement à notre fonction Go :
|
||||
|
||||
```c
|
||||
PHP_FUNCTION(go_upper)
|
||||
{
|
||||
zend_string *str;
|
||||
|
||||
ZEND_PARSE_PARAMETERS_START(1, 1)
|
||||
Z_PARAM_STR(str)
|
||||
ZEND_PARSE_PARAMETERS_END();
|
||||
|
||||
zend_string *result = go_upper(str);
|
||||
RETVAL_STR(result);
|
||||
}
|
||||
```
|
||||
|
||||
Vous pouvez en apprendre plus sur `ZEND_PARSE_PARAMETERS_START` et l'analyse des paramètres dans la page dédiée du [PHP Internals Book](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters). Ici, nous disons à PHP que notre fonction prend un paramètre obligatoire de type `string` comme `zend_string`. Nous passons ensuite cette chaîne directement à notre fonction Go et retournons le résultat en utilisant `RETVAL_STR`.
|
||||
|
||||
Il ne reste qu'une chose à faire : implémenter la fonction `go_upper` en Go.
|
||||
|
||||
#### Implémenter la Fonction Go
|
||||
|
||||
Notre fonction Go prendra un `*C.zend_string` comme paramètre, le convertira en chaîne Go en utilisant la fonction d'aide de FrankenPHP, le traitera, et retournera le résultat comme un nouveau `*C.zend_string`. Les fonctions d'aide gèrent toute la complexité de gestion de mémoire et de conversion pour nous.
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
"strings"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export go_upper
|
||||
func go_upper(s *C.zend_string) *C.zend_string {
|
||||
str := frankenphp.GoString(unsafe.Pointer(s))
|
||||
|
||||
upper := strings.ToUpper(str)
|
||||
|
||||
return (*C.zend_string)(frankenphp.PHPString(upper, false))
|
||||
}
|
||||
```
|
||||
|
||||
Cette approche est beaucoup plus propre et sûre que la gestion manuelle de la mémoire. Les fonctions d'aide de FrankenPHP gèrent la conversion entre le format `zend_string` de PHP et les chaînes Go automatiquement. Le paramètre `false` dans `PHPString()` indique que nous voulons créer une nouvelle chaîne non persistante (libérée à la fin de la requête).
|
||||
|
||||
> [!TIP]
|
||||
> Dans cet exemple, nous n'effectuons aucune gestion d'erreur, mais vous devriez toujours vérifier que les pointeurs ne sont pas `nil` et que les données sont valides avant de les utiliser dans vos fonctions Go.
|
||||
|
||||
### Intégrer l'Extension dans FrankenPHP
|
||||
|
||||
Notre extension est maintenant prête à être compilée et intégrée dans FrankenPHP. Pour ce faire, référez-vous à la [documentation de compilation](compile.md) de FrankenPHP pour apprendre comment compiler FrankenPHP. Ajoutez le module en utilisant le flag `--with`, pointant vers le chemin de votre module :
|
||||
|
||||
```console
|
||||
CGO_ENABLED=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
||||
CGO_CFLAGS=$(php-config --includes) \
|
||||
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
||||
xcaddy build \
|
||||
--output frankenphp \
|
||||
--with github.com/my-account/my-module
|
||||
```
|
||||
|
||||
C'est tout ! Votre extension est maintenant intégrée dans FrankenPHP et peut être utilisée dans votre code PHP.
|
||||
|
||||
### Tester Votre Extension
|
||||
|
||||
Après avoir intégré votre extension dans FrankenPHP, vous pouvez créer un fichier `index.php` avec des exemples pour les fonctions que vous avez implémentées :
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// Tester la fonction de base
|
||||
go_print();
|
||||
|
||||
// Tester la fonction avancée
|
||||
echo go_upper("hello world") . "\n";
|
||||
```
|
||||
|
||||
Vous pouvez maintenant exécuter FrankenPHP avec ce fichier en utilisant `./frankenphp php-server`, et vous devriez voir votre extension fonctionner.
|
||||
@@ -5,7 +5,7 @@
|
||||
Les extensions suivantes sont connues pour ne pas être compatibles avec FrankenPHP :
|
||||
|
||||
| Nom | Raison | Alternatives |
|
||||
|-------------------------------------------------------------------------------------------------------------|-----------------|----------------------------------------------------------------------------------------------------------------------|
|
||||
| ----------------------------------------------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| [imap](https://www.php.net/manual/en/imap.installation.php) | Non thread-safe | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |
|
||||
| [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | Non thread-safe | - |
|
||||
|
||||
@@ -14,7 +14,7 @@ Les extensions suivantes sont connues pour ne pas être compatibles avec Franken
|
||||
Les extensions suivantes ont des bugs connus ou des comportements inattendus lorsqu'elles sont utilisées avec FrankenPHP :
|
||||
|
||||
| Nom | Problème |
|
||||
|---------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [ext-openssl](https://www.php.net/manual/fr/book.openssl.php) | Lors de l'utilisation d'une version statique de FrankenPHP (construite avec la libc musl), l'extension OpenSSL peut planter sous de fortes charges. Une solution consiste à utiliser une version liée dynamiquement (comme celle utilisée dans les images Docker). Ce bogue est [suivi par PHP](https://github.com/php/php-src/issues/13648). |
|
||||
|
||||
## get_browser
|
||||
@@ -23,7 +23,7 @@ La fonction [get_browser()](https://www.php.net/manual/fr/function.get-browser.p
|
||||
|
||||
## Binaire autonome et images Docker basées sur Alpine
|
||||
|
||||
Le binaire autonome et les images docker basées sur Alpine (`dunglas/frankenphp:*-alpine`) utilisent [musl libc](https://musl.libc.org/) au lieu de [glibc et ses amis](https://www.etalabs.net/compare_libcs.html), pour garder une taille de binaire plus petite. Cela peut entraîner des problèmes de compatibilité. En particulier, le drapeau glob `GLOB_BRACE` n'est [pas disponible](https://www.php.net/manual/fr/function.glob.php).
|
||||
Le binaire autonome et les images Docker basées sur Alpine (`dunglas/frankenphp:*-alpine`) utilisent [musl libc](https://musl.libc.org/) au lieu de [glibc et ses amis](https://www.etalabs.net/compare_libcs.html), pour garder une taille de binaire plus petite. Cela peut entraîner des problèmes de compatibilité. En particulier, le drapeau glob `GLOB_BRACE` n'est [pas disponible](https://www.php.net/manual/fr/function.glob.php).
|
||||
|
||||
## Utilisation de `https://127.0.0.1` avec Docker
|
||||
|
||||
@@ -75,3 +75,69 @@ docker run \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
## Scripts Composer Faisant Références à `@php`
|
||||
|
||||
Les [scripts Composer](https://getcomposer.org/doc/articles/scripts.md) peuvent vouloir exécuter un binaire PHP pour certaines tâches, par exemple dans [un projet Laravel](laravel.md) pour exécuter `@php artisan package:discover --ansi`. Cela [echoue actuellement](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) pour deux raisons :
|
||||
|
||||
- Composer ne sait pas comment appeler le binaire FrankenPHP ;
|
||||
- Composer peut ajouter des paramètres PHP en utilisant le paramètre `-d` dans la commande, ce que FrankenPHP ne supporte pas encore.
|
||||
|
||||
Comme solution de contournement, nous pouvons créer un script shell dans `/usr/local/bin/php` qui supprime les paramètres non supportés et appelle ensuite FrankenPHP :
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
args=("$@")
|
||||
index=0
|
||||
for i in "$@"
|
||||
do
|
||||
if [ "$i" == "-d" ]; then
|
||||
unset 'args[$index]'
|
||||
unset 'args[$index+1]'
|
||||
fi
|
||||
index=$((index+1))
|
||||
done
|
||||
|
||||
/usr/local/bin/frankenphp php-cli ${args[@]}
|
||||
```
|
||||
|
||||
Ensuite, mettez la variable d'environnement `PHP_BINARY` au chemin de notre script `php` et lancez Composer :
|
||||
|
||||
```console
|
||||
export PHP_BINARY=/usr/local/bin/php
|
||||
composer install
|
||||
```
|
||||
|
||||
## Résolution des problèmes TLS/SSL avec les binaires statiques
|
||||
|
||||
Lorsque vous utilisez les binaires statiques, vous pouvez rencontrer les erreurs suivantes liées à TLS, par exemple lors de l'envoi de courriels utilisant STARTTLS :
|
||||
|
||||
```text
|
||||
Unable to connect with STARTTLS: stream_socket_enable_crypto(): SSL operation failed with code 5. OpenSSL Error messages:
|
||||
error:80000002:system library::No such file or directory
|
||||
error:80000002:system library::No such file or directory
|
||||
error:80000002:system library::No such file or directory
|
||||
error:0A000086:SSL routines::certificate verify failed
|
||||
```
|
||||
|
||||
Comme le binaire statique ne contient pas de certificats TLS, vous devez indiquer à OpenSSL l'installation de vos certificats CA locaux.
|
||||
|
||||
Inspectez la sortie de [`openssl_get_cert_locations()`](https://www.php.net/manual/en/function.openssl-get-cert-locations.php),
|
||||
pour trouver l'endroit où les certificats CA doivent être installés et stockez-les à cet endroit.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> Les contextes Web et CLI peuvent avoir des paramètres différents.
|
||||
> Assurez-vous d'exécuter `openssl_get_cert_locations()` dans le bon contexte.
|
||||
|
||||
[Les certificats CA extraits de Mozilla peuvent être téléchargés sur le site de cURL](https://curl.se/docs/caextract.html).
|
||||
|
||||
Alternativement, de nombreuses distributions, y compris Debian, Ubuntu, et Alpine fournissent des paquets nommés `ca-certificates` qui contiennent ces certificats.
|
||||
|
||||
Il est également possible d'utiliser `SSL_CERT_FILE` et `SSL_CERT_DIR` pour indiquer à OpenSSL où chercher les certificats CA :
|
||||
|
||||
```console
|
||||
# Définir les variables d'environnement des certificats TLS
|
||||
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
export SSL_CERT_DIR=/etc/ssl/certs
|
||||
```
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
## Docker
|
||||
|
||||
Déployer une application web [Laravel](https://laravel.com) avec FrankenPHP est très facile.
|
||||
Il suffit de monter le projet dans le répertoire `/app` de l'image Docker officielle.
|
||||
Déployer une application web [Laravel](https://laravel.com) avec FrankenPHP est très facile. Il suffit de monter le projet dans le répertoire `/app` de l'image Docker officielle.
|
||||
|
||||
Exécutez cette commande depuis le répertoire principal de votre application Laravel :
|
||||
|
||||
@@ -20,21 +19,23 @@ Vous pouvez également exécuter vos projets Laravel avec FrankenPHP depuis votr
|
||||
1. [Téléchargez le binaire correspondant à votre système](README.md#binaire-autonome)
|
||||
2. Ajoutez la configuration suivante dans un fichier nommé `Caddyfile` placé dans le répertoire racine de votre projet Laravel :
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp
|
||||
}
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp
|
||||
}
|
||||
|
||||
# Le nom de domaine de votre serveur
|
||||
localhost {
|
||||
# Définir le répertoire racine sur le dossier public/
|
||||
root * public/
|
||||
# Autoriser la compression (optionnel)
|
||||
encode zstd br gzip
|
||||
# Exécuter les scripts PHP du dossier public/ et servir les assets
|
||||
php_server
|
||||
}
|
||||
```
|
||||
# Le nom de domaine de votre serveur
|
||||
localhost {
|
||||
# Définir le répertoire racine sur le dossier public/
|
||||
root public/
|
||||
# Autoriser la compression (optionnel)
|
||||
encode zstd br gzip
|
||||
# Exécuter les scripts PHP du dossier public/ et servir les assets
|
||||
php_server {
|
||||
try_files {path} index.php
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Démarrez FrankenPHP depuis le répertoire racine de votre projet Laravel : `frankenphp run`
|
||||
|
||||
@@ -60,19 +61,125 @@ php artisan octane:frankenphp
|
||||
|
||||
La commande `octane:frankenphp` peut prendre les options suivantes :
|
||||
|
||||
* `--host` : L'adresse IP à laquelle le serveur doit se lier (par défaut : `127.0.0.1`)
|
||||
* `--port` : Le port sur lequel le serveur doit être disponible (par défaut : `8000`)
|
||||
* `--admin-port` : Le port sur lequel le serveur administratif doit être disponible (par défaut : `2019`)
|
||||
* `--workers` : Le nombre de workers qui doivent être disponibles pour traiter les requêtes (par défaut : `auto`)
|
||||
* `--max-requests` : Le nombre de requêtes à traiter avant de recharger le serveur (par défaut : `500`)
|
||||
* `--caddyfile` : Le chemin vers le fichier `Caddyfile` de FrankenPHP
|
||||
* `--https` : Activer HTTPS, HTTP/2, et HTTP/3, et générer automatiquement et renouveler les certificats
|
||||
* `--http-redirect` : Activer la redirection HTTP vers HTTPS (uniquement activé si --https est passé)
|
||||
* `--watch` : Recharger automatiquement le serveur lorsque l'application est modifiée
|
||||
* `--poll` : Utiliser le sondage du système de fichiers pendant la surveillance pour surveiller les fichiers sur un réseau
|
||||
* `--log-level` : Enregistrer les messages au niveau de journalisation spécifié ou au-dessus, en utilisant le logger natif de Caddy
|
||||
- `--host` : L'adresse IP à laquelle le serveur doit se lier (par défaut : `127.0.0.1`)
|
||||
- `--port` : Le port sur lequel le serveur doit être disponible (par défaut : `8000`)
|
||||
- `--admin-port` : Le port sur lequel le serveur administratif doit être disponible (par défaut : `2019`)
|
||||
- `--workers` : Le nombre de workers qui doivent être disponibles pour traiter les requêtes (par défaut : `auto`)
|
||||
- `--max-requests` : Le nombre de requêtes à traiter avant de recharger le serveur (par défaut : `500`)
|
||||
- `--caddyfile` : Le chemin vers le fichier `Caddyfile` de FrankenPHP
|
||||
- `--https` : Activer HTTPS, HTTP/2, et HTTP/3, et générer automatiquement et renouveler les certificats
|
||||
- `--http-redirect` : Activer la redirection HTTP vers HTTPS (uniquement activé si --https est passé)
|
||||
- `--watch` : Recharger automatiquement le serveur lorsque l'application est modifiée
|
||||
- `--poll` : Utiliser le sondage du système de fichiers pendant la surveillance pour surveiller les fichiers sur un réseau
|
||||
- `--log-level` : Enregistrer les messages au niveau de journalisation spécifié ou au-dessus, en utilisant le logger natif de Caddy
|
||||
|
||||
> [!TIP]
|
||||
> Pour obtenir des logs structurés en JSON logs (utile quand vous utilisez des solutions d'analyse de logs), passez explicitement l'otpion `--log-level`.
|
||||
> Pour obtenir des logs structurés en JSON logs (utile quand vous utilisez des solutions d'analyse de logs), passez explicitement l'option `--log-level`.
|
||||
|
||||
En savoir plus sur Laravel Octane [dans sa documentation officielle](https://laravel.com/docs/octane).
|
||||
|
||||
## Les Applications Laravel En Tant Que Binaires Autonomes
|
||||
|
||||
En utilisant la [fonctionnalité d'intégration d'applications de FrankenPHP](embed.md), il est possible de distribuer
|
||||
les applications Laravel sous forme de binaires autonomes.
|
||||
|
||||
Suivez ces étapes pour empaqueter votre application Laravel en tant que binaire autonome pour Linux :
|
||||
|
||||
1. Créez un fichier nommé `static-build.Dockerfile` dans le dépôt de votre application :
|
||||
|
||||
```dockerfile
|
||||
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
|
||||
# Si vous avez l'intention d'exécuter le binaire sur des systèmes musl-libc, utilisez plutôt static-builder-musl
|
||||
|
||||
# Copiez votre application
|
||||
WORKDIR /go/src/app/dist/app
|
||||
COPY . .
|
||||
|
||||
# Supprimez les tests et autres fichiers inutiles pour gagner de la place
|
||||
# Alternativement, ajoutez ces fichiers à un fichier .dockerignore
|
||||
RUN rm -Rf tests/
|
||||
|
||||
# Copiez le fichier .env
|
||||
RUN cp .env.example .env
|
||||
# Modifier APP_ENV et APP_DEBUG pour qu'ils soient prêts pour la production
|
||||
RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env
|
||||
|
||||
# Apportez d'autres modifications à votre fichier .env si nécessaire
|
||||
|
||||
# Installez les dépendances
|
||||
RUN composer install --ignore-platform-reqs --no-dev -a
|
||||
|
||||
# Construire le binaire statique
|
||||
WORKDIR /go/src/app/
|
||||
RUN EMBED=dist/app/ ./build-static.sh
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Certains fichiers `.dockerignore` ignoreront le répertoire `vendor/`
|
||||
> et les fichiers `.env`. Assurez-vous d'ajuster ou de supprimer le fichier `.dockerignore` avant la construction.
|
||||
|
||||
2. Build:
|
||||
|
||||
```console
|
||||
docker build -t static-laravel-app -f static-build.Dockerfile .
|
||||
```
|
||||
|
||||
3. Extraire le binaire
|
||||
|
||||
```console
|
||||
docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp
|
||||
```
|
||||
|
||||
4. Remplir les caches :
|
||||
|
||||
```console
|
||||
frankenphp php-cli artisan optimize
|
||||
```
|
||||
|
||||
5. Exécutez les migrations de base de données (s'il y en a) :
|
||||
|
||||
```console
|
||||
frankenphp php-cli artisan migrate
|
||||
```
|
||||
|
||||
6. Générer la clé secrète de l'application :
|
||||
|
||||
```console
|
||||
frankenphp php-cli artisan key:generate
|
||||
```
|
||||
|
||||
7. Démarrez le serveur:
|
||||
|
||||
```console
|
||||
frankenphp php-server
|
||||
```
|
||||
|
||||
Votre application est maintenant prête !
|
||||
|
||||
Pour en savoir plus sur les options disponibles et sur la construction de binaires pour d'autres systèmes d'exploitation,
|
||||
consultez la documentation [Applications PHP en tant que binaires autonomes](embed.md).
|
||||
|
||||
### Changer le chemin de stockage
|
||||
|
||||
Par défaut, Laravel stocke les fichiers téléchargés, les caches, les logs, etc. dans le répertoire `storage/` de l'application.
|
||||
Ceci n'est pas adapté aux applications embarquées, car chaque nouvelle version sera extraite dans un répertoire temporaire différent.
|
||||
|
||||
Définissez la variable d'environnement `LARAVEL_STORAGE_PATH` (par exemple, dans votre fichier `.env`) ou appelez la méthode `Illuminate\Foundation\Application::useStoragePath()` pour utiliser un répertoire en dehors du répertoire temporaire.
|
||||
|
||||
### Exécuter Octane avec des binaires autonomes
|
||||
|
||||
Il est même possible d'empaqueter les applications Laravel Octane en tant que binaires autonomes !
|
||||
|
||||
Pour ce faire, [installez Octane correctement](#laravel-octane) et suivez les étapes décrites dans [la section précédente](#les-applications-laravel-en-tant-que-binaires-autonomes).
|
||||
|
||||
Ensuite, pour démarrer FrankenPHP en mode worker via Octane, exécutez :
|
||||
|
||||
```console
|
||||
PATH="$PWD:$PATH" frankenphp php-cli artisan octane:frankenphp
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Pour que la commande fonctionne, le binaire autonome **doit** être nommé `frankenphp`
|
||||
> car Octane a besoin d'un programme nommé `frankenphp` disponible dans le chemin
|
||||
|
||||
17
docs/fr/metrics.md
Normal file
17
docs/fr/metrics.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Métriques
|
||||
|
||||
Lorsque les [métriques Caddy](https://caddyserver.com/docs/metrics) sont activées, FrankenPHP expose les métriques suivantes :
|
||||
|
||||
- `frankenphp_total_threads` : Le nombre total de threads PHP.
|
||||
- `frankenphp_busy_threads` : Le nombre de threads PHP en cours de traitement d'une requête (les workers en cours d'exécution consomment toujours un thread).
|
||||
- `frankenphp_queue_depth` : Le nombre de requêtes régulières en file d'attente
|
||||
- `frankenphp_total_workers{worker=« [nom_du_worker] »}` : Le nombre total de workers.
|
||||
- `frankenphp_busy_workers{worker=« [nom_du_worker] »}` : Le nombre de workers qui traitent actuellement une requête.
|
||||
- `frankenphp_worker_request_time{worker=« [nom_du_worker] »}` : Le temps passé à traiter les requêtes par tous les workers.
|
||||
- `frankenphp_worker_request_count{worker=« [nom_du_worker] »}` : Le nombre de requêtes traitées par tous les workers.
|
||||
- `frankenphp_ready_workers{worker=« [nom_du_worker] »}` : Le nombre de workers qui ont appelé `frankenphp_handle_request` au moins une fois.
|
||||
- `frankenphp_worker_crashes{worker=« [nom_du_worker] »}` : Le nombre de fois où un worker s'est arrêté de manière inattendue.
|
||||
- `frankenphp_worker_restarts{worker=« [nom_du_worker] »}` : Le nombre de fois où un worker a été délibérément redémarré.
|
||||
- `frankenphp_worker_queue_depth{worker=« [nom_du_worker] »}` : Le nombre de requêtes en file d'attente.
|
||||
|
||||
Pour les métriques de worker, le placeholder `[nom_du_worker]` est remplacé par le nom du worker dans le Caddyfile, sinon le chemin absolu du fichier du worker sera utilisé.
|
||||
@@ -16,6 +16,16 @@ Pour trouver les bonnes valeurs, il est souhaitable d'effectuer des tests de cha
|
||||
Pour configurer le nombre de threads, utilisez l'option `num_threads` des directives `php_server` et `php`.
|
||||
Pour changer le nombre de travailleurs, utilisez l'option `num` de la section `worker` de la directive `frankenphp`.
|
||||
|
||||
### `max_threads`
|
||||
|
||||
Bien qu'il soit toujours préférable de savoir exactement à quoi ressemblera votre trafic, les applications réelles
|
||||
ont tendance à être plus imprévisibles. Le paramètre `max_threads` permet à FrankenPHP de créer automatiquement des threads supplémentaires au moment de l'exécution, jusqu'à la limite spécifiée.
|
||||
`max_threads` peut vous aider à déterminer le nombre de threads dont vous avez besoin pour gérer votre trafic et peut rendre le serveur plus résistant aux pics de latence.
|
||||
Si elle est fixée à `auto`, la limite sera estimée en fonction de la valeur de `memory_limit` dans votre `php.ini`. Si ce n'est pas possible,
|
||||
`auto` prendra par défaut 2x `num_threads`. Gardez à l'esprit que `auto` peut fortement sous-estimer le nombre de threads nécessaires.
|
||||
`max_threads` est similaire à [pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children) de PHP FPM. La principale différence est que FrankenPHP utilise des threads au lieu de
|
||||
processus et les délègue automatiquement à différents scripts de travail et au `mode classique` selon les besoins.
|
||||
|
||||
## Mode worker
|
||||
|
||||
Activer [le mode worker](worker.md) améliore considérablement les performances,
|
||||
@@ -27,7 +37,7 @@ vous devez créer un script worker et vous assurer que l'application n'a pas de
|
||||
Les binaires statiques que nous fournissons, ainsi que la variante Alpine Linux des images Docker officielles, utilisent [la bibliothèque musl](https://musl.libc.org).
|
||||
|
||||
PHP est connu pour être [significativement plus lent](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381) lorsqu'il utilise cette bibliothèque C alternative au lieu de la bibliothèque GNU traditionnelle,
|
||||
surtout lorsqu'il est compilé en mode ZTS (*thread-safe*), ce qui est nécessaire pour FrankenPHP.
|
||||
surtout lorsqu'il est compilé en mode ZTS (_thread-safe_), ce qui est nécessaire pour FrankenPHP.
|
||||
|
||||
En outre, [certains bogues ne se produisent que lors de l'utilisation de musl](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl).
|
||||
|
||||
@@ -65,16 +75,54 @@ php_server {
|
||||
}
|
||||
```
|
||||
|
||||
## *Placeholders*
|
||||
## `try_files`
|
||||
|
||||
Vous pouvez utiliser des [*placeholders*](https://caddyserver.com/docs/conventions#placeholders) dans les directives `root` et `env`.
|
||||
En plus des fichiers statiques et des fichiers PHP, `php_server` essaiera aussi de servir les fichiers d'index
|
||||
et d'index de répertoire de votre application (`/path/` -> `/path/index.php`). Si vous n'avez pas besoin des index de répertoires,
|
||||
vous pouvez les désactiver en définissant explicitement `try_files` comme ceci :
|
||||
|
||||
```caddyfile
|
||||
php_server {
|
||||
try_files {path} index.php
|
||||
root /root/to/your/app # l'ajout explicite de la racine ici permet une meilleure mise en cache
|
||||
}
|
||||
```
|
||||
|
||||
Cela permet de réduire considérablement le nombre d'opérations inutiles sur les fichiers.
|
||||
|
||||
Une approche alternative avec 0 opérations inutiles sur le système de fichiers serait d'utiliser la directive `php`
|
||||
et de diviser les fichiers de PHP par chemin. Cette approche fonctionne bien si votre application entière est servie par un seul fichier d'entrée.
|
||||
Un exemple de [configuration](config.md#configuration-du-caddyfile) qui sert des fichiers statiques derrière un dossier `/assets` pourrait ressembler à ceci :
|
||||
|
||||
```caddyfile
|
||||
route {
|
||||
@assets {
|
||||
path /assets/*
|
||||
}
|
||||
|
||||
# tout ce qui se trouve derrière /assets est géré par le serveur de fichiers
|
||||
file_server @assets {
|
||||
root /root/to/your/app
|
||||
}
|
||||
|
||||
# tout ce qui n'est pas dans /assets est géré par votre index ou votre fichier PHP worker
|
||||
rewrite index.php
|
||||
php {
|
||||
root /root/to/your/app # l'ajout explicite de la racine ici permet une meilleure mise en cache
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## _Placeholders_
|
||||
|
||||
Vous pouvez utiliser des [_placeholders_](https://caddyserver.com/docs/conventions#placeholders) dans les directives `root` et `env`.
|
||||
Cependant, cela empêche la mise en cache de ces valeurs et a un coût important en termes de performances.
|
||||
|
||||
Si possible, évitez les *placeholders* dans ces directives.
|
||||
Si possible, évitez les _placeholders_ dans ces directives.
|
||||
|
||||
## `resolve_root_symlink`
|
||||
|
||||
Par défaut, si le *document root* est un lien symbolique, il est automatiquement résolu par FrankenPHP (c'est nécessaire pour le bon fonctionnement de PHP).
|
||||
Par défaut, si le _document root_ est un lien symbolique, il est automatiquement résolu par FrankenPHP (c'est nécessaire pour le bon fonctionnement de PHP).
|
||||
Si la racine du document n'est pas un lien symbolique, vous pouvez désactiver cette fonctionnalité.
|
||||
|
||||
```caddyfile
|
||||
@@ -83,12 +131,12 @@ php_server {
|
||||
}
|
||||
```
|
||||
|
||||
Cela améliorera les performances si la directive `root` contient des [*placeholders*](https://caddyserver.com/docs/conventions#placeholders).
|
||||
Cela améliorera les performances si la directive `root` contient des [_placeholders_](https://caddyserver.com/docs/conventions#placeholders).
|
||||
Le gain sera négligeable dans les autres cas.
|
||||
|
||||
## Journaux
|
||||
|
||||
La journalisation est évidemment très utile, mais, par définition, elle nécessite des opérations d'*I/O* et des allocations de mémoire,
|
||||
La journalisation est évidemment très utile, mais, par définition, elle nécessite des opérations d'_I/O_ et des allocations de mémoire,
|
||||
ce qui réduit considérablement les performances.
|
||||
Assurez-vous de [définir le niveau de journalisation](https://caddyserver.com/docs/caddyfile/options#log) correctement,
|
||||
et de ne journaliser que ce qui est nécessaire.
|
||||
@@ -100,10 +148,10 @@ Toutes les optimisations de performances habituelles liées à PHP s'appliquent
|
||||
|
||||
En particulier :
|
||||
|
||||
* vérifiez que [OPcache](https://www.php.net/manual/en/book.opcache.php) est installé, activé et correctement configuré
|
||||
* activez [les optimisations de l'autoloader de Composer](https://getcomposer.org/doc/articles/autoloader-optimization.md)
|
||||
* assurez-vous que le cache `realpath` est suffisamment grand pour les besoins de votre application
|
||||
* utilisez le [pré-chargement](https://www.php.net/manual/en/opcache.preloading.php)
|
||||
- vérifiez que [OPcache](https://www.php.net/manual/en/book.opcache.php) est installé, activé et correctement configuré
|
||||
- activez [les optimisations de l'autoloader de Composer](https://getcomposer.org/doc/articles/autoloader-optimization.md)
|
||||
- assurez-vous que le cache `realpath` est suffisamment grand pour les besoins de votre application
|
||||
- utilisez le [pré-chargement](https://www.php.net/manual/en/opcache.preloading.php)
|
||||
|
||||
Pour plus de détails, lisez [l'entrée de la documentation dédiée de Symfony](https://symfony.com/doc/current/performance.html)
|
||||
(la plupart des conseils sont utiles même si vous n'utilisez pas Symfony).
|
||||
|
||||
@@ -18,6 +18,9 @@ ENV SERVER_NAME=your-domain-name.example.com
|
||||
# Si vous souhaitez désactiver HTTPS, utilisez cette valeur à la place :
|
||||
#ENV SERVER_NAME=:80
|
||||
|
||||
# Si votre projet n'utilise pas le répertoire "public" comme racine web, vous pouvez le définir ici :
|
||||
# ENV SERVER_ROOT=web/
|
||||
|
||||
# Activer les paramètres de production de PHP
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
|
||||
@@ -4,20 +4,49 @@ Au lieu d'utiliser une installation locale de la bibliothèque PHP, il est possi
|
||||
|
||||
Avec cette méthode, un binaire portable unique contiendra l'interpréteur PHP, le serveur web Caddy et FrankenPHP !
|
||||
|
||||
Les exécutables natifs entièrement statiques ne nécessitent aucune dépendance et peuvent même être exécutés sur une [image Docker `scratch`](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch).
|
||||
Cependant, ils ne peuvent pas charger les extensions dynamiques de PHP (comme Xdebug) et ont quelques limitations parce qu'ils utilisent la librairie musl.
|
||||
|
||||
La plupart des binaires statiques ne nécessitent que la `glibc` et peuvent charger des extensions dynamiques.
|
||||
|
||||
Lorsque c'est possible, nous recommandons d'utiliser des binaires statiques basés sur la glibc.
|
||||
|
||||
FrankenPHP permet également [d'embarquer l'application PHP dans le binaire statique](embed.md).
|
||||
|
||||
## Linux
|
||||
|
||||
Nous fournissons une image Docker pour créer un binaire statique pour Linux :
|
||||
Nous fournissons des images Docker pour créer des binaires statiques pour Linux :
|
||||
|
||||
### Build entièrement statique, basé sur musl
|
||||
|
||||
Pour un binaire entièrement statique qui fonctionne sur n'importe quelle distribution Linux sans dépendances,
|
||||
mais qui ne prend pas en charge le chargement dynamique des extensions :
|
||||
|
||||
```console
|
||||
docker buildx bake --load static-builder
|
||||
docker cp $(docker create --name static-builder dunglas/frankenphp:static-builder):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder
|
||||
docker buildx bake --load static-builder-musl
|
||||
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-musl
|
||||
```
|
||||
|
||||
Le binaire statique résultant est nommé `frankenphp`, et il est disponible dans le répertoire courant.
|
||||
Pour améliorer les performances dans les scénarios fortement concurrents, envisagez d'utiliser l'allocateur [mimalloc](https://github.com/microsoft/mimalloc).
|
||||
|
||||
Si vous souhaitez construire le binaire statique sans Docker, regardez les instructions pour macOS, qui fonctionnent également pour Linux.
|
||||
```console
|
||||
docker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl
|
||||
```
|
||||
|
||||
### Construction principalement statique (avec prise en charge des extensions dynamiques), basé sur la glibc
|
||||
|
||||
Pour un binaire qui supporte le chargement dynamique des extensions PHP tout en ayant les extensions sélectionnées compilées statiquement :
|
||||
|
||||
```console
|
||||
docker buildx bake --load static-builder-gnu
|
||||
docker cp $(docker create --name static-builder-gnu dunglas/frankenphp:static-builder-gnu):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-gnu
|
||||
```
|
||||
|
||||
Ce binaire supporte toutes les versions 2.17 et supérieures de la glibc mais ne fonctionne pas sur les systèmes basés sur musl (comme Alpine Linux).
|
||||
|
||||
Le binaire résultant, principalement statique (à l'exception de `glibc`), est nommé `frankenphp` et est disponible dans le répertoire courant.
|
||||
|
||||
Si vous voulez construire le binaire statique sans Docker, jetez un coup d'œil aux instructions pour macOS, qui fonctionnent aussi pour Linux.
|
||||
|
||||
### Extensions personnalisées
|
||||
|
||||
@@ -28,7 +57,7 @@ Pour réduire la taille du binaire et diminuer la surface d'attaque, vous pouvez
|
||||
Par exemple, exécutez la commande suivante pour ne construire que l'extension `opcache` :
|
||||
|
||||
```console
|
||||
docker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder
|
||||
docker buildx bake --load --set static-builder-musl.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder-musl
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -37,9 +66,9 @@ Pour ajouter des bibliothèques permettant des fonctionnalités supplémentaires
|
||||
```console
|
||||
docker buildx bake \
|
||||
--load \
|
||||
--set static-builder.args.PHP_EXTENSIONS=gd \
|
||||
--set static-builder.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \
|
||||
static-builder
|
||||
--set static-builder-musl.args.PHP_EXTENSIONS=gd \
|
||||
--set static-builder-musl.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \
|
||||
static-builder-musl
|
||||
```
|
||||
|
||||
### Modules supplémentaires de Caddy
|
||||
@@ -49,8 +78,8 @@ Pour ajouter des modules Caddy supplémentaires ou passer d'autres arguments à
|
||||
```console
|
||||
docker buildx bake \
|
||||
--load \
|
||||
--set static-builder.args.XCADDY_ARGS="--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy" \
|
||||
static-builder
|
||||
--set static-builder-musl.args.XCADDY_ARGS="--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy" \
|
||||
static-builder-musl
|
||||
```
|
||||
|
||||
Dans cet exemple, nous ajoutons le module de cache HTTP [Souin](https://souin.io) pour Caddy ainsi que les modules [cbrotli](https://github.com/dunglas/caddy-cbrotli), [Mercure](https://mercure.rocks) et [Vulcain](https://vulcain.rocks).
|
||||
@@ -67,7 +96,7 @@ Voir aussi comment [personnaliser la construction](#personnalisation-de-la-const
|
||||
Si vous atteignez la limite de taux d'appels de l'API GitHub, définissez un jeton d'accès personnel GitHub dans une variable d'environnement nommée `GITHUB_TOKEN` :
|
||||
|
||||
```console
|
||||
GITHUB_TOKEN="xxx" docker --load buildx bake static-builder
|
||||
GITHUB_TOKEN="xxx" docker --load buildx bake static-builder-musl
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -76,7 +105,7 @@ GITHUB_TOKEN="xxx" docker --load buildx bake static-builder
|
||||
Exécutez le script suivant pour créer un binaire statique pour macOS (vous devez avoir [Homebrew](https://brew.sh/) d'installé) :
|
||||
|
||||
```console
|
||||
git clone https://github.com/dunglas/frankenphp
|
||||
git clone https://github.com/php/frankenphp
|
||||
cd frankenphp
|
||||
./build-static.sh
|
||||
```
|
||||
@@ -87,14 +116,46 @@ Note : ce script fonctionne également sur Linux (et probablement sur d'autres U
|
||||
|
||||
Les variables d'environnement suivantes peuvent être transmises à `docker build` et au script `build-static.sh` pour personnaliser la construction statique :
|
||||
|
||||
* `FRANKENPHP_VERSION` : la version de FrankenPHP à utiliser
|
||||
* `PHP_VERSION` : la version de PHP à utiliser
|
||||
* `PHP_EXTENSIONS` : les extensions PHP à construire ([liste des extensions prises en charge](https://static-php.dev/en/guide/extensions.html))
|
||||
* `PHP_EXTENSION_LIBS` : bibliothèques supplémentaires à construire qui ajoutent des fonctionnalités aux extensions
|
||||
* `XCADDY_ARGS` : arguments à passer à [xcaddy](https://github.com/caddyserver/xcaddy), par exemple pour ajouter des modules Caddy supplémentaires
|
||||
* `EMBED` : chemin de l'application PHP à intégrer dans le binaire
|
||||
* `CLEAN` : lorsque défini, `libphp` et toutes ses dépendances sont construites à partir de zéro (pas de cache)
|
||||
* `DEBUG_SYMBOLS` : lorsque défini, les symboles de débogage ne seront pas supprimés et seront ajoutés dans le binaire
|
||||
* `NO_COMPRESS`: ne pas compresser le binaire avec UPX
|
||||
* `MIMALLOC`: (expérimental, Linux seulement) remplace l'allocateur mallocng de musl par [mimalloc](https://github.com/microsoft/mimalloc) pour des performances améliorées
|
||||
* `RELEASE` : (uniquement pour les mainteneurs) lorsque défini, le binaire résultant sera uploadé sur GitHub
|
||||
- `FRANKENPHP_VERSION` : la version de FrankenPHP à utiliser
|
||||
- `PHP_VERSION` : la version de PHP à utiliser
|
||||
- `PHP_EXTENSIONS` : les extensions PHP à construire ([liste des extensions prises en charge](https://static-php.dev/en/guide/extensions.html))
|
||||
- `PHP_EXTENSION_LIBS` : bibliothèques supplémentaires à construire qui ajoutent des fonctionnalités aux extensions
|
||||
- `XCADDY_ARGS` : arguments à passer à [xcaddy](https://github.com/caddyserver/xcaddy), par exemple pour ajouter des modules Caddy supplémentaires
|
||||
- `EMBED` : chemin de l'application PHP à intégrer dans le binaire
|
||||
- `CLEAN` : lorsque défini, `libphp` et toutes ses dépendances sont construites à partir de zéro (pas de cache)
|
||||
- `DEBUG_SYMBOLS` : lorsque défini, les symboles de débogage ne seront pas supprimés et seront ajoutés dans le binaire
|
||||
- `NO_COMPRESS`: ne pas compresser le binaire avec UPX
|
||||
- `MIMALLOC`: (expérimental, Linux seulement) remplace l'allocateur mallocng de musl par [mimalloc](https://github.com/microsoft/mimalloc) pour des performances améliorées
|
||||
- `RELEASE` : (uniquement pour les mainteneurs) lorsque défini, le binaire résultant sera uploadé sur GitHub
|
||||
|
||||
## Extensions
|
||||
|
||||
Avec la glibc ou les binaires basés sur macOS, vous pouvez charger des extensions PHP dynamiquement. Cependant, ces extensions devront être compilées avec le support ZTS.
|
||||
Comme la plupart des gestionnaires de paquets ne proposent pas de versions ZTS de leurs extensions, vous devrez les compiler vous-même.
|
||||
|
||||
Pour cela, vous pouvez construire et exécuter le conteneur Docker `static-builder-gnu`, vous y connecter à distance et compiler les extensions avec `./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config`.
|
||||
|
||||
Exemple d'étapes pour [l'extension Xdebug](https://xdebug.org) :
|
||||
|
||||
```console
|
||||
docker build -t gnu-ext -f static-builder-gnu.Dockerfile --build-arg FRANKENPHP_VERSION=1.0 .
|
||||
docker create --name static-builder-gnu -it gnu-ext /bin/sh
|
||||
docker start static-builder-gnu
|
||||
docker exec -it static-builder-gnu /bin/sh
|
||||
cd /go/src/app/dist/static-php-cli/buildroot/bin
|
||||
git clone https://github.com/xdebug/xdebug.git && cd xdebug
|
||||
source scl_source enable devtoolset-10
|
||||
../phpize
|
||||
./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config
|
||||
make
|
||||
exit
|
||||
docker cp static-builder-gnu:/go/src/app/dist/static-php-cli/buildroot/bin/xdebug/modules/xdebug.so xdebug-zts.so
|
||||
docker cp static-builder-gnu:/go/src/app/dist/frankenphp-linux-$(uname -m) ./frankenphp
|
||||
docker stop static-builder-gnu
|
||||
docker rm static-builder-gnu
|
||||
docker rmi gnu-ext
|
||||
```
|
||||
|
||||
Cela aura créé `frankenphp` et `xdebug-zts.so` dans le répertoire courant.
|
||||
Si vous déplacez `xdebug-zts.so` dans votre répertoire d'extension, ajoutez `zend_extension=xdebug-zts.so` à votre php.ini
|
||||
et lancez FrankenPHP, il chargera Xdebug.
|
||||
|
||||
@@ -28,8 +28,18 @@ frankenphp php-server --worker /path/to/your/worker/script.php
|
||||
Si votre application PHP est [intégrée dans le binaire](embed.md), vous pouvez également ajouter un `Caddyfile` personnalisé dans le répertoire racine de l'application.
|
||||
Il sera utilisé automatiquement.
|
||||
|
||||
Il est également possible de [redémarrer le worker en cas de changement de fichier](config.md#surveillance-des-modifications-de-fichier) avec l'option `--watch`.
|
||||
La commande suivante déclenchera un redémarrage si un fichier se terminant par `.php` dans le répertoire `/path/to/your/app/` ou ses sous-répertoires est modifié :
|
||||
|
||||
```console
|
||||
frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to/your/app/**/*.php"
|
||||
```
|
||||
|
||||
## Runtime Symfony
|
||||
|
||||
> [!TIP]
|
||||
> La section suivante est nécessaire uniquement avant Symfony 7.4, où le support natif du mode worker de FrankenPHP a été introduit.
|
||||
|
||||
Le mode worker de FrankenPHP est pris en charge par le [Composant Runtime de Symfony](https://symfony.com/doc/current/components/runtime.html).
|
||||
Pour démarrer une application Symfony dans un worker, installez le package FrankenPHP de [PHP Runtime](https://github.com/php-runtime/runtime) :
|
||||
|
||||
@@ -76,14 +86,17 @@ $handler = static function () use ($myApp) {
|
||||
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
|
||||
};
|
||||
|
||||
for ($nbRequests = 0, $running = true; isset($_SERVER['MAX_REQUESTS']) && ($nbRequests < ((int)$_SERVER['MAX_REQUESTS'])) && $running; ++$nbRequests) {
|
||||
$running = \frankenphp_handle_request($handler);
|
||||
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
|
||||
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
|
||||
$keepRunning = \frankenphp_handle_request($handler);
|
||||
|
||||
// Faire quelque chose après l'envoi de la réponse HTTP
|
||||
$myApp->terminate();
|
||||
|
||||
// Exécuter le ramasse-miettes pour réduire les chances qu'il soit déclenché au milieu de la génération d'une page
|
||||
gc_collect_cycles();
|
||||
|
||||
if (!$keepRunning) break;
|
||||
}
|
||||
|
||||
// Nettoyage
|
||||
@@ -118,13 +131,46 @@ Une solution pour utiliser ce type de code en mode worker est de redémarrer le
|
||||
|
||||
Le code du worker précédent permet de configurer un nombre maximal de requêtes à traiter en définissant une variable d'environnement nommée `MAX_REQUESTS`.
|
||||
|
||||
### Redémarrer les workers manuellement
|
||||
|
||||
Bien qu'il soit possible de redémarrer les workers [en cas de changement de fichier](config.md#surveillance-des-modifications-de-fichier),
|
||||
il est également possible de redémarrer tous les workers de manière élégante via l'[API Admin de Caddy](https://caddyserver.com/docs/api).
|
||||
Si l'administration est activée dans votre [Caddyfile](config.md#configuration-du-caddyfile), vous pouvez envoyer un ping
|
||||
à l'endpoint de redémarrage avec une simple requête POST comme celle-ci :
|
||||
|
||||
```console
|
||||
curl -X POST http://localhost:2019/frankenphp/workers/restart
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> C'est une fonctionnalité expérimentale et peut être modifiée ou supprimée dans le futur.
|
||||
|
||||
### Worker Failures
|
||||
|
||||
Si un script de worker se plante avec un code de sortie non nul, FrankenPHP le redémarre avec une stratégie de backoff exponentielle.
|
||||
Si le script worker reste en place plus longtemps que le dernier backoff \* 2, FrankenPHP ne pénalisera pas le script et le redémarrera à nouveau.
|
||||
Toutefois, si le script de worker continue d'échouer avec un code de sortie non nul dans un court laps de temps
|
||||
(par exemple, une faute de frappe dans un script), FrankenPHP plantera avec l'erreur : `too many consecutive failures` (trop d'échecs consécutifs).
|
||||
|
||||
Le nombre d'échecs consécutifs peut être configuré dans votre [Caddyfile](config.md#configuration-du-caddyfile) avec l'option `max_consecutive_failures` :
|
||||
|
||||
```caddyfile
|
||||
frankenphp {
|
||||
worker {
|
||||
# ...
|
||||
max_consecutive_failures 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comportement des superglobales
|
||||
|
||||
[Les superglobales PHP](https://www.php.net/manual/fr/language.variables.superglobals.php) (`$_SERVER`, `$_ENV`, `$_GET`...)
|
||||
se comportent comme suit :
|
||||
|
||||
* avant le premier appel à `frankenphp_handle_request()`, les superglobales contiennent des valeurs liées au script worker lui-même
|
||||
* pendant et après l'appel à `frankenphp_handle_request()`, les superglobales contiennent des valeurs générées à partir de la requête HTTP traitée, chaque appel à `frankenphp_handle_request()` change les valeurs des superglobales
|
||||
- avant le premier appel à `frankenphp_handle_request()`, les superglobales contiennent des valeurs liées au script worker lui-même
|
||||
- pendant et après l'appel à `frankenphp_handle_request()`, les superglobales contiennent des valeurs générées à partir de la requête HTTP traitée, chaque appel à `frankenphp_handle_request()` change les valeurs des superglobales
|
||||
|
||||
Pour accéder aux superglobales du script worker à l'intérieur de la fonction de rappel, vous devez les copier et importer la copie dans le scope de la fonction :
|
||||
|
||||
|
||||
71
docs/fr/x-sendfile.md
Normal file
71
docs/fr/x-sendfile.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Servir efficacement les gros fichiers statiques (`X-Sendfile`/`X-Accel-Redirect`)
|
||||
|
||||
Habituellement, les fichiers statiques peuvent être servis directement par le serveur web,
|
||||
mais parfois, il est nécessaire d'exécuter du code PHP avant de les envoyer :
|
||||
contrôle d'accès, statistiques, en-têtes HTTP personnalisés...
|
||||
|
||||
Malheureusement, utiliser PHP pour servir de gros fichiers statiques est inefficace comparé à
|
||||
l'utilisation directe du serveur web (surcharge mémoire, diminution des performances...).
|
||||
|
||||
FrankenPHP permet de déléguer l'envoi des fichiers statiques au serveur web
|
||||
**après** avoir exécuté du code PHP personnalisé.
|
||||
|
||||
Pour ce faire, votre application PHP n'a qu'à définir un en-tête HTTP personnalisé
|
||||
contenant le chemin du fichier à servir. FrankenPHP se chargera du reste.
|
||||
|
||||
Cette fonctionnalité est connue sous le nom de **`X-Sendfile`** pour Apache, et **`X-Accel-Redirect`** pour NGINX.
|
||||
|
||||
Dans les exemples suivants, nous supposons que le "document root" du projet est le répertoire `public/`
|
||||
et que nous voulons utiliser PHP pour servir des fichiers stockés en dehors du dossier `public/`,
|
||||
depuis un répertoire nommé `private-files/`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Tout d'abord, ajoutez la configuration suivante à votre `Caddyfile` pour activer cette fonctionnalité :
|
||||
|
||||
```patch
|
||||
root public/
|
||||
# ...
|
||||
|
||||
+ # Needed for Symfony, Laravel and other projects using the Symfony HttpFoundation component
|
||||
+ request_header X-Sendfile-Type x-accel-redirect
|
||||
+ request_header X-Accel-Mapping ../private-files=/private-files
|
||||
+
|
||||
+ intercept {
|
||||
+ @accel header X-Accel-Redirect *
|
||||
+ handle_response @accel {
|
||||
+ root private-files/
|
||||
+ rewrite * {resp.header.X-Accel-Redirect}
|
||||
+ method * GET
|
||||
+
|
||||
+ # Remove the X-Accel-Redirect header set by PHP for increased security
|
||||
+ header -X-Accel-Redirect
|
||||
+
|
||||
+ file_server
|
||||
+ }
|
||||
+ }
|
||||
|
||||
php_server
|
||||
```
|
||||
|
||||
## PHP simple
|
||||
|
||||
Définissez le chemin relatif du fichier (à partir de `private-files/`) comme valeur de l'en-tête `X-Accel-Redirect` :
|
||||
|
||||
```php
|
||||
header('X-Accel-Redirect: file.txt') ;
|
||||
```
|
||||
|
||||
## Projets utilisant le composant Symfony HttpFoundation (Symfony, Laravel, Drupal...)
|
||||
|
||||
Symfony HttpFoundation [supporte nativement cette fonctionnalité](https://symfony.com/doc/current/components/http_foundation.html#serving-files).
|
||||
Il va automatiquement déterminer la bonne valeur pour l'en-tête `X-Accel-Redirect` et l'ajoutera à la réponse.
|
||||
|
||||
```php
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
BinaryFileResponse::trustXSendfileTypeHeader();
|
||||
$response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt');
|
||||
|
||||
// ...
|
||||
```
|
||||
@@ -7,7 +7,7 @@ every approved pull request or on your own fork once setup.
|
||||
|
||||
In the repository settings, under secrets, add the following secrets:
|
||||
|
||||
- `REGISTRY_LOGIN_SERVER`: The docker registry to use (e.g. `docker.io`).
|
||||
- `REGISTRY_LOGIN_SERVER`: The Docker registry to use (e.g. `docker.io`).
|
||||
- `REGISTRY_USERNAME`: The username to use to log in to the registry (e.g. `dunglas`).
|
||||
- `REGISTRY_PASSWORD`: The password to use to log in to the registry (e.g. an access key).
|
||||
- `IMAGE_NAME`: The name of the image (e.g. `dunglas/frankenphp`).
|
||||
|
||||
219
docs/ja/CONTRIBUTING.md
Normal file
219
docs/ja/CONTRIBUTING.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# コントリビューション
|
||||
|
||||
## PHPのコンパイル
|
||||
|
||||
### Dockerを使用する場合(Linux)
|
||||
|
||||
開発用Dockerイメージをビルドします:
|
||||
|
||||
```console
|
||||
docker build -t frankenphp-dev -f dev.Dockerfile .
|
||||
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev
|
||||
```
|
||||
|
||||
このイメージには通常の開発ツール(Go、GDB、Valgrind、Neovimなど)が含まれており、PHP設定ファイルは以下の場所に配置されます。
|
||||
|
||||
- php.ini: `/etc/frankenphp/php.ini` 開発用のプリセットが適用されたphp.iniファイルがデフォルトで提供されます。
|
||||
- 追加の設定ファイル: `/etc/frankenphp/php.d/*.ini`
|
||||
- PHP拡張モジュール: `/usr/lib/frankenphp/modules/`
|
||||
|
||||
お使いのDockerのバージョンが23.0未満の場合、dockerignore[パターンの問題](https://github.com/moby/moby/pull/42676)によりビルドに失敗する可能性があります。以下のように`.dockerignore`にディレクトリを追加してください。
|
||||
|
||||
```patch
|
||||
!testdata/*.php
|
||||
!testdata/*.txt
|
||||
+!caddy
|
||||
+!internal
|
||||
```
|
||||
|
||||
### Dockerを使用しない場合(LinuxおよびmacOS)
|
||||
|
||||
[ソースからのコンパイル手順](https://frankenphp.dev/docs/compile/)に従い、`--debug`設定フラグを渡してください。
|
||||
|
||||
## テストスイートの実行
|
||||
|
||||
```console
|
||||
go test -tags watcher -race -v ./...
|
||||
```
|
||||
|
||||
## Caddyモジュール
|
||||
|
||||
FrankenPHPのCaddyモジュール付きでCaddyをビルドします:
|
||||
|
||||
```console
|
||||
cd caddy/frankenphp/
|
||||
go build -tags watcher,brotli,nobadger,nomysql,nopgx
|
||||
cd ../../
|
||||
```
|
||||
|
||||
FrankenPHPのCaddyモジュール付きでCaddyを実行します:
|
||||
|
||||
```console
|
||||
cd testdata/
|
||||
../caddy/frankenphp/frankenphp run
|
||||
```
|
||||
|
||||
サーバーは`127.0.0.1:80`で待ち受けています:
|
||||
|
||||
> [!NOTE]
|
||||
> Dockerを使用している場合は、コンテナのポート80をバインドするか、コンテナ内で実行する必要があります。
|
||||
|
||||
```console
|
||||
curl -vk http://127.0.0.1/phpinfo.php
|
||||
```
|
||||
|
||||
## 最小構成のテストサーバー
|
||||
|
||||
最小構成のテストサーバーをビルドします:
|
||||
|
||||
```console
|
||||
cd internal/testserver/
|
||||
go build
|
||||
cd ../../
|
||||
```
|
||||
|
||||
テストサーバーを実行します:
|
||||
|
||||
```console
|
||||
cd testdata/
|
||||
../internal/testserver/testserver
|
||||
```
|
||||
|
||||
サーバーは`127.0.0.1:8080`で待ち受けています:
|
||||
|
||||
```console
|
||||
curl -v http://127.0.0.1:8080/phpinfo.php
|
||||
```
|
||||
|
||||
## Dockerイメージをローカルでビルドする
|
||||
|
||||
bakeプランを出力します:
|
||||
|
||||
```console
|
||||
docker buildx bake -f docker-bake.hcl --print
|
||||
```
|
||||
|
||||
amd64用のFrankenPHPイメージをローカルでビルドします:
|
||||
|
||||
```console
|
||||
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/amd64"
|
||||
```
|
||||
|
||||
arm64用のFrankenPHPイメージをローカルでビルドします:
|
||||
|
||||
```console
|
||||
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/arm64"
|
||||
```
|
||||
|
||||
arm64とamd64用のFrankenPHPイメージをスクラッチからビルドしてDocker Hubにプッシュします:
|
||||
|
||||
```console
|
||||
docker buildx bake -f docker-bake.hcl --pull --no-cache --push
|
||||
```
|
||||
|
||||
## 静的ビルドでのセグメンテーション違反のデバッグ
|
||||
|
||||
1. GitHubからFrankenPHPバイナリのデバッグ版をダウンロードするか、デバッグシンボルを含む独自の静的ビルドを作成します:
|
||||
|
||||
```console
|
||||
docker buildx bake \
|
||||
--load \
|
||||
--set static-builder.args.DEBUG_SYMBOLS=1 \
|
||||
--set "static-builder.platform=linux/amd64" \
|
||||
static-builder
|
||||
docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
|
||||
```
|
||||
|
||||
2. 現在使用している`frankenphp`を、デバッグ版のFrankenPHP実行ファイルに置き換えます
|
||||
3. 通常通りFrankenPHPを起動します(あるいは、GDBで直接FrankenPHPを開始することもできます:`gdb --args frankenphp run`)
|
||||
4. GDBでプロセスにアタッチします:
|
||||
|
||||
```console
|
||||
gdb -p `pidof frankenphp`
|
||||
```
|
||||
|
||||
5. 必要に応じて、GDBシェルで`continue`と入力します
|
||||
6. FrankenPHPをクラッシュさせます
|
||||
7. GDBシェルで`bt`と入力します
|
||||
8. 出力結果をコピーします
|
||||
|
||||
## GitHub Actionsでのセグメンテーション違反のデバッグ
|
||||
|
||||
1. `.github/workflows/tests.yml`を開きます
|
||||
2. PHPデバッグシンボルを有効にします
|
||||
|
||||
```patch
|
||||
- uses: shivammathur/setup-php@v2
|
||||
# ...
|
||||
env:
|
||||
phpts: ts
|
||||
+ debug: true
|
||||
```
|
||||
|
||||
3. `tmate`を有効にしてコンテナに接続できるようにします
|
||||
|
||||
```patch
|
||||
- name: Set CGO flags
|
||||
run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV"
|
||||
+ - run: |
|
||||
+ sudo apt install gdb
|
||||
+ mkdir -p /home/runner/.config/gdb/
|
||||
+ printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
|
||||
+ - uses: mxschmitt/action-tmate@v3
|
||||
```
|
||||
|
||||
4. コンテナに接続します
|
||||
5. `frankenphp.go`を開きます
|
||||
6. `cgosymbolizer`を有効にします
|
||||
|
||||
```patch
|
||||
- //_ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
+ _ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
```
|
||||
|
||||
7. モジュールを取得します:`go get`
|
||||
8. コンテナ内で、GDBなどを使用できます:
|
||||
|
||||
```console
|
||||
go test -tags watcher -c -ldflags=-w
|
||||
gdb --args frankenphp.test -test.run ^MyTest$
|
||||
```
|
||||
|
||||
9. バグが修正されたら、これらの変更をすべて元に戻します
|
||||
|
||||
## その他の開発リソース
|
||||
|
||||
- [uWSGIでのPHP埋め込み](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
|
||||
- [NGINX UnitでのPHP埋め込み](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
|
||||
- [Go言語でのPHP埋め込み (go-php)](https://github.com/deuill/go-php)
|
||||
- [Go言語でのPHP埋め込み (GoEmPHP)](https://github.com/mikespook/goemphp)
|
||||
- [C++でのPHP埋め込み](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
|
||||
- [Sara Golemon 著『Extending and Embedding PHP』](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
|
||||
- [TSRMLS_CCとは何か?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
|
||||
- [SDL バインディング](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)
|
||||
|
||||
## Docker関連リソース
|
||||
|
||||
- [Bakeファイル定義](https://docs.docker.com/build/customize/bake/file-definition/)
|
||||
- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)
|
||||
|
||||
## 便利なコマンド
|
||||
|
||||
```console
|
||||
apk add strace util-linux gdb
|
||||
strace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1
|
||||
```
|
||||
|
||||
## ドキュメントの翻訳
|
||||
|
||||
新しい言語でドキュメントとサイトを翻訳するには、
|
||||
以下の手順で行ってください。
|
||||
|
||||
1. このリポジトリの`docs/`ディレクトリに、言語の2文字ISOコードを名前にした新しいディレクトリを作成します
|
||||
2. `docs/`ディレクトリのルートにある全ての`.md`ファイルを新しいディレクトリにコピーします(翻訳のソースとして常に英語版を使用してください。英語版が最新版だからです)
|
||||
3. ルートディレクトリから`README.md`と`CONTRIBUTING.md`ファイルを新しいディレクトリにコピーします
|
||||
4. ファイルの内容を翻訳しますが、ファイル名は変更せず、`> [!`で始まる文字列も翻訳しないでください(これはGitHub用の特別なマークアップです)
|
||||
5. 翻訳でプルリクエストを作成します
|
||||
6. [サイトリポジトリ](https://github.com/dunglas/frankenphp-website/tree/main)で、`content/`、`data/`、`i18n/`ディレクトリの翻訳ファイルをコピーして翻訳します
|
||||
7. 作成されたYAMLファイルの値を翻訳します
|
||||
8. サイトリポジトリでプルリクエストを開きます
|
||||
157
docs/ja/README.md
Normal file
157
docs/ja/README.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# FrankenPHP: PHPのためのモダンなアプリケーションサーバー
|
||||
|
||||
<h1 align="center"><a href="https://frankenphp.dev"><img src="frankenphp.png" alt="FrankenPHP" width="600"></a></h1>
|
||||
|
||||
FrankenPHPは、[Caddy](https://caddyserver.com/) Webサーバーをベースに構築された、PHPのためのモダンなアプリケーションサーバーです。
|
||||
|
||||
FrankenPHPは、[_Early Hints_](https://frankenphp.dev/docs/early-hints/)、[ワーカーモード](https://frankenphp.dev/docs/worker/)、[リアルタイム機能](https://frankenphp.dev/docs/mercure/)、自動HTTPS、HTTP/2、HTTP/3などの驚異的な機能により、あなたのPHPアプリに強力な力を与えます。
|
||||
|
||||
FrankenPHPはあらゆるPHPアプリと連携し、ワーカーモードの公式統合によってLaravelやSymfonyプロジェクトをこれまで以上に高速化します。
|
||||
|
||||
また、FrankenPHPはスタンドアロンのGoライブラリとしても利用可能で、`net/http`を使って任意のアプリにPHPを埋め込むことができます。
|
||||
|
||||
[**詳しくは** _frankenphp.dev_](https://frankenphp.dev)と、このスライド資料もご参照ください:
|
||||
|
||||
<a href="https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/"><img src="https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png" alt="Slides" width="600"></a>
|
||||
|
||||
## はじめに
|
||||
|
||||
Windowsをお使いの場合は、[WSL](https://learn.microsoft.com/windows/wsl/)を使用してFrankenPHPを実行してください。
|
||||
|
||||
### インストールスクリプト
|
||||
|
||||
以下のコマンドをターミナルに貼り付けると、環境に合ったバージョンが自動的にインストールされます:
|
||||
|
||||
```console
|
||||
curl https://frankenphp.dev/install.sh | sh
|
||||
```
|
||||
|
||||
### スタンドアロンバイナリ
|
||||
|
||||
LinuxとmacOS向けに、開発用途の静的FrankenPHPバイナリを提供しています。
|
||||
[PHP 8.4](https://www.php.net/releases/8.4/en.php)と主要なPHP拡張が含まれます。
|
||||
|
||||
[FrankenPHPをダウンロード](https://github.com/php/frankenphp/releases)
|
||||
|
||||
**拡張のインストール:** よく使われる拡張は同梱されています。追加の拡張をインストールすることはできません。
|
||||
|
||||
### rpm パッケージ
|
||||
|
||||
メンテナーが `dnf` を使用するすべてのシステム向けに rpm パッケージを提供しています。インストール方法:
|
||||
|
||||
```console
|
||||
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
|
||||
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 利用可能
|
||||
sudo dnf install frankenphp
|
||||
```
|
||||
|
||||
**拡張のインストール:** `sudo dnf install php-zts-<extension>`
|
||||
|
||||
デフォルトで提供されていない拡張については [PIE](https://github.com/php/pie) を使用してください:
|
||||
|
||||
```console
|
||||
sudo dnf install pie-zts
|
||||
sudo pie-zts install asgrim/example-pie-extension
|
||||
```
|
||||
|
||||
### deb パッケージ
|
||||
|
||||
メンテナーが `apt` を使用するすべてのシステム向けに deb パッケージを提供しています。インストール方法:
|
||||
|
||||
```console
|
||||
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" | sudo tee /etc/apt/sources.list.d/static-php.list && \
|
||||
sudo apt update
|
||||
sudo apt install frankenphp
|
||||
```
|
||||
|
||||
**拡張のインストール:** `sudo apt install php-zts-<extension>`
|
||||
|
||||
デフォルトで提供されていない拡張については [PIE](https://github.com/php/pie) を使用してください:
|
||||
|
||||
```console
|
||||
sudo apt install pie-zts
|
||||
sudo pie-zts install asgrim/example-pie-extension
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
また、[Dockerイメージ](https://frankenphp.dev/docs/docker/)も利用可能です:
|
||||
|
||||
```console
|
||||
docker run -v .:/app/public \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
|
||||
ブラウザで`https://localhost`にアクセスして、FrankenPHPをお楽しみください!
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> `https://127.0.0.1`ではなく、`https://localhost`を使用して、自己署名証明書を受け入れてください。
|
||||
> 使用するドメインを変更したい場合は、[`SERVER_NAME` 環境変数](docs/config.md#environment-variables)を設定してください。
|
||||
|
||||
### Homebrew
|
||||
|
||||
FrankenPHPはmacOSおよびLinux向けに[Homebrew](https://brew.sh)パッケージとしても利用可能です。
|
||||
|
||||
インストール方法:
|
||||
|
||||
```console
|
||||
brew install dunglas/frankenphp/frankenphp
|
||||
```
|
||||
|
||||
**拡張のインストール:** [PIE](https://github.com/php/pie) を使用してください。
|
||||
|
||||
### 使い方
|
||||
|
||||
現在のディレクトリのコンテンツを配信するには、以下を実行してください:
|
||||
|
||||
```console
|
||||
frankenphp php-server
|
||||
```
|
||||
|
||||
コマンドラインスクリプトも実行できます:
|
||||
|
||||
```console
|
||||
frankenphp php-cli /path/to/your/script.php
|
||||
```
|
||||
|
||||
deb / rpm パッケージの場合は、systemd サービスを起動することもできます:
|
||||
|
||||
```console
|
||||
sudo systemctl start frankenphp
|
||||
```
|
||||
|
||||
## ドキュメント
|
||||
|
||||
- [クラシックモード](https://frankenphp.dev/docs/classic/)
|
||||
- [ワーカーモード](https://frankenphp.dev/docs/worker/)
|
||||
- [Early Hintsサポート(103 HTTPステータスコード)](https://frankenphp.dev/docs/early-hints/)
|
||||
- [リアルタイム](https://frankenphp.dev/docs/mercure/)
|
||||
- [大きな静的ファイルの効率的な提供](https://frankenphp.dev/docs/x-sendfile/)
|
||||
- [設定](https://frankenphp.dev/docs/config/)
|
||||
- [Dockerイメージ](https://frankenphp.dev/docs/docker/)
|
||||
- [本番環境でのデプロイ](https://frankenphp.dev/docs/production/)
|
||||
- [パフォーマンス最適化](https://frankenphp.dev/docs/performance/)
|
||||
- [**スタンドアロン**、自己実行可能なPHPアプリの作成](https://frankenphp.dev/docs/embed/)
|
||||
- [静的バイナリの作成](https://frankenphp.dev/docs/static/)
|
||||
- [ソースからのコンパイル](https://frankenphp.dev/docs/compile/)
|
||||
- [FrankenPHPの監視](https://frankenphp.dev/docs/metrics/)
|
||||
- [Laravel統合](https://frankenphp.dev/docs/laravel/)
|
||||
- [既知の問題](https://frankenphp.dev/docs/known-issues/)
|
||||
- [デモアプリ(Symfony)とベンチマーク](https://github.com/dunglas/frankenphp-demo)
|
||||
- [Goライブラリドキュメント](https://pkg.go.dev/github.com/dunglas/frankenphp)
|
||||
- [コントリビューションとデバッグ](https://frankenphp.dev/docs/contributing/)
|
||||
|
||||
## 例とスケルトン
|
||||
|
||||
- [Symfony](https://github.com/dunglas/symfony-docker)
|
||||
- [API Platform](https://api-platform.com/docs/symfony)
|
||||
- [Laravel](https://frankenphp.dev/docs/laravel/)
|
||||
- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
|
||||
- [WordPress](https://github.com/StephenMiracle/frankenwp)
|
||||
- [Drupal](https://github.com/dunglas/frankenphp-drupal)
|
||||
- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
|
||||
- [TYPO3](https://github.com/ochorocho/franken-typo3)
|
||||
- [Magento2](https://github.com/ekino/frankenphp-magento2)
|
||||
11
docs/ja/classic.md
Normal file
11
docs/ja/classic.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# クラシックモードの使用
|
||||
|
||||
追加の設定を行わなくても、FrankenPHPはクラシックモードで動作します。このモードでは、FrankenPHPは従来のPHPサーバーのように機能し、PHPファイルを直接提供します。これにより、PHP-FPMやmod_phpを使ったApacheの置き換えとしてシームレスに利用できます。
|
||||
|
||||
Caddyと同様に、FrankenPHPは無制限の接続を受け付け、[固定数のスレッド](config.md#caddyfile-config)でそれらを処理します。受け入れられキューに入れられる接続の数は、利用可能なシステムリソースによってのみ制限されます。
|
||||
PHPスレッドプールは、起動時に初期化された固定数のスレッドで動作し、これはPHP-FPMの静的モードに相当します。また、PHP-FPMの動的モードと同様に、[実行時にスレッドを自動的にスケール](performance.md#max_threads)させることも可能です。
|
||||
|
||||
キューに入った接続は、PHPスレッドが空くまで無期限に待機します。これを避けるために、FrankenPHP のグローバル設定内の `max_wait_time` [設定](config.md#caddyfile-config)を使って、リクエストが空きスレッドを待てる最大時間を制限し、それを超えるとリクエストが拒否されるようにできます。
|
||||
加えて、[Caddy側で適切な書き込みタイムアウト](https://caddyserver.com/docs/caddyfile/options#timeouts)を設定することも可能です。
|
||||
|
||||
各Caddyインスタンスは、1つのFrankenPHPスレッドプールのみを起動し、すべての`php_server`ブロック間でこのプールを共有します。
|
||||
127
docs/ja/compile.md
Normal file
127
docs/ja/compile.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# ソースからのコンパイル
|
||||
|
||||
このドキュメントでは、PHPを動的ライブラリとしてロードするFrankenPHPバイナリの作成方法を説明します。
|
||||
これが推奨される方法です。
|
||||
|
||||
または、[完全静的およびほぼ静的なビルド](static.md)も作成できます。
|
||||
|
||||
## PHPのインストール
|
||||
|
||||
FrankenPHPはPHP 8.2以上と互換性があります。
|
||||
|
||||
### Homebrewを使用する場合(LinuxとMac)
|
||||
|
||||
FrankenPHPと互換性のあるlibphpのバージョンをインストールする最も簡単な方法は、[Homebrew PHP](https://github.com/shivammathur/homebrew-php)が提供するZTSパッケージを使用することです。
|
||||
|
||||
まず、まだインストールしていない場合は[Homebrew](https://brew.sh)をインストールしてください。
|
||||
|
||||
次に、PHPのZTSバリアント、Brotli(オプション、圧縮サポート用)、watcher(オプション、ファイル変更検出用)をインストールします:
|
||||
|
||||
```console
|
||||
brew install shivammathur/php/php-zts brotli watcher
|
||||
brew link --overwrite --force shivammathur/php/php-zts
|
||||
```
|
||||
|
||||
### PHPをコンパイルする場合
|
||||
|
||||
別の方法として、FrankenPHPに必要なオプションを指定してPHPをソースからコンパイルすることもできます。
|
||||
|
||||
まず、[PHPのソース](https://www.php.net/downloads.php)を取得して展開します:
|
||||
|
||||
```console
|
||||
tar xf php-*
|
||||
cd php-*/
|
||||
```
|
||||
|
||||
次に、プラットフォームに応じて必要なオプションを指定して`configure`スクリプトを実行します。
|
||||
以下の`./configure`フラグは必須ですが、例えば拡張機能モジュールや追加機能をコンパイルするために他のフラグを追加することもできます。
|
||||
|
||||
#### Linux
|
||||
|
||||
```console
|
||||
./configure \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--enable-zend-max-execution-timers
|
||||
```
|
||||
|
||||
#### Mac
|
||||
|
||||
[Homebrew](https://brew.sh/)パッケージマネージャーを使用して、必須およびオプションの依存関係をインストールします:
|
||||
|
||||
```console
|
||||
brew install libiconv bison brotli re2c pkg-config watcher
|
||||
echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
|
||||
```
|
||||
|
||||
その後、以下のようにconfigureスクリプトを実行します:
|
||||
|
||||
```console
|
||||
./configure \
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals \
|
||||
--with-iconv=/opt/homebrew/opt/libiconv/
|
||||
```
|
||||
|
||||
#### PHPのコンパイル
|
||||
|
||||
最後に、PHPをコンパイルしてインストールします:
|
||||
|
||||
```console
|
||||
make -j"$(getconf _NPROCESSORS_ONLN)"
|
||||
sudo make install
|
||||
```
|
||||
|
||||
## オプション依存関係のインストール
|
||||
|
||||
FrankenPHPの一部の機能は、システムにインストールされているオプションの依存パッケージに依存しています。
|
||||
または、Goコンパイラにビルドタグを渡すことで、これらの機能を無効にできます。
|
||||
|
||||
| 機能 | 依存関係 | 無効にするためのビルドタグ |
|
||||
| ------------------------------ | --------------------------------------------------------------------- | -------------------------- |
|
||||
| Brotli圧縮 | [Brotli](https://github.com/google/brotli) | nobrotli |
|
||||
| ファイル変更時のワーカー再起動 | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
|
||||
|
||||
## Goアプリのコンパイル
|
||||
|
||||
いよいよ最終的なバイナリをビルドできるようになりました。
|
||||
|
||||
### xcaddyを使う場合
|
||||
|
||||
推奨される方法は、[xcaddy](https://github.com/caddyserver/xcaddy)を使用してFrankenPHPをコンパイルする方法です。
|
||||
`xcaddy`を使うと、[Caddyのカスタムモジュール](https://caddyserver.com/docs/modules/)やFrankenPHP拡張を簡単に追加できます:
|
||||
|
||||
```console
|
||||
CGO_ENABLED=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
||||
CGO_CFLAGS=$(php-config --includes) \
|
||||
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
||||
xcaddy build \
|
||||
--output frankenphp \
|
||||
--with github.com/dunglas/frankenphp/caddy \
|
||||
--with github.com/dunglas/mercure/caddy \
|
||||
--with github.com/dunglas/vulcain/caddy
|
||||
# 追加のCaddyモジュールとFrankenPHP拡張をここに追加
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> musl libc(Alpine Linuxのデフォルト)とSymfonyを使用している場合、
|
||||
> デフォルトのスタックサイズを増やす必要がある場合があります。
|
||||
> そうしないと、`PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`のようなエラーが発生する可能性があります。
|
||||
>
|
||||
> これを行うには、`XCADDY_GO_BUILD_FLAGS`環境変数を
|
||||
> `XCADDY_GO_BUILD_FLAGS=$'-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'`のようなものに変更してください
|
||||
> (アプリの要件に応じてスタックサイズの値を変更してください)。
|
||||
|
||||
### xcaddyを使用しない場合
|
||||
|
||||
代替として、`xcaddy`を使わずに`go`コマンドを直接使ってFrankenPHPをコンパイルすることも可能です:
|
||||
|
||||
```console
|
||||
curl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz
|
||||
cd frankenphp-main/caddy/frankenphp
|
||||
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build -tags=nobadger,nomysql,nopgx
|
||||
```
|
||||
306
docs/ja/config.md
Normal file
306
docs/ja/config.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# 設定
|
||||
|
||||
FrankenPHP、Caddy、そしてMercureやVulcainモジュールは、[Caddyでサポートされる形式](https://caddyserver.com/docs/getting-started#your-first-config)を使用して設定できます。
|
||||
|
||||
[Dockerイメージ](docker.md)では、`Caddyfile`は`/etc/frankenphp/Caddyfile`に配置されています。
|
||||
静的バイナリは、`frankenphp run`コマンドを実行したディレクトリ内の`Caddyfile`を参照します。
|
||||
また、`-c`または`--config`オプションでカスタムのパスを指定できます。
|
||||
|
||||
PHP自体の設定は[`php.ini` ファイルを使用](https://www.php.net/manual/en/configuration.file.php)して行えます。
|
||||
|
||||
インストール方法に応じて、PHPインタープリターは上記いずれかの場所にある設定ファイルを参照します。
|
||||
|
||||
## Docker
|
||||
|
||||
- `php.ini`: `/usr/local/etc/php/php.ini`(デフォルトでは`php.ini`は含まれていません)
|
||||
- 追加の設定ファイル: `/usr/local/etc/php/conf.d/*.ini`
|
||||
- PHP拡張モジュール: `/usr/local/lib/php/extensions/no-debug-zts-<YYYYMMDD>/`
|
||||
- PHPプロジェクトが提供する公式テンプレートをコピーすることを推奨します:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
|
||||
# 本番環境:
|
||||
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
|
||||
|
||||
# または開発環境:
|
||||
RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
|
||||
```
|
||||
|
||||
## RPMおよびDebianパッケージ
|
||||
|
||||
- `php.ini`: `/etc/frankenphp/php.ini`(本番環境向けのプリセットの`php.ini`ファイルがデフォルトで提供されます)
|
||||
- 追加の設定ファイル: `/etc/frankenphp/php.d/*.ini`
|
||||
- PHP拡張モジュール: `/usr/lib/frankenphp/modules/`
|
||||
|
||||
## 静的バイナリ
|
||||
|
||||
- `php.ini`: `frankenphp run`または`frankenphp php-server`を実行したディレクトリ内、なければ`/etc/frankenphp/php.ini`を参照
|
||||
- 追加の設定ファイル: `/etc/frankenphp/php.d/*.ini`
|
||||
- PHP拡張モジュール: ロードできません、バイナリ自体にバンドルする必要があります
|
||||
- [PHPソース](https://github.com/php/php-src/)で提供される`php.ini-production`または`php.ini-development`のいずれかをコピーしてください
|
||||
|
||||
## Caddyfileの設定
|
||||
|
||||
`php_server`または`php`の[HTTPディレクティブ](https://caddyserver.com/docs/caddyfile/concepts#directives)は、サイトブロック内で使用してPHPアプリを配信できます。
|
||||
|
||||
最小構成の例:
|
||||
|
||||
```caddyfile
|
||||
localhost {
|
||||
# 圧縮を有効化(オプション)
|
||||
encode zstd br gzip
|
||||
# 現在のディレクトリ内のPHPファイルを実行し、アセットを配信
|
||||
php_server
|
||||
}
|
||||
```
|
||||
|
||||
グローバルオプションを使用してFrankenPHPを明示的に設定することもできます:
|
||||
`frankenphp`の[グローバルオプション](https://caddyserver.com/docs/caddyfile/concepts#global-options)を使用してFrankenPHPを構成できます。
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
num_threads <num_threads> # 開始するPHPスレッド数を設定します。デフォルト: 利用可能なCPU数の2倍。
|
||||
max_threads <num_threads> # 実行時に追加で開始できるPHPスレッドの最大数を制限します。デフォルト: num_threads。 'auto'を設定可能。
|
||||
max_wait_time <duration> # リクエストがタイムアウトする前にPHPのスレッドが空くのを待つ最大時間を設定します。デフォルト: 無効。
|
||||
php_ini <key> <value> # php.iniのディレクティブを設定します。複数のディレクティブを設定するために何度でも使用できます。
|
||||
worker {
|
||||
file <path> # ワーカースクリプトのパスを設定します。
|
||||
num <num> # 開始するPHPスレッド数を設定します。デフォルト: 利用可能なCPU数の2倍。
|
||||
env <key> <value> # 追加の環境変数を指定された値に設定する。複数の環境変数に対して複数回指定することができます。
|
||||
watch <path> # ファイル変更を監視するパスを設定します。複数のパスに対して複数回指定できます。
|
||||
name <name> # ワーカーの名前を設定します。ログとメトリクスで使用されます。デフォルト: ワーカーファイルの絶対パス
|
||||
max_consecutive_failures <num> # workerが不健全とみなされるまでの、連続失敗の最大回数を設定します。 -1 はワーカーを常に再起動することを意味します。デフォルトは 6 です。
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
代わりに、`worker`オプションのワンライナー形式を使用することもできます:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker <file> <num>
|
||||
}
|
||||
}
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
同じサーバーで複数のアプリを提供する場合は、複数のワーカーを定義することもできます:
|
||||
|
||||
```caddyfile
|
||||
app.example.com {
|
||||
root /path/to/app/public
|
||||
php_server {
|
||||
root /path/to/app/public # キャッシュ効率を高める
|
||||
worker index.php <num>
|
||||
}
|
||||
}
|
||||
|
||||
other.example.com {
|
||||
root /path/to/other/public
|
||||
php_server {
|
||||
root /path/to/other/public
|
||||
worker index.php <num>
|
||||
}
|
||||
}
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
通常は`php_server`ディレクティブを使えば十分ですが、
|
||||
より細かい制御が必要な場合は、より低レベルの`php`ディレクティブを使用できます。
|
||||
`php`ディレクティブは、対象がPHPファイルかどうかを確認せず、すべての入力をPHPに渡します。
|
||||
詳しくは[パフォーマンスページ](performance.md#try_files)をお読みください。
|
||||
|
||||
`php_server`ディレクティブの使用は、以下の設定と同等です:
|
||||
|
||||
```caddyfile
|
||||
route {
|
||||
# ディレクトリへのリクエストに末尾スラッシュを追加
|
||||
@canonicalPath {
|
||||
file {path}/index.php
|
||||
not path */
|
||||
}
|
||||
redir @canonicalPath {path}/ 308
|
||||
# 要求されたファイルが存在しない場合は、indexファイルを試行
|
||||
@indexFiles file {
|
||||
try_files {path} {path}/index.php index.php
|
||||
split_path .php
|
||||
}
|
||||
rewrite @indexFiles {http.matchers.file.relative}
|
||||
# FrankenPHP!
|
||||
@phpFiles path *.php
|
||||
php @phpFiles
|
||||
file_server
|
||||
}
|
||||
```
|
||||
|
||||
`php_server`と`php`ディレクティブには以下のオプションがあります:
|
||||
|
||||
```caddyfile
|
||||
php_server [<matcher>] {
|
||||
root <directory> # サイトのルートフォルダを設定します。デフォルト: `root`ディレクティブ。
|
||||
split_path <delim...> # URIを2つの部分に分割するための部分文字列を設定します。最初にマッチする部分文字列がURIから「パス情報」を分割するために使用されます。最初の部分はマッチする部分文字列で接尾辞が付けられ、実際のリソース(CGIスクリプト)名とみなされます。2番目の部分はスクリプトが使用する PATH_INFO に設定されます。デフォルト: `.php`
|
||||
resolve_root_symlink false # シンボリックリンクが存在する場合`root`ディレクトリをシンボリックリンクの評価によって実際の値に解決することを無効にする(デフォルトで有効)。
|
||||
env <key> <value> # 追加の環境変数を指定された値に設定する。複数の環境変数を指定する場合は、複数回指定することができます。
|
||||
file_server off # 組み込みのfile_serverディレクティブを無効にします。
|
||||
worker { # このサーバー固有のワーカーを作成します。複数のワーカーに対して複数回指定できます。
|
||||
file <path> # ワーカースクリプトへのパスを設定します。php_serverのルートからの相対パスとなります。
|
||||
num <num> # 起動するPHPスレッド数を設定します。デフォルトは利用可能なスレッド数の 2 倍です。
|
||||
name <name> # ログとメトリクスで使用されるワーカーの名前を設定します。デフォルト: ワーカーファイルの絶対パス。php_server ブロックで定義されている場合は、常にm#で始まります。
|
||||
watch <path> # ファイルの変更を監視するパスを設定する。複数のパスに対して複数回指定することができる。
|
||||
env <key> <value> # 追加の環境変数を指定された値に設定する。複数の環境変数を指定する場合は、複数回指定することができます。このワーカーの環境変数もphp_serverの親から継承されますが、 ここで上書きすることもできます。
|
||||
match <path> # ワーカーをパスパターンにマッチさせます。try_filesを上書きし、php_serverディレクティブでのみ使用できます。
|
||||
}
|
||||
worker <other_file> <num> # グローバルfrankenphpブロックのような短縮形式も使用できます。
|
||||
}
|
||||
```
|
||||
|
||||
### ファイルの変更監視
|
||||
|
||||
ワーカーはアプリケーションを一度だけ起動してメモリに保持するため、
|
||||
PHPファイルに変更を加えても即座には反映されません。
|
||||
|
||||
代わりに、`watch`ディレクティブを使用してファイル変更時にワーカーを再起動させることができます。
|
||||
これは開発環境において有用です。
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker {
|
||||
file /path/to/app/public/worker.php
|
||||
watch
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`watch`ディレクトリが指定されていない場合、`./**/*.{php,yaml,yml,twig,env}`にフォールバックします。
|
||||
これは、FrankenPHPプロセスが開始されたディレクトリおよびそのサブディレクトリ内のすべての`.php`、`.yaml`、`.yml`、`.twig`、`.env`ファイルすべてを監視します。
|
||||
代わりに、[シェルのファイル名パターン](https://pkg.go.dev/path/filepath#Match)を使用して
|
||||
1つ以上のディレクトリを指定することもできます:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
worker {
|
||||
file /path/to/app/public/worker.php
|
||||
watch /path/to/app # /path/to/app以下すべてのサブディレクトリのファイルを監視
|
||||
watch /path/to/app/*.php # /path/to/app内の.phpで終わるファイルを監視
|
||||
watch /path/to/app/**/*.php # /path/to/appおよびサブディレクトリのPHPファイルを監視
|
||||
watch /path/to/app/**/*.{php,twig} # /path/to/appおよびサブディレクトリ内のPHPとTwigファイルを監視
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `**` パターンは再帰的な監視を意味します
|
||||
- ディレクトリは相対パス(FrankenPHPプロセスの開始ディレクトリから)にもできます
|
||||
- 複数のワーカーが定義されている場合、いずれかのファイルが変更されるとすべてのワーカーが再起動されます
|
||||
- 実行時に生成されるファイル(ログなど)を監視対象に含めると、意図しないワーカーの再起動を引き起こす可能性があるため注意が必要です
|
||||
|
||||
ファイルウォッチャーは[e-dant/watcher](https://github.com/e-dant/watcher)に基づいています。
|
||||
|
||||
## パスにワーカーをマッチさせる
|
||||
|
||||
従来のPHPアプリケーションでは、スクリプトは常にpublicディレクトリに配置されます。
|
||||
これはワーカースクリプトにも当てはまり、他のPHPスクリプトと同様に扱われます。
|
||||
ワーカースクリプトをpublicディレクトリの外に配置したい場合は、`match`ディレクティブを使用して実現できます。
|
||||
|
||||
`match`ディレクティブは、`try_files`の最適化された代替手段であり、`php_server`および`php`の中でのみ使用できます。
|
||||
次の例では、public ディレクトリ内にファイルが存在すればそれを配信し、
|
||||
存在しなければ、パスパターンに一致するワーカーにリクエストを転送します。
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
php_server {
|
||||
worker {
|
||||
file /path/to/worker.php # ファイルはpublicパス外でも可
|
||||
match /api/* # /api/で始まるすべてのリクエストはこのワーカーで処理される
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### フルデュプレックス(HTTP/1)
|
||||
|
||||
HTTP/1.xを使用する場合、全体のボディが読み取られる前にレスポンスを書き込めるようにするため、
|
||||
フルデュプレックスモードを有効にすることが望ましい場合があります(例:WebSocket、Server-Sent Eventsなど)。
|
||||
|
||||
これは明示的に有効化する必要がある設定で、`Caddyfile`のグローバルオプションに追加する必要があります:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
servers {
|
||||
enable_full_duplex
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> このオプションを有効にすると、フルデュプレックスをサポートしない古いHTTP/1.xクライアントでデッドロックが発生する可能性があります。
|
||||
> これは`CADDY_GLOBAL_OPTIONS`環境設定を使用しても設定できます:
|
||||
|
||||
```sh
|
||||
CADDY_GLOBAL_OPTIONS="servers {
|
||||
enable_full_duplex
|
||||
}"
|
||||
```
|
||||
|
||||
この設定の詳細については、[Caddyドキュメント](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex)をご覧ください。
|
||||
|
||||
## 環境変数
|
||||
|
||||
以下の環境変数を使用することで、`Caddyfile`を直接変更せずにCaddyディレクティブを注入できます:
|
||||
|
||||
- `SERVER_NAME`: [待ち受けアドレス](https://caddyserver.com/docs/caddyfile/concepts#addresses)を変更し、指定したホスト名はTLS証明書の生成にも使用されます
|
||||
- `SERVER_ROOT`: サイトのルートディレクトリを変更します。デフォルトは`public/`
|
||||
- `CADDY_GLOBAL_OPTIONS`: [グローバルオプション](https://caddyserver.com/docs/caddyfile/options)を注入します
|
||||
- `FRANKENPHP_CONFIG`: `frankenphp`ディレクティブの下に設定を注入します
|
||||
|
||||
FPM や CLI SAPI と同様に、環境変数はデフォルトで`$_SERVER`スーパーグローバルで公開されます。
|
||||
|
||||
[`variables_order` PHPディレクティブ](https://www.php.net/manual/en/ini.core.php#ini.variables-order)の`S`値は、このディレクティブ内での`E`の位置にかかわらず常に`ES`と同等です。
|
||||
|
||||
## PHP設定
|
||||
|
||||
[追加のPHP設定ファイル](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan)を読み込むには、
|
||||
`PHP_INI_SCAN_DIR`環境変数を使用できます。
|
||||
設定されると、PHPは指定されたディレクトリに存在する`.ini`拡張子を持つすべてのファイルを読み込みます。
|
||||
|
||||
また、`Caddyfile`の`php_ini`ディレクティブを使用してPHP設定を変更することもできます:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
frankenphp {
|
||||
php_ini memory_limit 256M
|
||||
|
||||
# または
|
||||
|
||||
php_ini {
|
||||
memory_limit 256M
|
||||
max_execution_time 15
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## デバッグモードの有効化
|
||||
|
||||
Dockerイメージを使用する場合、`CADDY_GLOBAL_OPTIONS`環境変数に`debug`を設定するとデバッグモードが有効になります:
|
||||
|
||||
```console
|
||||
docker run -v $PWD:/app/public \
|
||||
-e CADDY_GLOBAL_OPTIONS=debug \
|
||||
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||
dunglas/frankenphp
|
||||
```
|
||||
205
docs/ja/docker.md
Normal file
205
docs/ja/docker.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# カスタムDockerイメージのビルド
|
||||
|
||||
[FrankenPHPのDockerイメージ](https://hub.docker.com/r/dunglas/frankenphp)は、[公式PHPイメージ](https://hub.docker.com/_/php/)をベースにしています。主要なアーキテクチャに対してDebianとAlpine Linuxのバリアントを提供しており、Debianバリアントの使用を推奨しています。
|
||||
|
||||
PHP 8.2、8.3、8.4、8.5向けのバリアントが提供されています。
|
||||
|
||||
タグは次のパターンに従います:`dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`
|
||||
|
||||
- `<frankenphp-version>`および`<php-version>`は、それぞれFrankenPHPおよびPHPのバージョン番号で、メジャー(例:`1`)、マイナー(例:`1.2`)からパッチバージョン(例:`1.2.3`)まであります。
|
||||
- `<os>`は`bookworm`(Debian Bookworm用)または`alpine`(Alpine最新安定版用)のいずれかです。
|
||||
|
||||
[タグを閲覧](https://hub.docker.com/r/dunglas/frankenphp/tags)。
|
||||
|
||||
## イメージの使用方法
|
||||
|
||||
プロジェクトに`Dockerfile`を作成します:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
|
||||
COPY . /app/public
|
||||
```
|
||||
|
||||
次に、以下のコマンドを実行してDockerイメージをビルドし、実行します:
|
||||
|
||||
```console
|
||||
docker build -t my-php-app .
|
||||
docker run -it --rm --name my-running-app my-php-app
|
||||
```
|
||||
|
||||
## PHP拡張モジュールの追加インストール方法
|
||||
|
||||
ベースイメージには[`docker-php-extension-installer`](https://github.com/mlocati/docker-php-extension-installer)スクリプトが含まれており、
|
||||
追加のPHP拡張モジュールを簡単にインストールできます:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
|
||||
# 追加の拡張モジュールをここに追加:
|
||||
RUN install-php-extensions \
|
||||
pdo_mysql \
|
||||
gd \
|
||||
intl \
|
||||
zip \
|
||||
opcache
|
||||
```
|
||||
|
||||
## Caddyモジュールの追加インストール方法
|
||||
|
||||
FrankenPHPはCaddyをベースに構築されているため、すべての[Caddyモジュール](https://caddyserver.com/docs/modules/)をFrankenPHPでも使用できます。
|
||||
|
||||
カスタムCaddyモジュールをインストールする最も簡単な方法は、[xcaddy](https://github.com/caddyserver/xcaddy)を使用することです:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp:builder AS builder
|
||||
|
||||
# builderイメージにxcaddyをコピー
|
||||
COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy
|
||||
|
||||
# FrankenPHPをビルドするにはCGOを有効にする必要があります
|
||||
RUN CGO_ENABLED=1 \
|
||||
XCADDY_SETCAP=1 \
|
||||
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
|
||||
CGO_CFLAGS=$(php-config --includes) \
|
||||
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
|
||||
xcaddy build \
|
||||
--output /usr/local/bin/frankenphp \
|
||||
--with github.com/dunglas/frankenphp=./ \
|
||||
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
|
||||
--with github.com/dunglas/caddy-cbrotli \
|
||||
# MercureとVulcainは公式ビルドに含まれていますが、お気軽に削除してください
|
||||
--with github.com/dunglas/mercure/caddy \
|
||||
--with github.com/dunglas/vulcain/caddy
|
||||
# ここに追加のCaddyモジュールを指定してください
|
||||
|
||||
FROM dunglas/frankenphp AS runner
|
||||
|
||||
# 公式バイナリをカスタムモジュールを含むものに置き換え
|
||||
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
|
||||
```
|
||||
|
||||
FrankenPHPが提供する`builder`イメージには、コンパイル済みの`libphp`が含まれています。
|
||||
[ビルダーイメージ](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder)は、FrankenPHPおよびPHPのすべてのバージョンに対して、DebianとAlpineの両方が提供されています。
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> Alpine LinuxとSymfonyを使用している場合は、
|
||||
> [デフォルトのスタックサイズを増やす](compile.md#using-xcaddy) 必要がある場合があります。
|
||||
|
||||
## デフォルトでワーカーモードを有効にする
|
||||
|
||||
FrankenPHPをワーカースクリプトで起動するには、`FRANKENPHP_CONFIG`環境変数を設定します:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
|
||||
# ...
|
||||
|
||||
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
|
||||
```
|
||||
|
||||
## 開発時にボリュームを使う
|
||||
|
||||
FrankenPHPでの開発を簡単に行うには、ホスト側のアプリケーションのソースコードを含むディレクトリを、Dockerコンテナ内にボリュームとしてマウントします:
|
||||
|
||||
```console
|
||||
docker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> `--tty`オプションを使うと、JSONではなく人間が読みやすいログが表示されます。
|
||||
|
||||
Docker Composeを使用する場合:
|
||||
|
||||
```yaml
|
||||
# compose.yaml
|
||||
|
||||
services:
|
||||
php:
|
||||
image: dunglas/frankenphp
|
||||
# カスタムDockerfileを使用したい場合は以下の行のコメントを外してください
|
||||
#build: .
|
||||
# 本番環境で使用する場合は以下の行のコメントを外してください
|
||||
# restart: always
|
||||
ports:
|
||||
- "80:80" # HTTP
|
||||
- "443:443" # HTTPS
|
||||
- "443:443/udp" # HTTP/3
|
||||
volumes:
|
||||
- ./:/app/public
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
# 開発環境で人間が読みやすいログを出力するため、本番ではこの行をコメントアウトしてください
|
||||
tty: true
|
||||
|
||||
# Caddyの証明書や設定に必要なボリューム
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
```
|
||||
|
||||
## 非rootユーザーとして実行する
|
||||
|
||||
FrankenPHPはDockerで非rootユーザーとして実行できます。
|
||||
|
||||
これを行うサンプル`Dockerfile`は以下の通りです:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
|
||||
ARG USER=appuser
|
||||
|
||||
RUN \
|
||||
# Alpine系ディストリビューションでは "adduser -D ${USER}" を使用
|
||||
useradd ${USER}; \
|
||||
# ポート 80 や 443 にバインドするための追加ケーパビリティを追加
|
||||
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
|
||||
# /data/caddy および /config/caddy への書き込み権限を付与
|
||||
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy
|
||||
|
||||
USER ${USER}
|
||||
```
|
||||
|
||||
### ケーパビリティなしでの実行
|
||||
|
||||
FrankenPHPをroot以外のユーザーで実行する場合でも、特権ポート(80と443)でWebサーバーを
|
||||
バインドするために`CAP_NET_BIND_SERVICE`ケーパビリティが必要です。
|
||||
|
||||
FrankenPHPを非特権ポート(1024以上)で公開する場合は、
|
||||
ウェブサーバーを非rootユーザーとして実行し、ケーパビリティを必要とせずに実行することが可能です:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
|
||||
ARG USER=appuser
|
||||
|
||||
RUN \
|
||||
# Alpine 系ディストリビューションでは "adduser -D ${USER}" を使用
|
||||
useradd ${USER}; \
|
||||
# デフォルトのケーパビリティを削除
|
||||
setcap -r /usr/local/bin/frankenphp; \
|
||||
# /data/caddy と /config/caddy への書き込み権限を付与
|
||||
chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy
|
||||
|
||||
USER ${USER}
|
||||
```
|
||||
|
||||
その後、`SERVER_NAME`環境変数を設定して非特権ポートを使用します。
|
||||
例: `:8000`
|
||||
|
||||
## アップデート
|
||||
|
||||
Dockerイメージは以下のタイミングでビルドされます:
|
||||
|
||||
- 新しいリリースがタグ付けされたとき
|
||||
- 公式PHPイメージに新しいバージョンがある場合、毎日UTC午前4時に自動ビルド
|
||||
|
||||
## 開発版
|
||||
|
||||
開発版は[`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev)Dockerリポジトリで利用できます。
|
||||
GitHubリポジトリのmainブランチにコミットがpushされるたびに新しいビルドが実行されます。
|
||||
|
||||
`latest*`タグは`main`ブランチのヘッドを指しており、
|
||||
`sha-<git-commit-hash>` 形式のタグも利用可能です。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user