mirror of
https://github.com/dunglas/frankenphp.git
synced 2025-12-24 13:38:11 +08:00
Compare commits
76 Commits
feat/frank
...
add/ext/mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12311107f4 | ||
|
|
c57e1c6d2a | ||
|
|
44d58e3590 | ||
|
|
36cdb72536 | ||
|
|
17eba05bcd | ||
|
|
50208aa818 | ||
|
|
f76fd14c3f | ||
|
|
6c208e2753 | ||
|
|
31e045bb75 | ||
|
|
0d9dda91e9 | ||
|
|
74e9e9aa19 | ||
|
|
327a20ce63 | ||
|
|
8efbc6c1e2 | ||
|
|
7ea6e7c093 | ||
|
|
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 |
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.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:
|
||||
|
||||
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.
|
||||
|
||||
23
.github/workflows/docker.yaml
vendored
23
.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
|
||||
@@ -63,9 +70,11 @@ 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="; "")')
|
||||
|
||||
if [[ "${FRANKENPHP_82_LATEST}" == "${PHP_82_LATEST}" ]] && [[ "${FRANKENPHP_83_LATEST}" == "${PHP_83_LATEST}" ]] && [[ "${FRANKENPHP_84_LATEST}" == "${PHP_84_LATEST}" ]]; then
|
||||
echo skip=true >> "${GITHUB_OUTPUT}"
|
||||
@@ -73,7 +82,7 @@ jobs:
|
||||
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
|
||||
@@ -197,7 +206,7 @@ jobs:
|
||||
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 ./...'
|
||||
sh -c './go.sh test -tags ${{ matrix.race }} -v $(./go.sh list ./... | grep -v github.com/dunglas/frankenphp/internal/testext | grep -v github.com/dunglas/frankenphp/internal/extgen) && cd caddy && ../go.sh test ${{ matrix.race }} -v ./...'
|
||||
env:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
|
||||
3
.github/workflows/lint.yaml
vendored
3
.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:
|
||||
|
||||
3
.github/workflows/sanitizers.yaml
vendored
3
.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:
|
||||
|
||||
11
.github/workflows/static.yaml
vendored
11
.github/workflows/static.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
|
||||
|
||||
7
.github/workflows/tests.yaml
vendored
7
.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:
|
||||
@@ -53,8 +56,10 @@ jobs:
|
||||
- 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 ./...
|
||||
|
||||
28
Dockerfile
28
Dockerfile
@@ -46,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"
|
||||
|
||||
@@ -81,16 +81,21 @@ RUN apt-get update && \
|
||||
|
||||
# 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 | \
|
||||
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 && \
|
||||
cmake --build build && \
|
||||
cmake --install build && \
|
||||
ldconfig
|
||||
|
||||
WORKDIR /go/src/app
|
||||
@@ -110,10 +115,9 @@ 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'" -buildvcs=true && \
|
||||
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/frankenphp/Caddyfile && \
|
||||
frankenphp version && \
|
||||
|
||||
@@ -23,7 +23,7 @@ containing [PHP 8.4](https://www.php.net/releases/8.4/en.php) and most popular P
|
||||
|
||||
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
|
||||
[Download FrankenPHP](https://github.com/php/frankenphp/releases) or copy this line into your
|
||||
terminal to automatically install the version appropriate for your platform:
|
||||
|
||||
```console
|
||||
@@ -84,6 +84,7 @@ frankenphp php-server
|
||||
- [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/)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -46,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"
|
||||
|
||||
@@ -88,16 +88,21 @@ RUN apk add --no-cache --virtual .build-deps \
|
||||
|
||||
# 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 | \
|
||||
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
|
||||
cmake --build build && \
|
||||
cmake --install build
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
@@ -117,7 +122,8 @@ 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'" -buildvcs=true && \
|
||||
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 && \
|
||||
|
||||
@@ -33,7 +33,7 @@ func (e *exponentialBackoff) recordFailure() bool {
|
||||
e.backoff = min(e.backoff*2, e.maxBackoff)
|
||||
|
||||
e.mu.Unlock()
|
||||
return e.failureCount >= e.maxConsecutiveFailures
|
||||
return e.maxConsecutiveFailures != -1 && e.failureCount >= e.maxConsecutiveFailures
|
||||
}
|
||||
|
||||
// wait sleeps for the backoff duration if failureCount is non-zero.
|
||||
|
||||
@@ -73,11 +73,8 @@ if [ -z "${PHP_VERSION}" ]; then
|
||||
export PHP_VERSION
|
||||
fi
|
||||
# default extension set
|
||||
defaultExtensions="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"
|
||||
# if [ "${os}" != "linux" ] || [ "${SPC_LIBC}" = "glibc" ]; then
|
||||
# defaultExtensions="${defaultExtensions},ffi"
|
||||
# fi
|
||||
defaultExtensionLibs="bzip2,freetype,libavif,libjpeg,liblz4,libwebp,libzip,nghttp2"
|
||||
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,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,xz,zip,zlib,yaml,zstd"
|
||||
defaultExtensionLibs="libavif,nghttp2,nghttp3,ngtcp2"
|
||||
|
||||
md5binary="md5sum"
|
||||
if [ "${os}" = "darwin" ]; then
|
||||
@@ -93,10 +90,6 @@ 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"
|
||||
|
||||
281
caddy/app.go
Normal file
281
caddy/app.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
|
||||
metrics frankenphp.Metrics
|
||||
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.logger = ctx.Slogger()
|
||||
|
||||
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()
|
||||
|
||||
opts := []frankenphp.Option{
|
||||
frankenphp.WithNumThreads(f.NumThreads),
|
||||
frankenphp.WithMaxThreads(f.MaxThreads),
|
||||
frankenphp.WithLogger(f.logger),
|
||||
frankenphp.WithMetrics(f.metrics),
|
||||
frankenphp.WithPhpIni(f.PhpIni),
|
||||
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
|
||||
}
|
||||
for _, w := range append(f.Workers) {
|
||||
workerOpts := []frankenphp.WorkerOption{
|
||||
frankenphp.WithWorkerEnv(w.Env),
|
||||
frankenphp.WithWorkerWatchMode(w.Watch),
|
||||
frankenphp.WithWorkerMaxFailures(w.MaxConsecutiveFailures),
|
||||
}
|
||||
|
||||
opts = append(opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, workerOpts...))
|
||||
}
|
||||
|
||||
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 the configuration so it doesn't bleed into later tests
|
||||
f.Workers = nil
|
||||
f.NumThreads = 0
|
||||
f.MaxWaitTime = 0
|
||||
|
||||
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 errors.New("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 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, err := parseWorkerConfig(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 fmt.Errorf(`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 fmt.Errorf("global workers must not have duplicate filenames: %q", wc.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
f.Workers = append(f.Workers, wc)
|
||||
default:
|
||||
allowedDirectives := "num_threads, max_threads, php_ini, worker, max_wait_time"
|
||||
return wrongSubDirectiveError("frankenphp", allowedDirectives, d.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
900
caddy/caddy.go
900
caddy/caddy.go
@@ -4,26 +4,12 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -31,8 +17,6 @@ const (
|
||||
defaultWatchPattern = "./**/*.{php,yaml,yml,twig,env}"
|
||||
)
|
||||
|
||||
var iniError = errors.New("'php_ini' must be in the format: php_ini \"<key>\" \"<value>\"")
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(FrankenPHPApp{})
|
||||
caddy.RegisterModule(FrankenPHPModule{})
|
||||
@@ -47,890 +31,6 @@ func init() {
|
||||
httpcaddyfile.RegisterDirectiveOrder("php_server", "before", "file_server")
|
||||
}
|
||||
|
||||
type workerConfig struct {
|
||||
// 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"`
|
||||
// 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"`
|
||||
}
|
||||
|
||||
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"`
|
||||
|
||||
metrics frankenphp.Metrics
|
||||
logger *slog.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.logger = ctx.Slogger()
|
||||
|
||||
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
|
||||
f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
opts := []frankenphp.Option{
|
||||
frankenphp.WithNumThreads(f.NumThreads),
|
||||
frankenphp.WithMaxThreads(f.MaxThreads),
|
||||
frankenphp.WithLogger(f.logger),
|
||||
frankenphp.WithMetrics(f.metrics),
|
||||
frankenphp.WithPhpIni(f.PhpIni),
|
||||
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
|
||||
}
|
||||
for _, w := range append(f.Workers) {
|
||||
opts = append(opts, frankenphp.WithWorkers(w.Name, 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 the configuration so it doesn't bleed into later tests
|
||||
f.Workers = nil
|
||||
f.NumThreads = 0
|
||||
f.MaxWaitTime = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseWorkerConfig(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, errors.New(`FrankenPHP: too many "worker" arguments: ` + d.Val())
|
||||
}
|
||||
|
||||
for d.NextBlock(1) {
|
||||
v := d.Val()
|
||||
switch 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, err
|
||||
}
|
||||
|
||||
wc.Num = 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":
|
||||
if !d.NextArg() {
|
||||
// the default if the watch directory is left empty:
|
||||
wc.Watch = append(wc.Watch, defaultWatchPattern)
|
||||
} else {
|
||||
wc.Watch = append(wc.Watch, d.Val())
|
||||
}
|
||||
default:
|
||||
allowedDirectives := "name, file, num, env, watch"
|
||||
return wc, wrongSubDirectiveError("worker", allowedDirectives, v)
|
||||
}
|
||||
}
|
||||
|
||||
if wc.FileName == "" {
|
||||
return wc, errors.New(`the "file" argument must be specified`)
|
||||
}
|
||||
|
||||
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
|
||||
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
|
||||
}
|
||||
|
||||
return wc, 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 errors.New("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 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, err := parseWorkerConfig(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 fmt.Errorf(`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 fmt.Errorf("global workers must not have duplicate filenames: %q", wc.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
f.Workers = append(f.Workers, wc)
|
||||
default:
|
||||
allowedDirectives := "num_threads, max_threads, php_ini, worker, max_wait_time"
|
||||
return wrongSubDirectiveError("frankenphp", allowedDirectives, d.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"`
|
||||
// Workers configures the worker scripts to start.
|
||||
Workers []workerConfig `json:"workers,omitempty"`
|
||||
|
||||
resolvedDocumentRoot string
|
||||
preparedEnv frankenphp.PreparedEnv
|
||||
preparedEnvNeedsReplacement bool
|
||||
logger *slog.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.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`)
|
||||
}
|
||||
|
||||
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 {
|
||||
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.ContainsAny(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
|
||||
var 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, "")
|
||||
}
|
||||
}
|
||||
|
||||
fullScriptPath, _ := fastabs.FastAbs(documentRoot + "/" + r.URL.Path)
|
||||
|
||||
workerName := ""
|
||||
for _, w := range f.Workers {
|
||||
if p, _ := fastabs.FastAbs(w.FileName); p == fullScriptPath {
|
||||
workerName = w.Name
|
||||
}
|
||||
}
|
||||
|
||||
fr, err := frankenphp.NewRequestWithContext(
|
||||
r,
|
||||
documentRootOption,
|
||||
frankenphp.WithRequestSplitPath(f.SplitPath),
|
||||
frankenphp.WithRequestPreparedEnv(env),
|
||||
frankenphp.WithOriginalRequest(&origReq),
|
||||
frankenphp.WithWorkerName(workerName),
|
||||
)
|
||||
|
||||
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 {
|
||||
// First pass: Parse all directives except "worker"
|
||||
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":
|
||||
for d.NextBlock(1) {
|
||||
}
|
||||
for d.NextArg() {
|
||||
}
|
||||
// Skip "worker" blocks in the first pass
|
||||
continue
|
||||
|
||||
default:
|
||||
allowedDirectives := "root, split, env, resolve_root_symlink, worker"
|
||||
return wrongSubDirectiveError("php or php_server", allowedDirectives, d.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: Parse only "worker" blocks
|
||||
d.Reset()
|
||||
for d.Next() {
|
||||
for d.NextBlock(0) {
|
||||
if d.Val() == "worker" {
|
||||
wc, err := parseWorkerConfig(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Inherit environment variables from the parent php_server directive
|
||||
if !filepath.IsAbs(wc.FileName) && f.Root != "" {
|
||||
wc.FileName = filepath.Join(f.Root, wc.FileName)
|
||||
}
|
||||
|
||||
if f.Env != nil {
|
||||
if wc.Env == nil {
|
||||
wc.Env = make(map[string]string)
|
||||
}
|
||||
for k, v := range f.Env {
|
||||
// Only set if not already defined in the worker
|
||||
if _, exists := wc.Env[k]; !exists {
|
||||
wc.Env[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a worker with this filename already exists in this module
|
||||
for _, existingWorker := range f.Workers {
|
||||
if existingWorker.FileName == wc.FileName {
|
||||
return fmt.Errorf(`workers in a single "php_server" block must not have duplicate filenames: %q`, wc.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
f.Workers = append(f.Workers, wc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// return a nice error message
|
||||
func wrongSubDirectiveError(module string, allowedDriectives string, wrongValue string) error {
|
||||
return fmt.Errorf("unknown '%s' subdirective: '%s' (allowed directives are: %s)", module, wrongValue, allowedDriectives)
|
||||
|
||||
@@ -1316,3 +1316,110 @@ func TestWorkerRestart(t *testing.T) {
|
||||
"frankenphp_worker_restarts",
|
||||
))
|
||||
}
|
||||
|
||||
func TestWorkerMatchDirective(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
}
|
||||
|
||||
http://localhost:`+testPort+` {
|
||||
php_server {
|
||||
root ../testdata/files
|
||||
worker {
|
||||
file ../worker-with-counter.php
|
||||
match /matched-path*
|
||||
num 1
|
||||
}
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
// worker is outside of public directory, match anyways
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/matched-path", http.StatusOK, "requests:1")
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/matched-path/anywhere", http.StatusOK, "requests:2")
|
||||
|
||||
// 404 on unmatched paths
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/elsewhere", http.StatusNotFound, "")
|
||||
|
||||
// static file will be served by the fileserver
|
||||
expectedFileResponse, err := os.ReadFile("../testdata/files/static.txt")
|
||||
require.NoError(t, err, "static.txt file must be readable for this test")
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/static.txt", http.StatusOK, string(expectedFileResponse))
|
||||
}
|
||||
|
||||
func TestWorkerMatchDirectiveWithMultipleWorkers(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
}
|
||||
http://localhost:`+testPort+` {
|
||||
php_server {
|
||||
root ../testdata
|
||||
worker {
|
||||
file worker-with-counter.php
|
||||
match /counter/*
|
||||
num 1
|
||||
}
|
||||
worker {
|
||||
file index.php
|
||||
match /index/*
|
||||
num 1
|
||||
}
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
// match 2 workers respectively (in the public directory)
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/counter/sub-path", http.StatusOK, "requests:1")
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/index/sub-path", http.StatusOK, "I am by birth a Genevese (i not set)")
|
||||
|
||||
// static file will be served by the fileserver
|
||||
expectedFileResponse, err := os.ReadFile("../testdata/files/static.txt")
|
||||
require.NoError(t, err, "static.txt file must be readable for this test")
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/files/static.txt", http.StatusOK, string(expectedFileResponse))
|
||||
|
||||
// serve php file directly as fallback
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/hello.php", http.StatusOK, "Hello from PHP")
|
||||
|
||||
// serve index.php file directly as fallback
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, "I am by birth a Genevese (i not set)")
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/not-matched", http.StatusOK, "I am by birth a Genevese (i not set)")
|
||||
}
|
||||
|
||||
func TestWorkerMatchDirectiveWithoutFileServer(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
}
|
||||
|
||||
http://localhost:`+testPort+` {
|
||||
route {
|
||||
php_server {
|
||||
index off
|
||||
file_server off
|
||||
root ../testdata/files
|
||||
worker {
|
||||
file ../worker-with-counter.php
|
||||
match /some-path
|
||||
}
|
||||
}
|
||||
|
||||
respond "Request falls through" 404
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
// find the worker at some-path
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/some-path", http.StatusOK, "requests:1")
|
||||
|
||||
// do not find the file at static.txt
|
||||
// the request should completely fall through the php_server module
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/static.txt", http.StatusNotFound, "Request falls through")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package caddy
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
53
caddy/extinit.go
Normal file
53
caddy/extinit.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/dunglas/frankenphp/internal/extgen"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
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: "(Experimental) Initializes a PHP extension from a Go file",
|
||||
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(fs 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 := strings.TrimSuffix(filepath.Base(sourceFile), ".go")
|
||||
|
||||
baseName = extgen.SanitizePackageName(baseName)
|
||||
|
||||
sourceDir := filepath.Dir(sourceFile)
|
||||
buildDir := filepath.Join(sourceDir, "build")
|
||||
|
||||
generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: buildDir}
|
||||
|
||||
if err := generator.Generate(); err != nil {
|
||||
return 1, err
|
||||
}
|
||||
|
||||
log.Printf("PHP extension %q initialized successfully in %q", baseName, generator.BuildDir)
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
#}
|
||||
|
||||
root {$SERVER_ROOT:public/}
|
||||
encode zstd br gzip
|
||||
encode zstd br gzip
|
||||
|
||||
# Uncomment the following lines to enable Mercure and Vulcain modules
|
||||
#mercure {
|
||||
|
||||
124
caddy/go.mod
124
caddy/go.mod
@@ -9,9 +9,9 @@ retract v1.0.0-rc.1 // Human error
|
||||
require (
|
||||
github.com/caddyserver/caddy/v2 v2.10.0
|
||||
github.com/caddyserver/certmagic v0.23.0
|
||||
github.com/dunglas/caddy-cbrotli v1.0.0
|
||||
github.com/dunglas/frankenphp v1.7.0
|
||||
github.com/dunglas/mercure/caddy v0.19.2
|
||||
github.com/dunglas/caddy-cbrotli v1.0.1
|
||||
github.com/dunglas/frankenphp v1.9.0
|
||||
github.com/dunglas/mercure/caddy v0.20.0
|
||||
github.com/dunglas/vulcain/caddy v1.2.0
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
@@ -22,27 +22,30 @@ require github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.3 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.7.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.5.0 // indirect
|
||||
github.com/KimMachineGun/automemlimit v0.7.2 // indirect
|
||||
github.com/KimMachineGun/automemlimit v0.7.4 // 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.9.6 // indirect
|
||||
github.com/MicahParks/keyfunc/v3 v3.4.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/RoaringBitmap/roaring v1.9.4 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.18.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.19.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.22.0 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||
github.com/ccoveille/go-safecast v1.6.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // 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
|
||||
@@ -57,39 +60,43 @@ require (
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dolthub/maphash v0.1.0 // indirect
|
||||
github.com/dunglas/httpsfv v1.1.0 // indirect
|
||||
github.com/dunglas/mercure v0.19.2 // indirect
|
||||
github.com/dunglas/mercure v0.20.0 // indirect
|
||||
github.com/dunglas/vulcain v1.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||
github.com/gammazero/deque v1.0.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gammazero/deque v1.1.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.132.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.2 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 // 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.1 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // 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.2 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/brotli/go/cbrotli v0.0.0-20241111155135-4303850b01d6 // indirect
|
||||
github.com/google/cel-go v0.25.0 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.1 // indirect
|
||||
github.com/google/brotli/go/cbrotli v1.1.0 // indirect
|
||||
github.com/google/cel-go v0.26.0 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.2 // indirect
|
||||
github.com/google/go-tpm v0.9.5 // indirect
|
||||
github.com/google/go-tspi v0.3.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e // indirect
|
||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // 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.6 // 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.26.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
@@ -100,7 +107,7 @@ require (
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kevburnsjr/skipfilter v0.0.1 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/libdns/libdns v1.1.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
@@ -110,7 +117,7 @@ require (
|
||||
github.com/maypok86/otter v1.2.4 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.2 // indirect
|
||||
github.com/miekg/dns v1.1.66 // indirect
|
||||
github.com/miekg/dns v1.1.67 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
@@ -127,8 +134,8 @@ require (
|
||||
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.2 // indirect
|
||||
github.com/prometheus/common v0.64.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.52.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
@@ -138,7 +145,7 @@ require (
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/slackhq/nebula v1.9.5 // indirect
|
||||
github.com/smallstep/certificates v0.28.3 // indirect
|
||||
github.com/smallstep/certificates v0.28.4 // indirect
|
||||
github.com/smallstep/cli-utils v0.12.1 // indirect
|
||||
github.com/smallstep/linkedca v0.23.0 // indirect
|
||||
github.com/smallstep/nosql v0.7.0 // indirect
|
||||
@@ -148,9 +155,9 @@ require (
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/cast v1.9.2 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
github.com/spf13/viper v1.20.1 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.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/tidwall/gjson v1.18.0 // indirect
|
||||
@@ -158,48 +165,49 @@ require (
|
||||
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/x448/float16 v0.8.4 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/yuin/goldmark v1.7.12 // 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.etcd.io/bbolt v1.4.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.35.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.35.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
|
||||
go.step.sm/crypto v0.66.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.37.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
||||
go.step.sm/crypto v0.67.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250531095911-4f9f0ca9fcfb // indirect
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250711192710-b903b535d3ef // indirect
|
||||
golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 // indirect
|
||||
golang.org/x/mod v0.26.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/grpc v1.72.2 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/term v0.33.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
google.golang.org/api v0.242.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 // indirect
|
||||
google.golang.org/grpc v1.73.0 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
|
||||
301
caddy/go.sum
301
caddy/go.sum
@@ -6,8 +6,8 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
|
||||
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
|
||||
cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
|
||||
cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
|
||||
cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc=
|
||||
cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
|
||||
@@ -30,15 +30,14 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/KimMachineGun/automemlimit v0.7.2 h1:DyfHI7zLWmZPn2Wqdy2AgTiUvrGPmnYWgwhHXtAegX4=
|
||||
github.com/KimMachineGun/automemlimit v0.7.2/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
|
||||
github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk=
|
||||
github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
||||
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/MauriceGit/skiplist v0.0.0-20191117202105-643e379adb62/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
|
||||
@@ -59,8 +58,8 @@ github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhve
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=
|
||||
github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4=
|
||||
github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
@@ -70,32 +69,32 @@ github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmO
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.16 h1:XkruGnXX1nEZ+Nyo9v84TzsX+nj86icbFAeust6uo8A=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.16/go.mod h1:uCW7PNjGwZ5cOGZ5jr8vCWrYkGIhPoTNV23Q/tpHKzg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.69 h1:8B8ZQboRc3uaIKjshve/XlvJ570R7BKNy3gftSbS178=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.69/go.mod h1:gPME6I8grR1jCqBFEGthULiolzf/Sexq/Wy42ibKK9c=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 h1:oQWSGexYasNpYp4epLGZxxjsDo8BMBh6iNWkTXQvkwk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31/go.mod h1:nc332eGUU+djP3vrMI6blS0woaCfHTe3KiSQUVTMRq0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.38.3 h1:RivOtUH3eEu6SWnUMFHKAW4MqDOzWn1vGQ3S38Y5QMg=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.38.3/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 h1:/ldKrPPXTC421bTNWrUIpq3CxwHwRI/kpc+jPUTJocM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16/go.mod h1:5vkf/Ws0/wgIMJDQbjI4p2op86hNW6Hie5QtebrDgT8=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.41.0 h1:2jKyib9msVrAVn+lngwlSplG13RpUZmzVte2yDao5nc=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.41.0/go.mod h1:RyhzxkWGcfixlkieewzpO3D4P4fTMxhIDqDZWsh0u/4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 h1:EU58LP8ozQDVroOEyAfcq0cGc5R/FTZjVoYJ6tvby3w=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.4/go.mod h1:CrtOgCcysxMvrCoHnvNAD7PHWclmoFG78Q2xLK0KKcs=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 h1:XB4z0hbQtpmBnb1FQYvKaCM7UsS6Y/u8jVBwIUGeCTk=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2/go.mod h1:hwRpqkRxnQ58J9blRDrB4IanlXCpcKmsC83EhG77upg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn1udWrQTjEF+SOXB0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo=
|
||||
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@@ -115,8 +114,8 @@ github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+Y
|
||||
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q=
|
||||
github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
@@ -140,7 +139,6 @@ github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmr
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
@@ -165,14 +163,14 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
|
||||
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
|
||||
github.com/dunglas/caddy-cbrotli v1.0.0 h1:+WNqXBkWyMcIpXB2rVZ3nwcElUbuAzf0kPxNXU4D+u0=
|
||||
github.com/dunglas/caddy-cbrotli v1.0.0/go.mod h1:KZsUu3fnQBgO0o3YDoQuO3Z61dFgUncr1F8rg8acwQw=
|
||||
github.com/dunglas/caddy-cbrotli v1.0.1 h1:mkg7EB1GmoyfBt3kY3mq4o/0bfnBeq7ZLQjmVmdBE3Y=
|
||||
github.com/dunglas/caddy-cbrotli v1.0.1/go.mod h1:uXABy3tjy1FABF+3JWKVh1ajFvIO/kfpwHaeZGSBaAY=
|
||||
github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54=
|
||||
github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
|
||||
github.com/dunglas/mercure v0.19.2 h1:eBMQhxvzJTenIffL+jlqtWO+TPevCjOQ+DQJb8iB6+s=
|
||||
github.com/dunglas/mercure v0.19.2/go.mod h1:/8edy/qugBTM4CbEzX4DwykEIh1VWHWlo1e/pHos120=
|
||||
github.com/dunglas/mercure/caddy v0.19.2 h1:Ug/nALeSn7DH4qMy7I2pImCf1D0H+0vr6K4AWjgmOKg=
|
||||
github.com/dunglas/mercure/caddy v0.19.2/go.mod h1:Kq+qvO79l8odqokiNcdKbH53aOLurarse8xZ42ALzJQ=
|
||||
github.com/dunglas/mercure v0.20.0 h1:BcgHjdZc7oWapXWHv8RSStz3HvuWallJ0KQ2yejgzgc=
|
||||
github.com/dunglas/mercure v0.20.0/go.mod h1:pswrX6EiPpYq0I1UZCxGZtw4zIPwYCCcGnN40ZSe6iU=
|
||||
github.com/dunglas/mercure/caddy v0.20.0 h1:I4Oy4YFDrv63pssf6fdoHez76aH9Q8cugB42RXJr9yE=
|
||||
github.com/dunglas/mercure/caddy v0.20.0/go.mod h1:vZdsT+3Kr8/fBNDr8vehMYwQsxGCMdD5zwGXc+hd9pM=
|
||||
github.com/dunglas/vulcain v1.2.0 h1:RPNYuTe0woh4bGfIMAJ3dCgJDN8VJwhDjucQiCOoUsE=
|
||||
github.com/dunglas/vulcain v1.2.0/go.mod h1:LhyYeqSAEw9P65l25CIzS1sRwJxkP75Qa7p8lIHZPsc=
|
||||
github.com/dunglas/vulcain/caddy v1.2.0 h1:2O2R7Hn+kkInv6mrmOk5LLDtgRdPKGlXzdFJUKrb/jE=
|
||||
@@ -190,43 +188,45 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34=
|
||||
github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gammazero/deque v1.1.0 h1:OyiyReBbnEG2PP0Bnv1AASLIYvyKqIFN5xfl1t8oGLo=
|
||||
github.com/gammazero/deque v1.1.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg=
|
||||
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
|
||||
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
||||
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
|
||||
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
@@ -238,16 +238,16 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/brotli/go/cbrotli v0.0.0-20241111155135-4303850b01d6 h1:ygfAzuHUP3wed0um8AamwtZIh022Ie5lnsWHhItj/sY=
|
||||
github.com/google/brotli/go/cbrotli v0.0.0-20241111155135-4303850b01d6/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
|
||||
github.com/google/brotli/go/cbrotli v1.1.0 h1:YwHD/rwSgUSL4b2S3ZM2jnNymm+tmwKQqjUIC63nmHU=
|
||||
github.com/google/brotli/go/cbrotli v1.1.0/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
|
||||
github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
|
||||
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
|
||||
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||
github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go=
|
||||
github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k=
|
||||
github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A=
|
||||
github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -263,19 +263,18 @@ github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
||||
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e h1:FJta/0WsADCe1r9vQjdHbd3KuiLPu7Y9WlyLGwMUNyE=
|
||||
github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ=
|
||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
|
||||
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
@@ -283,8 +282,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
@@ -316,8 +315,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -350,8 +349,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyex
|
||||
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
|
||||
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
|
||||
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
|
||||
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
|
||||
github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@@ -405,11 +404,11 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
|
||||
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
|
||||
@@ -459,8 +458,8 @@ github.com/slackhq/nebula v1.9.5 h1:ZrxcvP/lxwFglaijmiwXLuCSkybZMJnqSYI1S8DtGnY=
|
||||
github.com/slackhq/nebula v1.9.5/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
|
||||
github.com/smallstep/certificates v0.28.3 h1:rcMh1TAs8m2emP3aDJxKLkE9jriAtcFtCuj2gttnpmI=
|
||||
github.com/smallstep/certificates v0.28.3/go.mod h1:P/IjGTvRCem3YZ7d1XtUxpvK/8dfFsJn7gaVLpMXbJw=
|
||||
github.com/smallstep/certificates v0.28.4 h1:JTU6/A5Xes6m+OsR6fw1RACSA362vJc9SOFVG7poBEw=
|
||||
github.com/smallstep/certificates v0.28.4/go.mod h1:LUqo+7mKZE7FZldlTb0zhU4A0bq4G4+akieFMcTaWvA=
|
||||
github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE=
|
||||
github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
|
||||
@@ -493,13 +492,14 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
|
||||
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -512,7 +512,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
@@ -535,8 +534,8 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=
|
||||
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
||||
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
||||
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
|
||||
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -556,43 +555,43 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.60.0 h1:sevByeAWTtfBdJQT7nkJfK5wOCjNpmDMZGPEBx3l1RA=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.60.0/go.mod h1:uEhyRPnUTSeUwMjDdrMQnsJ0sQ2mf/fA94hfchemm4A=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.35.0 h1:xoXA+5dVwsf5uE5GvSJ3lKiapyMFuIzbEmJwQ0JP+QU=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.35.0/go.mod h1:s11Orts/IzEgw9Srw5iRXtk2kM2j3jt/45noUWyf60E=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.35.0 h1:DpwKW04LkdFRFCIgM3sqwTJA/QREHMeMHYPWP1WeaPQ=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.35.0/go.mod h1:9+SNxwqvCWo1qQwUpACBY5YKNVxFJn5mlbXg/4+uKBg=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU=
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.35.0 h1:ZsgYijVvOpju4mq3g4QyqCwLKs2vKenlCpZHbKu50OA=
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.35.0/go.mod h1:t1ZwtgjEtDH9uW6OlCRVLL2wOgsTJmp0pJwNouUq+HE=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
|
||||
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
|
||||
go.step.sm/crypto v0.66.0 h1:9TW6BEguOtcS9NIjja9bDQ+j8OjhenU/F6lJfHjbXNU=
|
||||
go.step.sm/crypto v0.66.0/go.mod h1:anqGyvO/Px05D1mznHq4/a9wwP1I1DmMZvk+TWX5Dzo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 h1:1+EHlhAe/tukctfePZRrDruB9vn7MdwyC+rf36nUSPM=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0/go.mod h1:skzESZBY3IYcqJgImc+fwXQWflvVe+jZxoA/uw60NaI=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.37.0 h1:cp8AFiM/qjBm10C/ATIRnEDXpD5MBknrA0ANw4T2/ss=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.37.0/go.mod h1:Cy8Hk2E2iSGEbsLnPUdeigrexaAOAGIAmBFK919EQs0=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 h1:0aGKdIuVhy5l4GClAjl72ntkZJhijf2wg1S7b5oLoYA=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.37.0/go.mod h1:nhyrxEJEOQdwR15zXrCKI6+cJK60PXAkJ/jRyfhr2mg=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 h1:pW+qDVo0jB0rLsNeaP85xLuz20cvsECUcN7TE+D8YTM=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0/go.mod h1:x7bd+t034hxLTve1hF9Yn9qQJlO/pP8H5pWIt7+gsFM=
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.37.0 h1:tVjnBF6EiTDMXoq2Xuc2vK0I7MTbEs05II/0j9mMK+E=
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.37.0/go.mod h1:MQjyNXtxAC8PGN9gzPtO4GY5zuP+RI3XX53uWbCTvEQ=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
||||
go.step.sm/crypto v0.67.0 h1:1km9LmxMKG/p+mKa1R4luPN04vlJYnRLlLQrWv7egGU=
|
||||
go.step.sm/crypto v0.67.0/go.mod h1:+AoDpB0mZxbW/PmOXuwkPSpXRgaUaoIK+/Wx/HGgtAU=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -616,13 +615,13 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250531095911-4f9f0ca9fcfb h1:Qr7HsIdEUI8QBvEohd2cTYzTErkEkUXafR9qx9D6QEo=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250531095911-4f9f0ca9fcfb/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250711192710-b903b535d3ef h1:EJekzaXZlPQg739ghq7w/XWZVcuAOY6mh35JX2D+7Gc=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250711192710-b903b535d3ef/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA=
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 h1:vuCObX8mQzik1tfEcYxWZBuVsmQtD1IjxCyPKM18Bh4=
|
||||
golang.org/x/exp v0.0.0-20250717185816-542afb5b7346/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
@@ -631,8 +630,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -649,8 +648,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -669,8 +668,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -693,8 +692,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -704,8 +703,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -716,12 +715,12 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -732,14 +731,14 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/api v0.234.0 h1:d3sAmYq3E9gdr2mpmiWGbm9pHsA/KJmyiLkwKfHBqU4=
|
||||
google.golang.org/api v0.234.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg=
|
||||
google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg=
|
||||
google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -749,18 +748,18 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 h1:iOye66xuaAK0WnkPuhQPUFy8eJcmwUXqGGP3om6IxX8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79/go.mod h1:HKJDgKsFUnv5VAGeQjz8kxcgDP0HoE0iZNp0OdZNlhE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 h1:1ZwqphdOdWYXsUHgMpU/101nCtf/kSp9hOrcvFsnl10=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
|
||||
613
caddy/module.go
Normal file
613
caddy/module.go
Normal file
@@ -0,0 +1,613 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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`)
|
||||
}
|
||||
|
||||
for i, wc := range f.Workers {
|
||||
|
||||
// make the file path absolute from the public directory
|
||||
// this can only be done if the root is definied 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)
|
||||
}
|
||||
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 {
|
||||
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.ContainsAny(s, "{}")
|
||||
}
|
||||
|
||||
// ServeHTTP implements caddyhttp.MiddlewareHandler.
|
||||
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
|
||||
var 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
|
||||
}
|
||||
}
|
||||
|
||||
fr, err := frankenphp.NewRequestWithContext(
|
||||
r,
|
||||
documentRootOption,
|
||||
frankenphp.WithRequestSplitPath(f.SplitPath),
|
||||
frankenphp.WithRequestPreparedEnv(env),
|
||||
frankenphp.WithOriginalRequest(&origReq),
|
||||
frankenphp.WithWorkerName(workerName),
|
||||
)
|
||||
|
||||
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 {
|
||||
// First pass: Parse all directives except "worker"
|
||||
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 := parseWorkerConfig(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Workers = append(f.Workers, wc)
|
||||
|
||||
default:
|
||||
allowedDirectives := "root, split, env, resolve_root_symlink, worker"
|
||||
return wrongSubDirectiveError("php or php_server", allowedDirectives, 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_server" block must not have duplicate filenames: %q`, w.FileName)
|
||||
}
|
||||
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 = ""
|
||||
}
|
||||
|
||||
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},
|
||||
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 {
|
||||
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{
|
||||
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{
|
||||
caddy.ModuleMap{"path": h.JSON(allWorkerMatches)},
|
||||
},
|
||||
HandlersRaw: []json.RawMessage{
|
||||
caddyconfig.JSONModuleObject(f, "handler", "php", nil),
|
||||
},
|
||||
})
|
||||
|
||||
return routes
|
||||
}
|
||||
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")
|
||||
}
|
||||
@@ -41,7 +41,7 @@ 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")
|
||||
@@ -213,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
|
||||
}
|
||||
|
||||
|
||||
166
caddy/workerconfig.go
Normal file
166
caddy/workerconfig.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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 {
|
||||
// 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"`
|
||||
// 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"`
|
||||
}
|
||||
|
||||
func parseWorkerConfig(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, errors.New(`FrankenPHP: too many "worker" arguments: ` + d.Val())
|
||||
}
|
||||
|
||||
for d.NextBlock(1) {
|
||||
v := d.Val()
|
||||
switch 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, err
|
||||
}
|
||||
|
||||
wc.Num = 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":
|
||||
if !d.NextArg() {
|
||||
// the default if the watch directory is left empty:
|
||||
wc.Watch = append(wc.Watch, defaultWatchPattern)
|
||||
} else {
|
||||
wc.Watch = append(wc.Watch, d.Val())
|
||||
}
|
||||
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())
|
||||
caddyMatchPath.Provision(caddy.Context{})
|
||||
wc.MatchPath = ([]string)(caddyMatchPath)
|
||||
case "max_consecutive_failures":
|
||||
if !d.NextArg() {
|
||||
return wc, d.ArgErr()
|
||||
}
|
||||
|
||||
v, err := strconv.Atoi(d.Val())
|
||||
if err != nil {
|
||||
return wc, err
|
||||
}
|
||||
if v < -1 {
|
||||
return wc, errors.New("max_consecutive_failures must be >= -1")
|
||||
}
|
||||
|
||||
wc.MaxConsecutiveFailures = int(v)
|
||||
default:
|
||||
allowedDirectives := "name, file, num, env, watch, match, max_consecutive_failures"
|
||||
return wc, wrongSubDirectiveError("worker", allowedDirectives, v)
|
||||
}
|
||||
}
|
||||
|
||||
if wc.FileName == "" {
|
||||
return wc, errors.New(`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
|
||||
}
|
||||
8
cgi.go
8
cgi.go
@@ -41,6 +41,7 @@ var knownServerKeys = []string{
|
||||
"SERVER_PROTOCOL",
|
||||
"SERVER_SOFTWARE",
|
||||
"SSL_PROTOCOL",
|
||||
"SSL_CIPHER",
|
||||
"AUTH_TYPE",
|
||||
"REMOTE_IDENT",
|
||||
"CONTENT_TYPE",
|
||||
@@ -73,11 +74,13 @@ func addKnownVariablesToServer(thread *phpThread, fc *frankenPHPContext, trackVa
|
||||
|
||||
var https string
|
||||
var sslProtocol string
|
||||
var sslCipher string
|
||||
var rs string
|
||||
if request.TLS == nil {
|
||||
rs = "http"
|
||||
https = ""
|
||||
sslProtocol = ""
|
||||
sslCipher = ""
|
||||
} else {
|
||||
rs = "https"
|
||||
https = "on"
|
||||
@@ -89,6 +92,10 @@ func addKnownVariablesToServer(thread *phpThread, fc *frankenPHPContext, trackVa
|
||||
} else {
|
||||
sslProtocol = ""
|
||||
}
|
||||
|
||||
if request.TLS.CipherSuite != 0 {
|
||||
sslCipher = tls.CipherSuiteName(request.TLS.CipherSuite)
|
||||
}
|
||||
}
|
||||
|
||||
reqHost, reqPort, _ := net.SplitHostPort(request.Host)
|
||||
@@ -151,6 +158,7 @@ func addKnownVariablesToServer(thread *phpThread, fc *frankenPHPContext, trackVa
|
||||
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
|
||||
|
||||
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"
|
||||
14
context.go
14
context.go
@@ -17,12 +17,12 @@ type frankenPHPContext struct {
|
||||
logger *slog.Logger
|
||||
request *http.Request
|
||||
originalRequest *http.Request
|
||||
worker *worker
|
||||
|
||||
docURI string
|
||||
pathInfo string
|
||||
scriptName string
|
||||
scriptFilename string
|
||||
workerName string
|
||||
|
||||
// Whether the request is already closed by us
|
||||
isDone bool
|
||||
@@ -89,8 +89,16 @@ func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
||||
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
|
||||
// if a worker is assigned explicitly, use its filename
|
||||
// determine if the filename belongs to a worker otherwise
|
||||
if fc.worker != nil {
|
||||
fc.scriptFilename = fc.worker.fileName
|
||||
} else {
|
||||
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
||||
fc.scriptFilename = sanitizedPathJoin(fc.documentRoot, fc.scriptName)
|
||||
fc.worker = getWorkerByPath(fc.scriptFilename)
|
||||
}
|
||||
|
||||
c := context.WithValue(r.Context(), contextKey, fc)
|
||||
|
||||
return r.WithContext(c), nil
|
||||
|
||||
@@ -79,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" ]
|
||||
|
||||
@@ -80,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" ]
|
||||
|
||||
@@ -116,6 +116,7 @@ target "default" {
|
||||
args = {
|
||||
FRANKENPHP_VERSION = VERSION
|
||||
}
|
||||
secret = ["id=github-token,env=GITHUB_TOKEN"]
|
||||
}
|
||||
|
||||
target "static-builder-musl" {
|
||||
|
||||
@@ -5,7 +5,7 @@ Without any additional configuration, FrankenPHP operates in classic mode. In th
|
||||
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, you can use the `max_wait_time` [configuration](config.md#caddyfile-config) to limit how long a request may wait for a free PHP thread before being rejected.
|
||||
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.
|
||||
|
||||
@@ -17,7 +17,7 @@ docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -
|
||||
- 附加配置文件: `/etc/frankenphp/php.d/*.ini`
|
||||
- php 扩展: `/usr/lib/frankenphp/modules/`
|
||||
|
||||
如果您的 docker 版本低于 23.0,则会因为 dockerignore [pattern issue](https://github.com/moby/moby/pull/42676) 而导致构建失败。将目录添加到 `.dockerignore`。
|
||||
如果你的 docker 版本低于 23.0,则会因为 dockerignore [pattern issue](https://github.com/moby/moby/pull/42676) 而导致构建失败。将目录添加到 `.dockerignore`。
|
||||
|
||||
```patch
|
||||
!testdata/*.php
|
||||
|
||||
@@ -4,22 +4,49 @@
|
||||
|
||||
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.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
|
||||
### 独立二进制
|
||||
|
||||
我们为 Linux 和 macOS 提供包含 [PHP 8.4](https://www.php.net/releases/8.4/zh.php) 以及大多数常用 PHP 扩展的 FrankenPHP 静态二进制文件。
|
||||
|
||||
在 Windows 上,请使用 [WSL](https://learn.microsoft.com/windows/wsl/) 运行 FrankenPHP。
|
||||
|
||||
你可以 [下载 FrankenPHP](https://github.com/dunglas/frankenphp/releases),或将以下命令复制到终端中,自动安装适用于你平台的版本:
|
||||
|
||||
```console
|
||||
docker run -v $PWD:/app/public \
|
||||
curl https://frankenphp.dev/install.sh | sh
|
||||
mv frankenphp /usr/local/bin/
|
||||
```
|
||||
|
||||
要提供当前目录的内容,请运行:
|
||||
|
||||
```console
|
||||
frankenphp php-server
|
||||
```
|
||||
|
||||
你还可以使用以下命令运行命令行脚本:
|
||||
|
||||
```console
|
||||
frankenphp php-cli /path/to/your/script.php
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
@@ -31,48 +58,47 @@ docker run -v $PWD:/app/public \
|
||||
> 不要尝试使用 `https://127.0.0.1`。使用 `https://localhost` 并接受自签名证书。
|
||||
> 使用 [`SERVER_NAME` 环境变量](config.md#环境变量) 更改要使用的域。
|
||||
|
||||
### 独立二进制
|
||||
### 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
|
||||
```
|
||||
|
||||
您还可以使用以下命令运行命令行脚本:
|
||||
要提供当前目录的内容,请运行:
|
||||
|
||||
```console
|
||||
./frankenphp php-cli /path/to/your/script.php
|
||||
frankenphp php-server
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
* [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/)
|
||||
- [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/)
|
||||
|
||||
## 示例和框架
|
||||
|
||||
* [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)
|
||||
|
||||
@@ -16,7 +16,7 @@ tar xf php-*
|
||||
cd php-*/
|
||||
```
|
||||
|
||||
然后,为您的平台配置 PHP.
|
||||
然后,为你的平台配置 PHP.
|
||||
|
||||
这些参数是必需的,但你也可以添加其他编译参数(例如额外的扩展)。
|
||||
|
||||
@@ -63,10 +63,10 @@ sudo make install
|
||||
|
||||
## 编译 Go 应用
|
||||
|
||||
您现在可以使用 Go 库并编译我们的 Caddy 构建:
|
||||
你现在可以使用 Go 库并编译我们的 Caddy 构建:
|
||||
|
||||
```console
|
||||
curl -L https://github.com/dunglas/frankenphp/archive/refs/heads/main.tar.gz | tar xz
|
||||
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
|
||||
```
|
||||
@@ -90,9 +90,9 @@ xcaddy build \
|
||||
> [!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\'"'`
|
||||
> (根据您的应用需求更改堆栈大小)。
|
||||
> (根据你的应用需求更改堆栈大小)。
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 项目提供的官方模板:
|
||||
- 你应该复制 PHP 项目提供的官方模板:
|
||||
|
||||
```dockerfile
|
||||
FROM dunglas/frankenphp
|
||||
@@ -39,7 +39,7 @@ FrankenPHP 安装 (.rpm 或 .deb):
|
||||
|
||||
## Caddyfile 配置
|
||||
|
||||
可以在站点块中使用 `php_server` 或 `php` [HTTP 指令](https://caddyserver.com/docs/caddyfile/concepts#directives) 来为您的 PHP 应用程序提供服务。
|
||||
可以在站点块中使用 `php_server` 或 `php` [HTTP 指令](https://caddyserver.com/docs/caddyfile/concepts#directives) 来为你的 PHP 应用程序提供服务。
|
||||
|
||||
最小示例:
|
||||
|
||||
@@ -52,7 +52,7 @@ localhost {
|
||||
}
|
||||
```
|
||||
|
||||
您也可以使用全局选项显式配置 FrankenPHP:
|
||||
你也可以使用全局选项显式配置 FrankenPHP:
|
||||
`frankenphp` [全局选项](https://caddyserver.com/docs/caddyfile/concepts#global-options) 可用于配置 FrankenPHP。
|
||||
|
||||
```caddyfile
|
||||
@@ -70,7 +70,7 @@ localhost {
|
||||
# ...
|
||||
```
|
||||
|
||||
或者,您可以使用 `worker` 选项的一行缩写形式:
|
||||
或者,你可以使用 `worker` 选项的一行缩写形式:
|
||||
|
||||
```caddyfile
|
||||
{
|
||||
@@ -86,13 +86,15 @@ localhost {
|
||||
|
||||
```caddyfile
|
||||
app.example.com {
|
||||
root /path/to/app/public
|
||||
php_server {
|
||||
root /path/to/app/public
|
||||
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>
|
||||
|
||||
@@ -77,7 +77,7 @@ FrankenPHP 提供的 `builder` 镜像包含 libphp 的编译版本。
|
||||
> [!TIP]
|
||||
>
|
||||
> 如果你的系统基于 musl libc(Alpine Linux 上默认使用)并搭配 Symfony 使用,
|
||||
> 您可能需要 [增加默认堆栈大小](compile.md#使用-xcaddy)。
|
||||
> 你可能需要 [增加默认堆栈大小](compile.md#使用-xcaddy)。
|
||||
|
||||
## 默认启用 worker 模式
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ FrankenPHP 能够将 PHP 应用程序的源代码和资源文件嵌入到静态
|
||||
|
||||
在创建独立二进制文件之前,请确保应用已准备好进行打包。
|
||||
|
||||
例如,您可能希望:
|
||||
例如,你可能希望:
|
||||
|
||||
* 给应用安装生产环境的依赖
|
||||
* 导出 autoloader
|
||||
* 如果可能,为应用启用生产模式
|
||||
* 丢弃不需要的文件,例如 `.git` 或测试文件,以减小最终二进制文件的大小
|
||||
|
||||
例如,对于 Symfony 应用程序,您可以使用以下命令:
|
||||
例如,对于 Symfony 应用程序,你可以使用以下命令:
|
||||
|
||||
```console
|
||||
# 导出项目以避免 .git/ 等目录
|
||||
@@ -80,10 +80,10 @@ composer dump-env prod
|
||||
|
||||
## 为其他操作系统创建二进制文件
|
||||
|
||||
如果您不想使用 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 \
|
||||
@@ -94,7 +94,7 @@ EMBED=/path/to/your/app \
|
||||
|
||||
## 使用二进制文件
|
||||
|
||||
就是这样!`my-app` 文件(或其他操作系统上的 `dist/frankenphp-<os>-<arch>`)包含您的独立应用程序!
|
||||
就是这样!`my-app` 文件(或其他操作系统上的 `dist/frankenphp-<os>-<arch>`)包含你的独立应用程序!
|
||||
|
||||
若要启动 Web 应用,请执行:
|
||||
|
||||
@@ -102,7 +102,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,7 +114,7 @@ EMBED=/path/to/your/app \
|
||||
./my-app php-server --domain localhost
|
||||
```
|
||||
|
||||
您还可以运行二进制文件中嵌入的 PHP CLI 脚本:
|
||||
你还可以运行二进制文件中嵌入的 PHP CLI 脚本:
|
||||
|
||||
```console
|
||||
./my-app php-cli bin/console
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 使用 GitHub Actions
|
||||
|
||||
此存储库构建 Docker 镜像并将其部署到 [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) 上
|
||||
每个批准的拉取请求或设置后在您自己的分支上。
|
||||
每个批准的拉取请求或设置后在你自己的分支上。
|
||||
|
||||
## 设置 GitHub Actions
|
||||
|
||||
|
||||
@@ -25,7 +25,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 +37,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 +55,7 @@ docker run \
|
||||
>
|
||||
> 请务必将 `172.17.0.3` 替换为将分配给容器的 IP。
|
||||
|
||||
您现在应该能够从主机访问 `https://127.0.0.1`。
|
||||
你现在应该能够从主机访问 `https://127.0.0.1`。
|
||||
|
||||
如果不是这种情况,请在调试模式下启动 FrankenPHP 以尝试找出问题:
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ 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
|
||||
@@ -45,7 +45,7 @@ Octane 可以通过 Composer 包管理器安装:
|
||||
composer require laravel/octane
|
||||
```
|
||||
|
||||
安装 Octane 后,您可以执行 `octane:install` Artisan 命令,该命令会将 Octane 的配置文件安装到您的应用程序中:
|
||||
安装 Octane 后,你可以执行 `octane:install` Artisan 命令,该命令会将 Octane 的配置文件安装到你的应用程序中:
|
||||
|
||||
```console
|
||||
php artisan octane:install --server=frankenphp
|
||||
|
||||
@@ -9,4 +9,4 @@ Mercure 允许将事件实时推送到所有连接的设备:它们将立即收
|
||||
|
||||
要启用 Mercure Hub,请按照 [Mercure 网站](https://mercure.rocks/docs/hub/config) 中的说明更新 `Caddyfile`。
|
||||
|
||||
要从您的代码中推送 Mercure 更新,我们推荐 [Symfony Mercure Component](https://symfony.com/components/Mercure)(不需要 Symfony 框架来使用)。
|
||||
要从你的代码中推送 Mercure 更新,我们推荐 [Symfony Mercure Component](https://symfony.com/components/Mercure)(不需要 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,7 +13,7 @@
|
||||
```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
|
||||
@@ -30,8 +30,8 @@ COPY . /app/public
|
||||
有关更多详细信息和选项,请参阅 [构建自定义 Docker 镜像](docker.md)。
|
||||
要了解如何自定义配置,请安装 PHP 扩展和 Caddy 模块。
|
||||
|
||||
如果您的项目使用 Composer,
|
||||
请务必将其包含在 Docker 镜像中并安装您的依赖。
|
||||
如果你的项目使用 Composer,
|
||||
请务必将其包含在 Docker 镜像中并安装你的依赖。
|
||||
|
||||
然后,添加一个 `compose.yaml` 文件:
|
||||
|
||||
@@ -63,25 +63,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 +92,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 +111,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/)。
|
||||
|
||||
@@ -127,7 +127,7 @@ git clone git@github.com:<username>/<project-name>.git
|
||||
docker compose up -d --wait
|
||||
```
|
||||
|
||||
您的服务器已启动并运行,并且已自动为您生成 HTTPS 证书。
|
||||
你的服务器已启动并运行,并且已自动为你生成 HTTPS 证书。
|
||||
去 `https://your-domain-name.example.com` 享受吧!
|
||||
|
||||
> [!CAUTION]
|
||||
|
||||
@@ -18,7 +18,7 @@ docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-b
|
||||
|
||||
生成的静态二进制文件名为 `frankenphp`,可在当前目录中找到。
|
||||
|
||||
如果您想在没有 Docker 的情况下构建静态二进制文件,请查看 macOS 说明,它也适用于 Linux。
|
||||
如果你想在没有 Docker 的情况下构建静态二进制文件,请查看 macOS 说明,它也适用于 Linux。
|
||||
|
||||
### 自定义扩展
|
||||
|
||||
@@ -77,7 +77,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
|
||||
```
|
||||
|
||||
@@ -96,7 +96,7 @@ docker run \
|
||||
```
|
||||
|
||||
默认情况下,每个 CPU 启动一个 worker。
|
||||
您还可以配置要启动的 worker 数:
|
||||
你还可以配置要启动的 worker 数:
|
||||
|
||||
```console
|
||||
docker run \
|
||||
|
||||
@@ -121,7 +121,7 @@ xcaddy build \
|
||||
Alternatively, it's possible to compile FrankenPHP without `xcaddy` by using the `go` command directly:
|
||||
|
||||
```console
|
||||
curl -L https://github.com/dunglas/frankenphp/archive/refs/heads/main.tar.gz | tar xz
|
||||
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
|
||||
```
|
||||
|
||||
@@ -71,6 +71,7 @@ The `frankenphp` [global option](https://caddyserver.com/docs/caddyfile/concepts
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,13 +95,15 @@ You can also define multiple workers if you serve multiple apps on the same serv
|
||||
|
||||
```caddyfile
|
||||
app.example.com {
|
||||
root /path/to/app/public
|
||||
php_server {
|
||||
root /path/to/app/public
|
||||
root /path/to/app/public # allows for better caching
|
||||
worker index.php <num>
|
||||
}
|
||||
}
|
||||
|
||||
other.example.com {
|
||||
root /path/to/other/public
|
||||
php_server {
|
||||
root /path/to/other/public
|
||||
worker index.php <num>
|
||||
@@ -113,7 +116,7 @@ other.example.com {
|
||||
Using the `php_server` directive is generally what you need,
|
||||
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).
|
||||
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:
|
||||
|
||||
@@ -153,6 +156,7 @@ php_server [<matcher>] {
|
||||
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.
|
||||
}
|
||||
@@ -203,6 +207,29 @@ where the FrankenPHP process was started. You can instead also specify one or mo
|
||||
|
||||
The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher).
|
||||
|
||||
## Matching the worker to a path
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
@@ -236,6 +263,7 @@ You can find more information about this setting in the [Caddy documentation](ht
|
||||
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
|
||||
- `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
|
||||
|
||||
|
||||
@@ -89,7 +89,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
|
||||
```
|
||||
|
||||
773
docs/extensions.md
Normal file
773
docs/extensions.md
Normal file
@@ -0,0 +1,773 @@
|
||||
# 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 github.com/my-account/my-module
|
||||
```
|
||||
|
||||
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
|
||||
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)
|
||||
}
|
||||
```
|
||||
|
||||
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.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | ✅ |
|
||||
| `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 the `frankenphp.Array` type. This type represents both PHP indexed arrays (lists) and associative arrays (hashmaps) with ordered key-value pairs.
|
||||
|
||||
**Creating and manipulating arrays in Go:**
|
||||
|
||||
```go
|
||||
//export_php:function process_data(array $input): array
|
||||
func process_data(arr *C.zval) unsafe.Pointer {
|
||||
// Convert PHP array to Go
|
||||
goArray := frankenphp.GoArray(unsafe.Pointer(arr))
|
||||
|
||||
result := &frankenphp.Array{}
|
||||
|
||||
result.SetInt(0, "first")
|
||||
result.SetInt(1, "second")
|
||||
result.Append("third") // Automatically assigns next integer key
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert back to PHP array
|
||||
return frankenphp.PHPArray(result)
|
||||
}
|
||||
```
|
||||
|
||||
**Key features of `frankenphp.Array`:**
|
||||
|
||||
* **Ordered key-value pairs** - Maintains insertion order like PHP arrays
|
||||
* **Mixed key types** - Supports both integer and string keys in the same array
|
||||
* **Type safety** - The `PHPKey` type ensures proper key handling
|
||||
* **Automatic list detection** - When converting to PHP, automatically detects if array should be a packed list or hashmap
|
||||
* **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:**
|
||||
|
||||
* `SetInt(key int64, value interface{})` - Set value with integer key
|
||||
* `SetString(key string, value interface{})` - Set value with string key
|
||||
* `Append(value interface{})` - Add value with next available integer key
|
||||
* `Len() uint32` - Get number of elements
|
||||
* `At(index uint32) (PHPKey, interface{})` - Get key-value pair at index
|
||||
* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convert to PHP array
|
||||
|
||||
### 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
|
||||
//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
|
||||
//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
|
||||
//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.
|
||||
|
||||
### Module Initialization and Shutdown
|
||||
|
||||
The generator supports defining module initialization and shutdown functions using the `//export_php:module` directive.
|
||||
This allows you to perform setup and cleanup operations when your extension is loaded and unloaded.
|
||||
|
||||
To define an initialization function, tag it with `//export_php:module init`:
|
||||
|
||||
```go
|
||||
//export_php:module init
|
||||
func initializeModule() {
|
||||
// Perform initialization tasks
|
||||
// For example, set up global resources, initialize data structures, etc.
|
||||
}
|
||||
```
|
||||
|
||||
To define a shutdown function, tag it with `//export_php:module shutdown`:
|
||||
|
||||
```go
|
||||
//export_php:module shutdown
|
||||
func cleanupModule() {
|
||||
// Perform cleanup tasks
|
||||
// For example, free resources, close connections, etc.
|
||||
}
|
||||
```
|
||||
|
||||
You can define either one, both, or none of these functions. The initialization function will be called when the PHP module is loaded, and the shutdown function will be called when the PHP module is unloaded.
|
||||
|
||||
### Declaring Constants
|
||||
|
||||
The generator supports exporting Go constants to PHP using two directives: `//export_php:const` for global constants and `//export_php:classconstant` 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
|
||||
//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:classconstant ClassName` directive to create constants that belong to a specific PHP class:
|
||||
|
||||
```go
|
||||
//export_php:classconstant User
|
||||
const STATUS_ACTIVE = 1
|
||||
|
||||
//export_php:classconstant User
|
||||
const STATUS_INACTIVE = 0
|
||||
|
||||
//export_php:classconstant User
|
||||
const ROLE_ADMIN = "admin"
|
||||
|
||||
//export_php:classconstant Order
|
||||
const STATE_PENDING = iota
|
||||
|
||||
//export_php:classconstant Order
|
||||
const STATE_PROCESSING = iota
|
||||
|
||||
//export_php:classconstant 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
|
||||
import (
|
||||
"C"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//export_php:const
|
||||
const STR_REVERSE = iota
|
||||
|
||||
//export_php:const
|
||||
const STR_NORMAL = iota
|
||||
|
||||
//export_php:classconstant StringProcessor
|
||||
const MODE_LOWERCASE = 1
|
||||
|
||||
//export_php:classconstant 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 main
|
||||
|
||||
import "C"
|
||||
|
||||
//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_FILE=php-src/build/gen_stub.php frankenphp extension-init my_extension.go
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Don't forget to set the `GEN_STUB_FILE` 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 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!")
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
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))
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -23,7 +23,7 @@ contenant [PHP 8.4](https://www.php.net/releases/8.4/fr.php) et la plupart des e
|
||||
|
||||
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 :
|
||||
[Téléchargez FrankenPHP](https://github.com/php/frankenphp/releases) ou copiez cette ligne dans votre terminal pour installer automatiquement la version appropriée à votre plateforme :
|
||||
|
||||
```console
|
||||
curl https://frankenphp.dev/install.sh | sh
|
||||
@@ -83,6 +83,7 @@ frankenphp php-server
|
||||
- [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)
|
||||
|
||||
@@ -5,7 +5,7 @@ Sans aucune configuration additionnelle, FrankenPHP fonctionne en mode classique
|
||||
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` pour limiter la durée pendant laquelle une requête peut attendre un thread PHP libre avant d'être rejetée.
|
||||
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`.
|
||||
|
||||
@@ -122,7 +122,7 @@ xcaddy build \
|
||||
Il est également possible de compiler FrankenPHP sans `xcaddy` en utilisant directement la commande `go` :
|
||||
|
||||
```console
|
||||
curl -L https://github.com/dunglas/frankenphp/archive/refs/heads/main.tar.gz | tar xz
|
||||
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
|
||||
```
|
||||
|
||||
@@ -70,6 +70,7 @@ L'[option globale](https://caddyserver.com/docs/caddyfile/concepts#global-option
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,13 +94,15 @@ Vous pouvez aussi définir plusieurs workers si vous servez plusieurs applicatio
|
||||
|
||||
```caddyfile
|
||||
app.example.com {
|
||||
root /path/to/app/public
|
||||
php_server {
|
||||
root /path/to/app/public
|
||||
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
|
||||
worker index.php <num>
|
||||
@@ -112,7 +115,7 @@ 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).
|
||||
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 :
|
||||
|
||||
@@ -235,6 +238,7 @@ Vous trouverez plus d'informations sur ce paramètre dans la [documentation Cadd
|
||||
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é
|
||||
- `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`
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ 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 ./build-static.sh
|
||||
```
|
||||
|
||||
746
docs/fr/extensions.md
Normal file
746
docs/fr/extensions.md
Normal file
@@ -0,0 +1,746 @@
|
||||
# É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
|
||||
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)
|
||||
}
|
||||
```
|
||||
|
||||
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 la 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.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | ✅ |
|
||||
| `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 le type `frankenphp.Array`. Ce type représente à la fois les tableaux indexés PHP (listes) et les tableaux associatifs (hashmaps) avec des paires clé-valeur ordonnées.
|
||||
|
||||
**Créer et manipuler des tableaux en Go :**
|
||||
|
||||
```go
|
||||
//export_php:function process_data(array $input): array
|
||||
func process_data(arr *C.zval) unsafe.Pointer {
|
||||
// Convertir le tableau PHP vers Go
|
||||
goArray := frankenphp.GoArray(unsafe.Pointer(arr))
|
||||
|
||||
result := &frankenphp.Array{}
|
||||
|
||||
result.SetInt(0, "first")
|
||||
result.SetInt(1, "second")
|
||||
result.Append("third") // Assigne automatiquement la prochaine clé entière
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Reconvertir vers un tableau PHP
|
||||
return frankenphp.PHPArray(result)
|
||||
}
|
||||
```
|
||||
|
||||
**Fonctionnalités clés de `frankenphp.Array` :**
|
||||
|
||||
* **Paires clé-valeur ordonnées** - Maintient l'ordre d'insertion comme les tableaux PHP
|
||||
* **Types de clés mixtes** - Supporte les clés entières et chaînes dans le même tableau
|
||||
* **Sécurité de type** - Le type `PHPKey` assure une gestion appropriée des clés
|
||||
* **Détection automatique de liste** - Lors de la conversion vers PHP, détecte automatiquement si le tableau doit être une liste compacte ou un hashmap
|
||||
* **Les objets ne sont pas supportés** - Actuellement, seuls les types scalaires et les tableaux sont supportés. Passer un objet en tant qu'élément du tableau résultera d'une valeur `null` dans le tableau PHP.
|
||||
|
||||
**Méthodes disponibles :**
|
||||
|
||||
* `SetInt(key int64, value interface{})` - Définir une valeur avec une clé entière
|
||||
* `SetString(key string, value interface{})` - Définir une valeur avec une clé chaîne
|
||||
* `Append(value interface{})` - Ajouter une valeur avec la prochaine clé entière disponible
|
||||
* `Len() uint32` - Obtenir le nombre d'éléments
|
||||
* `At(index uint32) (PHPKey, interface{})` - Obtenir la paire clé-valeur à l'index
|
||||
* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convertir vers un tableau PHP
|
||||
|
||||
### 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
|
||||
//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
|
||||
//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
|
||||
//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 est null?
|
||||
if name != nil {
|
||||
us.Name = frankenphp.GoString(unsafe.Pointer(name))
|
||||
}
|
||||
|
||||
// $age est null?
|
||||
if age != nil {
|
||||
us.Age = int(*age)
|
||||
}
|
||||
|
||||
// $active est 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:classconstant` 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
|
||||
//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:classconstant ClassName` pour créer des constantes qui appartiennent à une classe PHP spécifique :
|
||||
|
||||
```go
|
||||
//export_php:classconstant User
|
||||
const STATUS_ACTIVE = 1
|
||||
|
||||
//export_php:classconstant User
|
||||
const STATUS_INACTIVE = 0
|
||||
|
||||
//export_php:classconstant User
|
||||
const ROLE_ADMIN = "admin"
|
||||
|
||||
//export_php:classconstant Order
|
||||
const STATE_PENDING = iota
|
||||
|
||||
//export_php:classconstant Order
|
||||
const STATE_PROCESSING = iota
|
||||
|
||||
//export_php:classconstant 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
|
||||
import (
|
||||
"C"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//export_php:const
|
||||
const STR_REVERSE = iota
|
||||
|
||||
//export_php:const
|
||||
const STR_NORMAL = iota
|
||||
|
||||
//export_php:classconstant StringProcessor
|
||||
const MODE_LOWERCASE = 1
|
||||
|
||||
//export_php:classconstant 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 main
|
||||
|
||||
import "C"
|
||||
|
||||
//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_FILE=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_FILE` 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 Go vide, 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 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!")
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
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))
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -78,7 +78,7 @@ docker run \
|
||||
|
||||
## 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/dunglas/frankenphp/issues/483#issuecomment-1899890915) pour deux raisons :
|
||||
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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ GITHUB_TOKEN="xxx" docker --load buildx bake static-builder-musl
|
||||
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
|
||||
```
|
||||
|
||||
@@ -150,6 +150,17 @@ Si le script worker reste en place plus longtemps que le dernier backoff \* 2, F
|
||||
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`...)
|
||||
|
||||
@@ -78,7 +78,7 @@ docker run \
|
||||
|
||||
## Composer Scripts Referencing `@php`
|
||||
|
||||
[Composer scripts](https://getcomposer.org/doc/articles/scripts.md) may want to execute a PHP binary for some tasks, e.g. in [a Laravel project](laravel.md) to run `@php artisan package:discover --ansi`. This [currently fails](https://github.com/dunglas/frankenphp/issues/483#issuecomment-1899890915) for two reasons:
|
||||
[Composer scripts](https://getcomposer.org/doc/articles/scripts.md) may want to execute a PHP binary for some tasks, e.g. in [a Laravel project](laravel.md) to run `@php artisan package:discover --ansi`. This [currently fails](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) for two reasons:
|
||||
|
||||
- Composer does not know how to call the FrankenPHP binary;
|
||||
- Composer may add PHP settings using the `-d` flag in the command, which FrankenPHP does not yet support.
|
||||
|
||||
@@ -43,7 +43,7 @@ Also, [some bugs only happen when using musl](https://github.com/php/php-src/iss
|
||||
|
||||
In production environments, we recommend using FrankenPHP linked against glibc.
|
||||
|
||||
This can be achieved by using the Debian Docker images (the default), downloading the -gnu suffix binary from our [Releases](https://github.com/dunglas/frankenphp/releases), or by [compiling FrankenPHP from sources](compile.md).
|
||||
This can be achieved by using the Debian Docker images (the default), downloading the -gnu suffix binary from our [Releases](https://github.com/php/frankenphp/releases), or by [compiling FrankenPHP from sources](compile.md).
|
||||
|
||||
Alternatively, we provide static musl binaries compiled with [the mimalloc allocator](https://github.com/microsoft/mimalloc), which alleviates the problems in threaded scenarios.
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ ENV SERVER_NAME=your-domain-name.example.com
|
||||
# If you want to disable HTTPS, use this value instead:
|
||||
#ENV SERVER_NAME=:80
|
||||
|
||||
# If your project is not using the "public" directory as the web root, you can set it here:
|
||||
# ENV SERVER_ROOT=web/
|
||||
|
||||
# Enable PHP production settings
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ docker run -v .:/app/public \
|
||||
|
||||
Для Windows используйте [WSL](https://learn.microsoft.com/windows/wsl/) для запуска FrankenPHP.
|
||||
|
||||
[Скачать FrankenPHP](https://github.com/dunglas/frankenphp/releases) или выполните следующую команду для автоматической установки подходящей версии:
|
||||
[Скачать FrankenPHP](https://github.com/php/frankenphp/releases) или выполните следующую команду для автоматической установки подходящей версии:
|
||||
|
||||
```console
|
||||
curl https://frankenphp.dev/install.sh | sh
|
||||
|
||||
@@ -76,7 +76,7 @@ sudo make install
|
||||
Теперь можно собрать итоговый бинарный файл:
|
||||
|
||||
```console
|
||||
curl -L https://github.com/dunglas/frankenphp/archive/refs/heads/main.tar.gz | tar xz
|
||||
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
|
||||
```
|
||||
|
||||
@@ -89,13 +89,15 @@ localhost {
|
||||
|
||||
```caddyfile
|
||||
app.example.com {
|
||||
root /path/to/app/public
|
||||
php_server {
|
||||
root /path/to/app/public
|
||||
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>
|
||||
|
||||
@@ -87,7 +87,7 @@ composer dump-env prod
|
||||
Если вы не хотите использовать Docker или хотите собрать бинарный файл для macOS, используйте предоставленный скрипт:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
@@ -77,7 +77,7 @@ docker run \
|
||||
## Скрипты Composer с использованием `@php`
|
||||
|
||||
[Скрипты Composer](https://getcomposer.org/doc/articles/scripts.md) могут вызывать PHP для выполнения задач, например, в [проекте Laravel](laravel.md) для команды `@php artisan package:discover --ansi`.
|
||||
Это [на данный момент не поддерживается](https://github.com/dunglas/frankenphp/issues/483#issuecomment-1899890915) по двум причинам:
|
||||
Это [на данный момент не поддерживается](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) по двум причинам:
|
||||
|
||||
- Composer не знает, как вызывать бинарный файл FrankenPHP;
|
||||
- Composer может добавлять настройки PHP через флаг `-d`, который FrankenPHP пока не поддерживает.
|
||||
|
||||
@@ -76,7 +76,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
|
||||
```
|
||||
|
||||
@@ -105,7 +105,7 @@ GITHUB_TOKEN="xxx" docker --load buildx bake static-builder-musl
|
||||
Run the following script to create a static binary for macOS (you must have [Homebrew](https://brew.sh/) installed):
|
||||
|
||||
```console
|
||||
git clone https://github.com/dunglas/frankenphp
|
||||
git clone https://github.com/php/frankenphp
|
||||
cd frankenphp
|
||||
./build-static.sh
|
||||
```
|
||||
|
||||
@@ -34,18 +34,18 @@ docker run -v $PWD:/app/public \
|
||||
### Binary Çıktısı
|
||||
|
||||
Docker kullanmayı tercih etmiyorsanız, Linux ve macOS için bağımsız FrankenPHP binary dosyası sağlıyoruz
|
||||
[PHP 8.4](https://www.php.net/releases/8.4/en.php) ve en popüler PHP eklentilerini de içermekte: [FrankenPHP](https://github.com/dunglas/frankenphp/releases) indirin
|
||||
[PHP 8.4](https://www.php.net/releases/8.4/en.php) ve en popüler PHP eklentilerini de içermekte: [FrankenPHP](https://github.com/php/frankenphp/releases) indirin
|
||||
|
||||
Geçerli dizinin içeriğini başlatmak için çalıştırın:
|
||||
|
||||
```console
|
||||
./frankenphp php-server
|
||||
frankenphp php-server
|
||||
```
|
||||
|
||||
Ayrıca aşağıdaki tek komut satırı ile de çalıştırabilirsiniz:
|
||||
|
||||
```console
|
||||
./frankenphp php-cli /path/to/your/script.php
|
||||
frankenphp php-cli /path/to/your/script.php
|
||||
```
|
||||
|
||||
## Docs
|
||||
|
||||
@@ -68,7 +68,7 @@ sudo make install
|
||||
Artık Go kütüphanesini kullanabilir ve Caddy yapımızı derleyebilirsiniz:
|
||||
|
||||
```console
|
||||
curl -L https://github.com/dunglas/frankenphp/archive/refs/heads/main.tar.gz | tar xz
|
||||
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
|
||||
```
|
||||
|
||||
@@ -88,13 +88,15 @@ Aynı sunucuda birden fazla uygulamaya hizmet veriyorsanız birden fazla işçi
|
||||
|
||||
```caddyfile
|
||||
app.example.com {
|
||||
root /path/to/app/public
|
||||
php_server {
|
||||
root /path/to/app/public
|
||||
root /path/to/app/public # daha iyi önbelleğe almayı sağlar
|
||||
worker index.php <num>
|
||||
}
|
||||
}
|
||||
|
||||
other.example.com {
|
||||
root /path/to/other/public
|
||||
php_server {
|
||||
root /path/to/other/public
|
||||
worker index.php <num>
|
||||
|
||||
@@ -83,7 +83,7 @@ Elde edilen binary dosyası, geçerli dizindeki `my-app` adlı dosyadır.
|
||||
Docker kullanmak istemiyorsanız veya bir macOS binary dosyası oluşturmak istiyorsanız, sağladığımız kabuk betiğini kullanın:
|
||||
|
||||
```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 \
|
||||
|
||||
@@ -76,7 +76,7 @@ docker run \
|
||||
|
||||
## `@php` Referanslı Composer Betikler
|
||||
|
||||
[Composer betikleri](https://getcomposer.org/doc/articles/scripts.md) bazı görevler için bir PHP binary çalıştırmak isteyebilir, örneğin [bir Laravel projesinde](laravel.md) `@php artisan package:discover --ansi` çalıştırmak. Bu [şu anda mümkün değil](https://github.com/dunglas/frankenphp/issues/483#issuecomment-1899890915) ve 2 nedeni var:
|
||||
[Composer betikleri](https://getcomposer.org/doc/articles/scripts.md) bazı görevler için bir PHP binary çalıştırmak isteyebilir, örneğin [bir Laravel projesinde](laravel.md) `@php artisan package:discover --ansi` çalıştırmak. Bu [şu anda mümkün değil](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) ve 2 nedeni var:
|
||||
|
||||
- Composer FrankenPHP binary dosyasını nasıl çağıracağını bilmiyor;
|
||||
- Composer, FrankenPHP'nin henüz desteklemediği `-d` bayrağını kullanarak PHP ayarlarını komuta ekleyebilir.
|
||||
|
||||
@@ -16,7 +16,7 @@ And tadını çıkarın!
|
||||
|
||||
Alternatif olarak, Laravel projelerinizi FrankenPHP ile yerel makinenizden çalıştırabilirsiniz:
|
||||
|
||||
1. [Sisteminize karşılık gelen binary dosyayı indirin](https://github.com/dunglas/frankenphp/releases)
|
||||
1. [Sisteminize karşılık gelen binary dosyayı indirin](https://github.com/php/frankenphp/releases)
|
||||
2. Aşağıdaki yapılandırmayı Laravel projenizin kök dizinindeki `Caddyfile` adlı bir dosyaya ekleyin:
|
||||
|
||||
```caddyfile
|
||||
|
||||
@@ -77,7 +77,7 @@ GITHUB_TOKEN="xxx" docker --load buildx bake static-builder
|
||||
macOS için statik bir binary oluşturmak için aşağıdaki betiği çalıştırın ([Homebrew](https://brew.sh/) yüklü olmalıdır):
|
||||
|
||||
```console
|
||||
git clone https://github.com/dunglas/frankenphp
|
||||
git clone https://github.com/php/frankenphp
|
||||
cd frankenphp
|
||||
./build-static.sh
|
||||
```
|
||||
|
||||
@@ -146,6 +146,17 @@ it will not penalize the worker script and restart it again.
|
||||
However, if the worker script continues to fail with a non-zero exit code in a short period of time
|
||||
(for example, having a typo in a script), FrankenPHP will crash with the error: `too many consecutive failures`.
|
||||
|
||||
The number of consecutive failures can be configured in your [Caddyfile](config.md#caddyfile-config) with the `max_consecutive_failures` option:
|
||||
|
||||
```caddyfile
|
||||
frankenphp {
|
||||
worker {
|
||||
# ...
|
||||
max_consecutive_failures 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Superglobals Behavior
|
||||
|
||||
[PHP superglobals](https://www.php.net/manual/en/language.variables.superglobals.php) (`$_SERVER`, `$_ENV`, `$_GET`...)
|
||||
|
||||
29
ext.go
Normal file
29
ext.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package frankenphp
|
||||
|
||||
//#include "frankenphp.h"
|
||||
import "C"
|
||||
import (
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
extensions []*C.zend_module_entry
|
||||
registerOnce sync.Once
|
||||
)
|
||||
|
||||
// RegisterExtension registers a new PHP extension.
|
||||
func RegisterExtension(me unsafe.Pointer) {
|
||||
extensions = append(extensions, (*C.zend_module_entry)(me))
|
||||
}
|
||||
|
||||
func registerExtensions() {
|
||||
if len(extensions) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
registerOnce.Do(func() {
|
||||
C.register_extensions(extensions[0], C.int(len(extensions)))
|
||||
extensions = nil
|
||||
})
|
||||
}
|
||||
113
frankenphp.c
113
frankenphp.c
@@ -33,6 +33,13 @@
|
||||
ZEND_TSRMLS_CACHE_DEFINE()
|
||||
#endif
|
||||
|
||||
/**
|
||||
* The list of modules to reload on each request. If an external module
|
||||
* requires to be reloaded between requests, it is possible to hook on
|
||||
* `sapi_module.activate` and `sapi_module.deactivate`.
|
||||
*
|
||||
* @see https://github.com/DataDog/dd-trace-php/pull/3169 for an example
|
||||
*/
|
||||
static const char *MODULES_TO_RELOAD[] = {"filter", "session", NULL};
|
||||
|
||||
frankenphp_version frankenphp_get_version() {
|
||||
@@ -121,7 +128,6 @@ static void frankenphp_worker_request_shutdown() {
|
||||
zend_try { php_output_end_all(); }
|
||||
zend_end_try();
|
||||
|
||||
/* TODO: store the list of modules to reload in a global module variable */
|
||||
const char **module_name;
|
||||
zend_module_entry *module;
|
||||
for (module_name = MODULES_TO_RELOAD; *module_name; module_name++) {
|
||||
@@ -223,7 +229,6 @@ static int frankenphp_worker_request_startup() {
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: store the list of modules to reload in a global module variable */
|
||||
const char **module_name;
|
||||
zend_module_entry *module;
|
||||
for (module_name = MODULES_TO_RELOAD; *module_name; module_name++) {
|
||||
@@ -243,9 +248,7 @@ static int frankenphp_worker_request_startup() {
|
||||
}
|
||||
|
||||
PHP_FUNCTION(frankenphp_finish_request) { /* {{{ */
|
||||
if (zend_parse_parameters_none() == FAILURE) {
|
||||
RETURN_THROWS();
|
||||
}
|
||||
ZEND_PARSE_PARAMETERS_NONE();
|
||||
|
||||
if (go_is_context_done(thread_index)) {
|
||||
RETURN_FALSE;
|
||||
@@ -313,9 +316,7 @@ PHP_FUNCTION(frankenphp_getenv) {
|
||||
|
||||
/* {{{ Fetch all HTTP request headers */
|
||||
PHP_FUNCTION(frankenphp_request_headers) {
|
||||
if (zend_parse_parameters_none() == FAILURE) {
|
||||
RETURN_THROWS();
|
||||
}
|
||||
ZEND_PARSE_PARAMETERS_NONE();
|
||||
|
||||
struct go_apache_request_headers_return headers =
|
||||
go_apache_request_headers(thread_index);
|
||||
@@ -373,9 +374,7 @@ static void add_response_header(sapi_header_struct *h,
|
||||
|
||||
PHP_FUNCTION(frankenphp_response_headers) /* {{{ */
|
||||
{
|
||||
if (zend_parse_parameters_none() == FAILURE) {
|
||||
RETURN_THROWS();
|
||||
}
|
||||
ZEND_PARSE_PARAMETERS_NONE();
|
||||
|
||||
array_init(return_value);
|
||||
zend_llist_apply_with_argument(
|
||||
@@ -546,10 +545,7 @@ static int frankenphp_startup(sapi_module_struct *sapi_module) {
|
||||
return php_module_startup(sapi_module, &frankenphp_module);
|
||||
}
|
||||
|
||||
static int frankenphp_deactivate(void) {
|
||||
/* TODO: flush everything */
|
||||
return SUCCESS;
|
||||
}
|
||||
static int frankenphp_deactivate(void) { return SUCCESS; }
|
||||
|
||||
static size_t frankenphp_ub_write(const char *str, size_t str_length) {
|
||||
struct go_ub_write_return result =
|
||||
@@ -641,7 +637,7 @@ void frankenphp_register_bulk(
|
||||
ht_key_value_pair gateway_interface, ht_key_value_pair server_protocol,
|
||||
ht_key_value_pair server_software, ht_key_value_pair http_host,
|
||||
ht_key_value_pair auth_type, ht_key_value_pair remote_ident,
|
||||
ht_key_value_pair request_uri) {
|
||||
ht_key_value_pair request_uri, ht_key_value_pair ssl_cipher) {
|
||||
HashTable *ht = Z_ARRVAL_P(track_vars_array);
|
||||
frankenphp_register_trusted_var(remote_addr.key, remote_addr.val,
|
||||
remote_addr.val_len, ht);
|
||||
@@ -664,6 +660,8 @@ void frankenphp_register_bulk(
|
||||
frankenphp_register_trusted_var(https.key, https.val, https.val_len, ht);
|
||||
frankenphp_register_trusted_var(ssl_protocol.key, ssl_protocol.val,
|
||||
ssl_protocol.val_len, ht);
|
||||
frankenphp_register_trusted_var(ssl_cipher.key, ssl_cipher.val,
|
||||
ssl_cipher.val_len, ht);
|
||||
frankenphp_register_trusted_var(request_scheme.key, request_scheme.val,
|
||||
request_scheme.val_len, ht);
|
||||
frankenphp_register_trusted_var(server_name.key, server_name.val,
|
||||
@@ -750,6 +748,15 @@ void frankenphp_register_variable_safe(char *key, char *val, size_t val_len,
|
||||
}
|
||||
}
|
||||
|
||||
static inline void register_server_variable_filtered(const char *key,
|
||||
char **val,
|
||||
size_t *val_len,
|
||||
zval *track_vars_array) {
|
||||
if (sapi_module.input_filter(PARSE_SERVER, key, val, *val_len, val_len)) {
|
||||
php_register_variable_safe(key, *val, *val_len, track_vars_array);
|
||||
}
|
||||
}
|
||||
|
||||
static void frankenphp_register_variables(zval *track_vars_array) {
|
||||
/* https://www.php.net/manual/en/reserved.variables.server.php */
|
||||
|
||||
@@ -768,6 +775,11 @@ static void frankenphp_register_variables(zval *track_vars_array) {
|
||||
/* In worker mode we cache the os environment */
|
||||
if (os_environment == NULL) {
|
||||
os_environment = malloc(sizeof(zval));
|
||||
if (os_environment == NULL) {
|
||||
php_error(E_ERROR, "Failed to allocate memory for os_environment");
|
||||
|
||||
return;
|
||||
}
|
||||
array_init(os_environment);
|
||||
get_full_env(os_environment);
|
||||
// php_import_environment_variables(os_environment);
|
||||
@@ -1069,7 +1081,7 @@ static void cli_register_file_handles(bool no_close) /* {{{ */
|
||||
|
||||
static void sapi_cli_register_variables(zval *track_vars_array) /* {{{ */
|
||||
{
|
||||
size_t len;
|
||||
size_t len = strlen(cli_script);
|
||||
char *docroot = "";
|
||||
|
||||
/*
|
||||
@@ -1079,33 +1091,21 @@ static void sapi_cli_register_variables(zval *track_vars_array) /* {{{ */
|
||||
php_import_environment_variables(track_vars_array);
|
||||
|
||||
/* Build the special-case PHP_SELF variable for the CLI version */
|
||||
len = strlen(cli_script);
|
||||
if (sapi_module.input_filter(PARSE_SERVER, "PHP_SELF", &cli_script, len,
|
||||
&len)) {
|
||||
php_register_variable_safe("PHP_SELF", cli_script, len, track_vars_array);
|
||||
}
|
||||
if (sapi_module.input_filter(PARSE_SERVER, "SCRIPT_NAME", &cli_script, len,
|
||||
&len)) {
|
||||
php_register_variable_safe("SCRIPT_NAME", cli_script, len,
|
||||
track_vars_array);
|
||||
}
|
||||
register_server_variable_filtered("PHP_SELF", &cli_script, &len,
|
||||
track_vars_array);
|
||||
register_server_variable_filtered("SCRIPT_NAME", &cli_script, &len,
|
||||
track_vars_array);
|
||||
|
||||
/* filenames are empty for stdin */
|
||||
if (sapi_module.input_filter(PARSE_SERVER, "SCRIPT_FILENAME", &cli_script,
|
||||
len, &len)) {
|
||||
php_register_variable_safe("SCRIPT_FILENAME", cli_script, len,
|
||||
track_vars_array);
|
||||
}
|
||||
if (sapi_module.input_filter(PARSE_SERVER, "PATH_TRANSLATED", &cli_script,
|
||||
len, &len)) {
|
||||
php_register_variable_safe("PATH_TRANSLATED", cli_script, len,
|
||||
track_vars_array);
|
||||
}
|
||||
register_server_variable_filtered("SCRIPT_FILENAME", &cli_script, &len,
|
||||
track_vars_array);
|
||||
register_server_variable_filtered("PATH_TRANSLATED", &cli_script, &len,
|
||||
track_vars_array);
|
||||
|
||||
/* just make it available */
|
||||
len = 0U;
|
||||
if (sapi_module.input_filter(PARSE_SERVER, "DOCUMENT_ROOT", &docroot, len,
|
||||
&len)) {
|
||||
php_register_variable_safe("DOCUMENT_ROOT", docroot, len, track_vars_array);
|
||||
}
|
||||
register_server_variable_filtered("DOCUMENT_ROOT", &docroot, &len,
|
||||
track_vars_array);
|
||||
}
|
||||
/* }}} */
|
||||
|
||||
@@ -1182,3 +1182,34 @@ int frankenphp_reset_opcache(void) {
|
||||
}
|
||||
|
||||
int frankenphp_get_current_memory_limit() { return PG(memory_limit); }
|
||||
|
||||
static zend_module_entry *modules = NULL;
|
||||
static int modules_len = 0;
|
||||
static int (*original_php_register_internal_extensions_func)(void) = NULL;
|
||||
|
||||
PHPAPI int register_internal_extensions(void) {
|
||||
if (original_php_register_internal_extensions_func != NULL &&
|
||||
original_php_register_internal_extensions_func() != SUCCESS) {
|
||||
return FAILURE;
|
||||
}
|
||||
|
||||
for (int i = 0; i < modules_len; i++) {
|
||||
if (zend_register_internal_module(&modules[i]) == NULL) {
|
||||
return FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
modules = NULL;
|
||||
modules_len = 0;
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
void register_extensions(zend_module_entry *m, int len) {
|
||||
modules = m;
|
||||
modules_len = len;
|
||||
|
||||
original_php_register_internal_extensions_func =
|
||||
php_register_internal_extensions_func;
|
||||
php_register_internal_extensions_func = register_internal_extensions;
|
||||
}
|
||||
|
||||
@@ -14,14 +14,6 @@ package frankenphp
|
||||
|
||||
// #cgo nocallback frankenphp_update_server_context
|
||||
// #cgo noescape frankenphp_update_server_context
|
||||
// #cgo darwin pkg-config: libxml-2.0
|
||||
// #cgo CFLAGS: -Wall -Werror
|
||||
// #cgo CFLAGS: -I/usr/local/include -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib
|
||||
// #cgo linux CFLAGS: -D_GNU_SOURCE
|
||||
// #cgo darwin CFLAGS: -I/opt/homebrew/include
|
||||
// #cgo LDFLAGS: -L/usr/local/lib -L/usr/lib -lphp -lm -lutil
|
||||
// #cgo linux LDFLAGS: -ldl -lresolv
|
||||
// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -L/opt/homebrew/lib -L/opt/homebrew/opt/libiconv/lib -liconv -ldl
|
||||
// #include <stdlib.h>
|
||||
// #include <stdint.h>
|
||||
// #include <php_variables.h>
|
||||
@@ -157,7 +149,7 @@ func calculateMaxThreads(opt *opt) (int, int, int, error) {
|
||||
var numWorkers int
|
||||
for i, w := range opt.workers {
|
||||
if w.num <= 0 {
|
||||
// https://github.com/dunglas/frankenphp/issues/126
|
||||
// https://github.com/php/frankenphp/issues/126
|
||||
opt.workers[i].num = maxProcs
|
||||
}
|
||||
metrics.TotalWorkers(w.name, w.num)
|
||||
@@ -222,10 +214,12 @@ func Init(options ...Option) error {
|
||||
}
|
||||
isRunning = true
|
||||
|
||||
// Ignore all SIGPIPE signals to prevent weird issues with systemd: https://github.com/dunglas/frankenphp/issues/1020
|
||||
// Ignore all SIGPIPE signals to prevent weird issues with systemd: https://github.com/php/frankenphp/issues/1020
|
||||
// Docker/Moby has a similar hack: https://github.com/moby/moby/blob/d828b032a87606ae34267e349bf7f7ccb1f6495a/cmd/dockerd/docker.go#L87-L90
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
|
||||
registerExtensions()
|
||||
|
||||
opt := &opt{}
|
||||
for _, o := range options {
|
||||
if err := o(opt); err != nil {
|
||||
@@ -401,8 +395,9 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error
|
||||
}
|
||||
|
||||
// Detect if a worker is available to handle this request
|
||||
if worker, ok := workers[getWorkerKey(fc.workerName, fc.scriptFilename)]; ok {
|
||||
worker.handleRequest(fc)
|
||||
if fc.worker != nil {
|
||||
fc.worker.handleRequest(fc)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -478,14 +473,43 @@ func go_apache_request_headers(threadIndex C.uintptr_t) (*C.go_string, C.size_t)
|
||||
}
|
||||
|
||||
func addHeader(fc *frankenPHPContext, cString *C.char, length C.int) {
|
||||
parts := strings.SplitN(C.GoStringN(cString, length), ": ", 2)
|
||||
if len(parts) != 2 {
|
||||
fc.logger.LogAttrs(context.Background(), slog.LevelDebug, "invalid header", slog.String("header", parts[0]))
|
||||
|
||||
key, val := splitRawHeader(cString, int(length))
|
||||
if key == "" {
|
||||
fc.logger.LogAttrs(context.Background(), slog.LevelDebug, "invalid header", slog.String("header", C.GoStringN(cString, length)))
|
||||
return
|
||||
}
|
||||
fc.responseWriter.Header().Add(key, val)
|
||||
}
|
||||
|
||||
fc.responseWriter.Header().Add(parts[0], parts[1])
|
||||
// split the raw header coming from C with minimal allocations
|
||||
func splitRawHeader(rawHeader *C.char, length int) (string, string) {
|
||||
buf := unsafe.Slice((*byte)(unsafe.Pointer(rawHeader)), length)
|
||||
|
||||
// Search for the colon in 'Header-Key: value'
|
||||
var i int
|
||||
for i = 0; i < length; i++ {
|
||||
if buf[i] == ':' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if i == length {
|
||||
return "", "" // No colon found, invalid header
|
||||
}
|
||||
|
||||
headerKey := C.GoStringN(rawHeader, C.int(i))
|
||||
|
||||
// skip whitespaces after the colon
|
||||
j := i + 1
|
||||
for j < length && buf[j] == ' ' {
|
||||
j++
|
||||
}
|
||||
|
||||
// anything left is the header value
|
||||
valuePtr := (*C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(rawHeader)) + uintptr(j)))
|
||||
headerValue := C.GoStringN(valuePtr, C.int(length-j))
|
||||
|
||||
return headerKey, headerValue
|
||||
}
|
||||
|
||||
//export go_write_headers
|
||||
@@ -611,6 +635,9 @@ func go_is_context_done(threadIndex C.uintptr_t) C.bool {
|
||||
// ExecuteScriptCLI executes the PHP script passed as parameter.
|
||||
// It returns the exit status code of the script.
|
||||
func ExecuteScriptCLI(script string, args []string) int {
|
||||
// Ensure extensions are registered before CLI execution
|
||||
registerExtensions()
|
||||
|
||||
cScript := C.CString(script)
|
||||
defer C.free(unsafe.Pointer(cScript))
|
||||
|
||||
@@ -621,6 +648,9 @@ func ExecuteScriptCLI(script string, args []string) int {
|
||||
}
|
||||
|
||||
func ExecutePHPCode(phpCode string) int {
|
||||
// Ensure extensions are registered before CLI execution
|
||||
registerExtensions()
|
||||
|
||||
cCode := C.CString(phpCode)
|
||||
defer C.free(unsafe.Pointer(cCode))
|
||||
return int(C.frankenphp_execute_script_cli(cCode, 0, nil, true))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef _FRANKENPPHP_H
|
||||
#define _FRANKENPPHP_H
|
||||
#ifndef _FRANKENPHP_H
|
||||
#define _FRANKENPHP_H
|
||||
|
||||
#include <Zend/zend_modules.h>
|
||||
#include <Zend/zend_types.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
@@ -90,6 +91,8 @@ void frankenphp_register_bulk(
|
||||
ht_key_value_pair gateway_interface, ht_key_value_pair server_protocol,
|
||||
ht_key_value_pair server_software, ht_key_value_pair http_host,
|
||||
ht_key_value_pair auth_type, ht_key_value_pair remote_ident,
|
||||
ht_key_value_pair request_uri);
|
||||
ht_key_value_pair request_uri, ht_key_value_pair ssl_cipher);
|
||||
|
||||
void register_extensions(zend_module_entry *m, int len);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -66,7 +66,11 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
|
||||
|
||||
initOpts := []frankenphp.Option{frankenphp.WithLogger(opts.logger)}
|
||||
if opts.workerScript != "" {
|
||||
initOpts = append(initOpts, frankenphp.WithWorkers("workerName", testDataDir+opts.workerScript, opts.nbWorkers, opts.env, opts.watch))
|
||||
workerOpts := []frankenphp.WorkerOption{
|
||||
frankenphp.WithWorkerEnv(opts.env),
|
||||
frankenphp.WithWorkerWatchMode(opts.watch),
|
||||
}
|
||||
initOpts = append(initOpts, frankenphp.WithWorkers("workerName", testDataDir+opts.workerScript, opts.nbWorkers, workerOpts...))
|
||||
}
|
||||
initOpts = append(initOpts, opts.initOpts...)
|
||||
if opts.phpIni != nil {
|
||||
@@ -103,19 +107,40 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func testRequest(req *http.Request, handler func(http.ResponseWriter, *http.Request), t *testing.T) (string, *http.Response) {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
resp := w.Result()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
return string(body), resp
|
||||
}
|
||||
|
||||
func testGet(url string, handler func(http.ResponseWriter, *http.Request), t *testing.T) (string, *http.Response) {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodGet, url, nil)
|
||||
|
||||
return testRequest(req, handler, t)
|
||||
}
|
||||
|
||||
func testPost(url string, body string, handler func(http.ResponseWriter, *http.Request), t *testing.T) (string, *http.Response) {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodPost, url, nil)
|
||||
req.Body = io.NopCloser(strings.NewReader(body))
|
||||
|
||||
return testRequest(req, handler, t)
|
||||
}
|
||||
|
||||
func TestHelloWorld_module(t *testing.T) { testHelloWorld(t, nil) }
|
||||
func TestHelloWorld_worker(t *testing.T) {
|
||||
testHelloWorld(t, &testOptions{workerScript: "index.php"})
|
||||
}
|
||||
func testHelloWorld(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/index.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
assert.Equal(t, fmt.Sprintf("I am by birth a Genevese (%d)", i), string(body))
|
||||
body, _ := testGet(fmt.Sprintf("http://example.com/index.php?i=%d", i), handler, t)
|
||||
assert.Equal(t, fmt.Sprintf("I am by birth a Genevese (%d)", i), body)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -125,13 +150,8 @@ func TestFinishRequest_worker(t *testing.T) {
|
||||
}
|
||||
func testFinishRequest(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/finish-request.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
assert.Equal(t, fmt.Sprintf("This is output %d\n", i), string(body))
|
||||
body, _ := testGet(fmt.Sprintf("http://example.com/finish-request.php?i=%d", i), handler, t)
|
||||
assert.Equal(t, fmt.Sprintf("This is output %d\n", i), body)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -146,39 +166,33 @@ func testServerVariable(t *testing.T, opts *testOptions) {
|
||||
req := httptest.NewRequest("POST", fmt.Sprintf("http://example.com/server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i), strings.NewReader("foo"))
|
||||
req.SetBasicAuth(strings.Clone("kevin"), strings.Clone("password"))
|
||||
req.Header.Add(strings.Clone("Content-Type"), strings.Clone("text/plain"))
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
body, _ := testRequest(req, handler, t)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
strBody := string(body)
|
||||
|
||||
assert.Contains(t, strBody, "[REMOTE_HOST]")
|
||||
assert.Contains(t, strBody, "[REMOTE_USER] => kevin")
|
||||
assert.Contains(t, strBody, "[PHP_AUTH_USER] => kevin")
|
||||
assert.Contains(t, strBody, "[PHP_AUTH_PW] => password")
|
||||
assert.Contains(t, strBody, "[HTTP_AUTHORIZATION] => Basic a2V2aW46cGFzc3dvcmQ=")
|
||||
assert.Contains(t, strBody, "[DOCUMENT_ROOT]")
|
||||
assert.Contains(t, strBody, "[PHP_SELF] => /server-variable.php/baz/bat")
|
||||
assert.Contains(t, strBody, "[CONTENT_TYPE] => text/plain")
|
||||
assert.Contains(t, strBody, fmt.Sprintf("[QUERY_STRING] => foo=a&bar=b&i=%d#hash", i))
|
||||
assert.Contains(t, strBody, fmt.Sprintf("[REQUEST_URI] => /server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i))
|
||||
assert.Contains(t, strBody, "[CONTENT_LENGTH]")
|
||||
assert.Contains(t, strBody, "[REMOTE_ADDR]")
|
||||
assert.Contains(t, strBody, "[REMOTE_PORT]")
|
||||
assert.Contains(t, strBody, "[REQUEST_SCHEME] => http")
|
||||
assert.Contains(t, strBody, "[DOCUMENT_URI]")
|
||||
assert.Contains(t, strBody, "[AUTH_TYPE]")
|
||||
assert.Contains(t, strBody, "[REMOTE_IDENT]")
|
||||
assert.Contains(t, strBody, "[REQUEST_METHOD] => POST")
|
||||
assert.Contains(t, strBody, "[SERVER_NAME] => example.com")
|
||||
assert.Contains(t, strBody, "[SERVER_PROTOCOL] => HTTP/1.1")
|
||||
assert.Contains(t, strBody, "[SCRIPT_FILENAME]")
|
||||
assert.Contains(t, strBody, "[SERVER_SOFTWARE] => FrankenPHP")
|
||||
assert.Contains(t, strBody, "[REQUEST_TIME_FLOAT]")
|
||||
assert.Contains(t, strBody, "[REQUEST_TIME]")
|
||||
assert.Contains(t, strBody, "[SERVER_PORT] => 80")
|
||||
assert.Contains(t, body, "[REMOTE_HOST]")
|
||||
assert.Contains(t, body, "[REMOTE_USER] => kevin")
|
||||
assert.Contains(t, body, "[PHP_AUTH_USER] => kevin")
|
||||
assert.Contains(t, body, "[PHP_AUTH_PW] => password")
|
||||
assert.Contains(t, body, "[HTTP_AUTHORIZATION] => Basic a2V2aW46cGFzc3dvcmQ=")
|
||||
assert.Contains(t, body, "[DOCUMENT_ROOT]")
|
||||
assert.Contains(t, body, "[PHP_SELF] => /server-variable.php/baz/bat")
|
||||
assert.Contains(t, body, "[CONTENT_TYPE] => text/plain")
|
||||
assert.Contains(t, body, fmt.Sprintf("[QUERY_STRING] => foo=a&bar=b&i=%d#hash", i))
|
||||
assert.Contains(t, body, fmt.Sprintf("[REQUEST_URI] => /server-variable.php/baz/bat?foo=a&bar=b&i=%d#hash", i))
|
||||
assert.Contains(t, body, "[CONTENT_LENGTH]")
|
||||
assert.Contains(t, body, "[REMOTE_ADDR]")
|
||||
assert.Contains(t, body, "[REMOTE_PORT]")
|
||||
assert.Contains(t, body, "[REQUEST_SCHEME] => http")
|
||||
assert.Contains(t, body, "[DOCUMENT_URI]")
|
||||
assert.Contains(t, body, "[AUTH_TYPE]")
|
||||
assert.Contains(t, body, "[REMOTE_IDENT]")
|
||||
assert.Contains(t, body, "[REQUEST_METHOD] => POST")
|
||||
assert.Contains(t, body, "[SERVER_NAME] => example.com")
|
||||
assert.Contains(t, body, "[SERVER_PROTOCOL] => HTTP/1.1")
|
||||
assert.Contains(t, body, "[SCRIPT_FILENAME]")
|
||||
assert.Contains(t, body, "[SERVER_SOFTWARE] => FrankenPHP")
|
||||
assert.Contains(t, body, "[REQUEST_TIME_FLOAT]")
|
||||
assert.Contains(t, body, "[REQUEST_TIME]")
|
||||
assert.Contains(t, body, "[SERVER_PORT] => 80")
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -206,19 +220,12 @@ func testPathInfo(t *testing.T, opts *testOptions) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/pathinfo/%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
body, _ := testGet(fmt.Sprintf("http://example.com/pathinfo/%d", i), handler, t)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
strBody := string(body)
|
||||
|
||||
assert.Contains(t, strBody, "[PATH_INFO] => /pathinfo")
|
||||
assert.Contains(t, strBody, fmt.Sprintf("[REQUEST_URI] => /pathinfo/%d", i))
|
||||
assert.Contains(t, strBody, "[PATH_TRANSLATED] =>")
|
||||
assert.Contains(t, strBody, "[SCRIPT_NAME] => /server-variable.php")
|
||||
assert.Contains(t, body, "[PATH_INFO] => /pathinfo")
|
||||
assert.Contains(t, body, fmt.Sprintf("[REQUEST_URI] => /pathinfo/%d", i))
|
||||
assert.Contains(t, body, "[PATH_TRANSLATED] =>")
|
||||
assert.Contains(t, body, "[SCRIPT_NAME] => /server-variable.php")
|
||||
|
||||
}, opts)
|
||||
}
|
||||
@@ -227,17 +234,13 @@ func TestHeaders_module(t *testing.T) { testHeaders(t, nil) }
|
||||
func TestHeaders_worker(t *testing.T) { testHeaders(t, &testOptions{workerScript: "headers.php"}) }
|
||||
func testHeaders(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/headers.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
body, resp := testGet(fmt.Sprintf("http://example.com/headers.php?i=%d", i), handler, t)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, "Hello", string(body))
|
||||
assert.Equal(t, "Hello", body)
|
||||
assert.Equal(t, 201, resp.StatusCode)
|
||||
assert.Equal(t, "bar", resp.Header.Get("Foo"))
|
||||
assert.Equal(t, "bar2", resp.Header.Get("Foo2"))
|
||||
assert.Equal(t, "bar3", resp.Header.Get("Foo3"), "header without whitespace after colon")
|
||||
assert.Empty(t, resp.Header.Get("Invalid"))
|
||||
assert.Equal(t, fmt.Sprintf("%d", i), resp.Header.Get("I"))
|
||||
}, opts)
|
||||
@@ -249,12 +252,7 @@ func TestResponseHeaders_worker(t *testing.T) {
|
||||
}
|
||||
func testResponseHeaders(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/response-headers.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
body, resp := testGet(fmt.Sprintf("http://example.com/response-headers.php?i=%d", i), handler, t)
|
||||
|
||||
if i%3 != 0 {
|
||||
assert.Equal(t, i+100, resp.StatusCode)
|
||||
@@ -262,11 +260,11 @@ func testResponseHeaders(t *testing.T, opts *testOptions) {
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
|
||||
assert.Contains(t, string(body), "'X-Powered-By' => 'PH")
|
||||
assert.Contains(t, string(body), "'Foo' => 'bar',")
|
||||
assert.Contains(t, string(body), "'Foo2' => 'bar2',")
|
||||
assert.Contains(t, string(body), fmt.Sprintf("'I' => '%d',", i))
|
||||
assert.NotContains(t, string(body), "Invalid")
|
||||
assert.Contains(t, body, "'X-Powered-By' => 'PH")
|
||||
assert.Contains(t, body, "'Foo' => 'bar',")
|
||||
assert.Contains(t, body, "'Foo2' => 'bar2',")
|
||||
assert.Contains(t, body, fmt.Sprintf("'I' => '%d',", i))
|
||||
assert.NotContains(t, body, "Invalid")
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -274,14 +272,9 @@ func TestInput_module(t *testing.T) { testInput(t, nil) }
|
||||
func TestInput_worker(t *testing.T) { testInput(t, &testOptions{workerScript: "input.php"}) }
|
||||
func testInput(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("POST", "http://example.com/input.php", strings.NewReader(fmt.Sprintf("post data %d", i)))
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
body, resp := testPost("http://example.com/input.php", fmt.Sprintf("post data %d", i), handler, t)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, fmt.Sprintf("post data %d", i), string(body))
|
||||
assert.Equal(t, fmt.Sprintf("post data %d", i), body)
|
||||
assert.Equal(t, "bar", resp.Header.Get("Foo"))
|
||||
}, opts)
|
||||
}
|
||||
@@ -295,16 +288,12 @@ func testPostSuperGlobals(t *testing.T, opts *testOptions) {
|
||||
formData := url.Values{"baz": {"bat"}, "i": {fmt.Sprintf("%d", i)}}
|
||||
req := httptest.NewRequest("POST", fmt.Sprintf("http://example.com/super-globals.php?foo=bar&iG=%d", i), strings.NewReader(formData.Encode()))
|
||||
req.Header.Set("Content-Type", strings.Clone("application/x-www-form-urlencoded"))
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
body, _ := testRequest(req, handler, t)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), "'foo' => 'bar'")
|
||||
assert.Contains(t, string(body), fmt.Sprintf("'i' => '%d'", i))
|
||||
assert.Contains(t, string(body), "'baz' => 'bat'")
|
||||
assert.Contains(t, string(body), fmt.Sprintf("'iG' => '%d'", i))
|
||||
assert.Contains(t, body, "'foo' => 'bar'")
|
||||
assert.Contains(t, body, fmt.Sprintf("'i' => '%d'", i))
|
||||
assert.Contains(t, body, "'baz' => 'bat'")
|
||||
assert.Contains(t, body, fmt.Sprintf("'iG' => '%d'", i))
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -315,14 +304,10 @@ func testCookies(t *testing.T, opts *testOptions) {
|
||||
req := httptest.NewRequest("GET", "http://example.com/cookies.php", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
|
||||
req.AddCookie(&http.Cookie{Name: "i", Value: fmt.Sprintf("%d", i)})
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
body, _ := testRequest(req, handler, t)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), "'foo' => 'bar'")
|
||||
assert.Contains(t, string(body), fmt.Sprintf("'i' => '%d'", i))
|
||||
assert.Contains(t, body, "'foo' => 'bar'")
|
||||
assert.Contains(t, body, fmt.Sprintf("'i' => '%d'", i))
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -332,21 +317,17 @@ func TestMalformedCookie(t *testing.T) {
|
||||
req.Header.Add("Cookie", "foo =bar; ===;;==; .dot.=val ;\x00 ; PHPSESSID=1234")
|
||||
// Multiple Cookie header should be joined https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2.5
|
||||
req.Header.Add("Cookie", "secondCookie=test; secondCookie=overwritten")
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
body, _ := testRequest(req, handler, t)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), "'foo_' => 'bar'")
|
||||
assert.Contains(t, string(body), "'_dot_' => 'val '")
|
||||
assert.Contains(t, body, "'foo_' => 'bar'")
|
||||
assert.Contains(t, body, "'_dot_' => 'val '")
|
||||
|
||||
// PHPSESSID should still be present since we remove the null byte
|
||||
assert.Contains(t, string(body), "'PHPSESSID' => '1234'")
|
||||
assert.Contains(t, body, "'PHPSESSID' => '1234'")
|
||||
|
||||
// The cookie in the second headers should be present,
|
||||
// but it should not be overwritten by following values
|
||||
assert.Contains(t, string(body), "'secondCookie' => 'test'")
|
||||
assert.Contains(t, body, "'secondCookie' => 'test'")
|
||||
|
||||
}, &testOptions{nbParallelRequests: 1})
|
||||
}
|
||||
@@ -386,19 +367,14 @@ func TestPhpInfo_worker(t *testing.T) { testPhpInfo(t, &testOptions{workerScript
|
||||
func testPhpInfo(t *testing.T, opts *testOptions) {
|
||||
var logOnce sync.Once
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/phpinfo.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
body, _ := testGet(fmt.Sprintf("http://example.com/phpinfo.php?i=%d", i), handler, t)
|
||||
|
||||
logOnce.Do(func() {
|
||||
t.Log(string(body))
|
||||
t.Log(body)
|
||||
})
|
||||
|
||||
assert.Contains(t, string(body), "frankenphp")
|
||||
assert.Contains(t, string(body), fmt.Sprintf("i=%d", i))
|
||||
assert.Contains(t, body, "frankenphp")
|
||||
assert.Contains(t, body, fmt.Sprintf("i=%d", i))
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -408,17 +384,12 @@ func TestPersistentObject_worker(t *testing.T) {
|
||||
}
|
||||
func testPersistentObject(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/persistent-object.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
body, _ := testGet(fmt.Sprintf("http://example.com/persistent-object.php?i=%d", i), handler, t)
|
||||
|
||||
assert.Equal(t, fmt.Sprintf(`request: %d
|
||||
class exists: 1
|
||||
id: obj1
|
||||
object id: 1`, i), string(body))
|
||||
object id: 1`, i), body)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -428,15 +399,10 @@ func TestAutoloader_worker(t *testing.T) {
|
||||
}
|
||||
func testAutoloader(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/autoloader.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
body, _ := testGet(fmt.Sprintf("http://example.com/autoloader.php?i=%d", i), handler, t)
|
||||
|
||||
assert.Equal(t, fmt.Sprintf(`request %d
|
||||
my_autoloader`, i), string(body))
|
||||
my_autoloader`, i), body)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -493,15 +459,10 @@ func TestException_worker(t *testing.T) {
|
||||
}
|
||||
func testException(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/exception.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
body, _ := testGet(fmt.Sprintf("http://example.com/exception.php?i=%d", i), handler, t)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), "hello")
|
||||
assert.Contains(t, string(body), fmt.Sprintf(`Uncaught Exception: request %d`, i))
|
||||
assert.Contains(t, body, "hello")
|
||||
assert.Contains(t, body, fmt.Sprintf(`Uncaught Exception: request %d`, i))
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -580,18 +541,14 @@ func TestLargeRequest_worker(t *testing.T) {
|
||||
}
|
||||
func testLargeRequest(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest(
|
||||
"POST",
|
||||
body, _ := testPost(
|
||||
fmt.Sprintf("http://example.com/large-request.php?i=%d", i),
|
||||
strings.NewReader(strings.Repeat("f", 6_048_576)),
|
||||
strings.Repeat("f", 6_048_576),
|
||||
handler,
|
||||
t,
|
||||
)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), fmt.Sprintf("Request body size: 6048576 (%d)", i))
|
||||
assert.Contains(t, body, fmt.Sprintf("Request body size: 6048576 (%d)", i))
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -611,14 +568,8 @@ func TestFiberNonCgo_worker(t *testing.T) {
|
||||
}
|
||||
func testFiberNoCgo(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/fiber-no-cgo.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, string(body), fmt.Sprintf("Fiber %d", i))
|
||||
body, _ := testGet(fmt.Sprintf("http://example.com/fiber-no-cgo.php?i=%d", i), handler, t)
|
||||
assert.Equal(t, body, fmt.Sprintf("Fiber %d", i))
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -628,14 +579,8 @@ func TestFiberBasic_worker(t *testing.T) {
|
||||
}
|
||||
func testFiberBasic(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/fiber-basic.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, string(body), fmt.Sprintf("Fiber %d", i))
|
||||
body, _ := testGet(fmt.Sprintf("http://example.com/fiber-basic.php?i=%d", i), handler, t)
|
||||
assert.Equal(t, body, fmt.Sprintf("Fiber %d", i))
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -648,27 +593,17 @@ func testRequestHeaders(t *testing.T, opts *testOptions) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/request-headers.php?i=%d", i), nil)
|
||||
req.Header.Add(strings.Clone("Content-Type"), strings.Clone("text/plain"))
|
||||
req.Header.Add(strings.Clone("Frankenphp-I"), strings.Clone(strconv.Itoa(i)))
|
||||
body, _ := testRequest(req, handler, t)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Contains(t, string(body), "[Content-Type] => text/plain")
|
||||
assert.Contains(t, string(body), fmt.Sprintf("[Frankenphp-I] => %d", i))
|
||||
assert.Contains(t, body, "[Content-Type] => text/plain")
|
||||
assert.Contains(t, body, fmt.Sprintf("[Frankenphp-I] => %d", i))
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func TestFailingWorker(t *testing.T) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", "http://example.com/failing-worker.php", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
assert.Contains(t, string(body), "ok")
|
||||
body, _ := testGet("http://example.com/failing-worker.php", handler, t)
|
||||
assert.Contains(t, body, "ok")
|
||||
}, &testOptions{workerScript: "failing-worker.php"})
|
||||
}
|
||||
|
||||
@@ -684,12 +619,7 @@ func testEnv(t *testing.T, opts *testOptions) {
|
||||
assert.NoError(t, os.Setenv("EMPTY", ""))
|
||||
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/env/test-env.php?var=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
body, _ := testGet(fmt.Sprintf("http://example.com/env/test-env.php?var=%d", i), handler, t)
|
||||
|
||||
// execute the script as regular php script
|
||||
cmd := exec.Command("php", "testdata/env/test-env.php", strconv.Itoa(i))
|
||||
@@ -699,18 +629,18 @@ func testEnv(t *testing.T, opts *testOptions) {
|
||||
stdoutStderr = []byte("Set MY_VAR successfully.\nMY_VAR = HelloWorld\nUnset MY_VAR successfully.\nMY_VAR is unset.\nMY_VAR set to empty successfully.\nMY_VAR = \nUnset NON_EXISTING_VAR successfully.\n")
|
||||
}
|
||||
|
||||
assert.Equal(t, string(stdoutStderr), string(body))
|
||||
assert.Equal(t, string(stdoutStderr), body)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func TestEnvIsResetInNonWorkerMode(t *testing.T) {
|
||||
assert.NoError(t, os.Setenv("test", ""))
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
putResult := fetchBody("GET", fmt.Sprintf("http://example.com/env/putenv.php?key=test&put=%d", i), handler)
|
||||
putResult, _ := testGet(fmt.Sprintf("http://example.com/env/putenv.php?key=test&put=%d", i), handler, t)
|
||||
|
||||
assert.Equal(t, fmt.Sprintf("test=%d", i), putResult, "putenv and then echo getenv")
|
||||
|
||||
getResult := fetchBody("GET", "http://example.com/env/putenv.php?key=test", handler)
|
||||
getResult, _ := testGet("http://example.com/env/putenv.php?key=test", handler, t)
|
||||
|
||||
assert.Equal(t, "test=", getResult, "putenv should be reset across requests")
|
||||
}, &testOptions{})
|
||||
@@ -720,21 +650,21 @@ func TestEnvIsResetInNonWorkerMode(t *testing.T) {
|
||||
func TestEnvIsNotResetInWorkerMode(t *testing.T) {
|
||||
assert.NoError(t, os.Setenv("index", ""))
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
putResult := fetchBody("GET", fmt.Sprintf("http://example.com/env/remember-env.php?index=%d", i), handler)
|
||||
putResult, _ := testGet(fmt.Sprintf("http://example.com/env/remember-env.php?index=%d", i), handler, t)
|
||||
|
||||
assert.Equal(t, "success", putResult, "putenv and then echo getenv")
|
||||
|
||||
getResult := fetchBody("GET", "http://example.com/env/remember-env.php", handler)
|
||||
getResult, _ := testGet("http://example.com/env/remember-env.php", handler, t)
|
||||
|
||||
assert.Equal(t, "success", getResult, "putenv should not be reset across worker requests")
|
||||
}, &testOptions{workerScript: "env/remember-env.php"})
|
||||
}
|
||||
|
||||
// reproduction of https://github.com/dunglas/frankenphp/issues/1061
|
||||
// reproduction of https://github.com/php/frankenphp/issues/1061
|
||||
func TestModificationsToEnvPersistAcrossRequests(t *testing.T) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
for j := 0; j < 3; j++ {
|
||||
result := fetchBody("GET", "http://example.com/env/overwrite-env.php", handler)
|
||||
result, _ := testGet("http://example.com/env/overwrite-env.php", handler, t)
|
||||
assert.Equal(t, "custom_value", result, "a var directly added to $_ENV should persist")
|
||||
}
|
||||
}, &testOptions{
|
||||
@@ -760,11 +690,7 @@ func testFileUpload(t *testing.T, opts *testOptions) {
|
||||
req := httptest.NewRequest("POST", "http://example.com/file-upload.php", requestBody)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
body, _ := testRequest(req, handler, t)
|
||||
|
||||
assert.Contains(t, string(body), "Upload OK")
|
||||
}, opts)
|
||||
@@ -1010,15 +936,10 @@ func testRejectInvalidHeaders(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
|
||||
req := httptest.NewRequest("GET", "http://example.com/headers.php", nil)
|
||||
req.Header.Add(header[0], header[1])
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
body, resp := testRequest(req, handler, t)
|
||||
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
assert.Contains(t, string(body), "invalid")
|
||||
assert.Contains(t, body, "invalid")
|
||||
}, opts)
|
||||
}
|
||||
}
|
||||
@@ -1030,11 +951,7 @@ func TestFlushEmptyRespnse_worker(t *testing.T) {
|
||||
|
||||
func testFlushEmptyResponse(t *testing.T, opts *testOptions) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
|
||||
req := httptest.NewRequest("GET", "http://example.com/only-headers.php", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
_, resp := testGet("http://example.com/only-headers.php", handler, t)
|
||||
assert.Equal(t, 204, resp.StatusCode)
|
||||
}, opts)
|
||||
}
|
||||
@@ -1043,13 +960,13 @@ func testFlushEmptyResponse(t *testing.T, opts *testOptions) {
|
||||
// Make sure referenced streams are not cleaned up
|
||||
func TestFileStreamInWorkerMode(t *testing.T) {
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
|
||||
resp1 := fetchBody("GET", "http://example.com/file-stream.php", handler)
|
||||
resp1, _ := testGet("http://example.com/file-stream.php", handler, t)
|
||||
assert.Equal(t, resp1, "word1")
|
||||
|
||||
resp2 := fetchBody("GET", "http://example.com/file-stream.php", handler)
|
||||
resp2, _ := testGet("http://example.com/file-stream.php", handler, t)
|
||||
assert.Equal(t, resp2, "word2")
|
||||
|
||||
resp3 := fetchBody("GET", "http://example.com/file-stream.php", handler)
|
||||
resp3, _ := testGet("http://example.com/file-stream.php", handler, t)
|
||||
assert.Equal(t, resp3, "word3")
|
||||
}, &testOptions{workerScript: "file-stream.php", nbParallelRequests: 1, nbWorkers: 1})
|
||||
}
|
||||
@@ -1069,38 +986,23 @@ func FuzzRequest(f *testing.F) {
|
||||
req.URL = &url.URL{RawQuery: "test=" + fuzzedString, Path: "/server-variable.php/" + fuzzedString}
|
||||
req.Header.Add(strings.Clone("Fuzzed"), strings.Clone(fuzzedString))
|
||||
req.Header.Add(strings.Clone("Content-Type"), fuzzedString)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
body, resp := testRequest(req, handler, t)
|
||||
|
||||
// The response status must be 400 if the request path contains null bytes
|
||||
if strings.Contains(req.URL.Path, "\x00") {
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
assert.Contains(t, string(body), "Invalid request path")
|
||||
assert.Contains(t, body, "Invalid request path")
|
||||
return
|
||||
}
|
||||
|
||||
// The fuzzed string must be present in the path
|
||||
assert.Contains(t, string(body), fmt.Sprintf("[PATH_INFO] => /%s", fuzzedString))
|
||||
assert.Contains(t, string(body), fmt.Sprintf("[PATH_TRANSLATED] => %s", filepath.Join(absPath, fuzzedString)))
|
||||
assert.Contains(t, body, fmt.Sprintf("[PATH_INFO] => /%s", fuzzedString))
|
||||
assert.Contains(t, body, fmt.Sprintf("[PATH_TRANSLATED] => %s", filepath.Join(absPath, fuzzedString)))
|
||||
|
||||
// Headers should always be present even if empty
|
||||
assert.Contains(t, string(body), fmt.Sprintf("[CONTENT_TYPE] => %s", fuzzedString))
|
||||
assert.Contains(t, string(body), fmt.Sprintf("[HTTP_FUZZED] => %s", fuzzedString))
|
||||
assert.Contains(t, body, fmt.Sprintf("[CONTENT_TYPE] => %s", fuzzedString))
|
||||
assert.Contains(t, body, fmt.Sprintf("[HTTP_FUZZED] => %s", fuzzedString))
|
||||
|
||||
}, &testOptions{workerScript: "request-headers.php"})
|
||||
})
|
||||
}
|
||||
|
||||
func fetchBody(method string, url string, handler func(http.ResponseWriter, *http.Request)) string {
|
||||
req := httptest.NewRequest(method, url, nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
return string(body)
|
||||
}
|
||||
|
||||
23
go.mod
23
go.mod
@@ -5,30 +5,41 @@ go 1.24.0
|
||||
retract v1.0.0-rc.1 // Human error
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/maypok86/otter v1.2.4
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.uber.org/zap v1.27.0
|
||||
go.uber.org/zap/exp v0.3.0
|
||||
golang.org/x/net v0.40.0
|
||||
golang.org/x/net v0.42.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dolthub/maphash v0.1.0 // indirect
|
||||
github.com/gammazero/deque v1.0.0 // indirect
|
||||
github.com/gammazero/deque v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.64.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.9.2 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
7
go.sh
Executable file
7
go.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
# Runs the go command with the proper Go and cgo flags.
|
||||
|
||||
GOFLAGS="$GOFLAGS -tags=nobadger,nomysql,nopgx" \
|
||||
CGO_CFLAGS="$CGO_CFLAGS $(php-config --includes)" \
|
||||
CGO_LDFLAGS="$CGO_LDFLAGS $(php-config --ldflags) $(php-config --libs)" \
|
||||
go "$@"
|
||||
48
go.sum
48
go.sum
@@ -1,3 +1,11 @@
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
@@ -6,10 +14,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
|
||||
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
|
||||
github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34=
|
||||
github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/gammazero/deque v1.1.0 h1:OyiyReBbnEG2PP0Bnv1AASLIYvyKqIFN5xfl1t8oGLo=
|
||||
github.com/gammazero/deque v1.1.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -18,6 +32,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
|
||||
github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -26,12 +44,16 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
|
||||
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -42,12 +64,14 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -76,9 +76,9 @@ if [ $? -eq 1 ]; then
|
||||
fi
|
||||
|
||||
if type "curl" >/dev/null 2>&1; then
|
||||
curl -L --progress-bar "https://github.com/dunglas/frankenphp/releases/latest/download/${THE_ARCH_BIN}" -o "${DEST}"
|
||||
curl -L --progress-bar "https://github.com/php/frankenphp/releases/latest/download/${THE_ARCH_BIN}" -o "${DEST}"
|
||||
elif type "wget" >/dev/null 2>&1; then
|
||||
${SUDO} wget "https://github.com/dunglas/frankenphp/releases/latest/download/${THE_ARCH_BIN}" -O "${DEST}"
|
||||
${SUDO} wget "https://github.com/php/frankenphp/releases/latest/download/${THE_ARCH_BIN}" -O "${DEST}"
|
||||
else
|
||||
echo "❗ Please install ${italic}curl${normal} or ${italic}wget${normal} to download FrankenPHP"
|
||||
exit 1
|
||||
@@ -91,4 +91,4 @@ echo "🥳 FrankenPHP downloaded successfully to ${italic}${DEST}${normal}"
|
||||
echo "🔧 Move the binary to ${italic}/usr/local/bin/${normal} or another directory in your ${italic}PATH${normal} to use it globally:"
|
||||
echo " ${bold}sudo mv ${DEST} /usr/local/bin/${normal}"
|
||||
echo
|
||||
echo "⭐ If you like FrankenPHP, please give it a star on GitHub: ${italic}https://github.com/dunglas/frankenphp${normal}"
|
||||
echo "⭐ If you like FrankenPHP, please give it a star on GitHub: ${italic}https://github.com/php/frankenphp${normal}"
|
||||
|
||||
50
internal/extgen/arginfo.go
Normal file
50
internal/extgen/arginfo.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type arginfoGenerator struct {
|
||||
generator *Generator
|
||||
}
|
||||
|
||||
func (ag *arginfoGenerator) generate() error {
|
||||
genStubPath := os.Getenv("GEN_STUB_SCRIPT")
|
||||
if genStubPath == "" {
|
||||
genStubPath = "/usr/local/src/php/build/gen_stub.php"
|
||||
}
|
||||
|
||||
if _, err := os.Stat(genStubPath); err != nil {
|
||||
return fmt.Errorf(`the PHP "gen_stub.php" file couldn't be found under %q, you can set the "GEN_STUB_SCRIPT" environement variable to set a custom location`, genStubPath)
|
||||
}
|
||||
|
||||
stubFile := ag.generator.BaseName + ".stub.php"
|
||||
cmd := exec.Command("php", genStubPath, filepath.Join(ag.generator.BuildDir, stubFile))
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("running gen_stub script: %w", err)
|
||||
}
|
||||
|
||||
return ag.fixArginfoFile(stubFile)
|
||||
}
|
||||
|
||||
func (ag *arginfoGenerator) fixArginfoFile(stubFile string) error {
|
||||
arginfoFile := strings.TrimSuffix(stubFile, ".stub.php") + "_arginfo.h"
|
||||
arginfoPath := filepath.Join(ag.generator.BuildDir, arginfoFile)
|
||||
|
||||
content, err := readFile(arginfoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading arginfo file: %w", err)
|
||||
}
|
||||
|
||||
// FIXME: the script generate "zend_register_internal_class_with_flags" but it is not recognized by the compiler
|
||||
fixedContent := strings.ReplaceAll(content,
|
||||
"zend_register_internal_class_with_flags(&ce, NULL, 0)",
|
||||
"zend_register_internal_class(&ce)")
|
||||
|
||||
return writeFile(arginfoPath, fixedContent)
|
||||
}
|
||||
78
internal/extgen/cfile.go
Normal file
78
internal/extgen/cfile.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//go:embed templates/extension.c.tpl
|
||||
var cFileContent string
|
||||
|
||||
type cFileGenerator struct {
|
||||
generator *Generator
|
||||
}
|
||||
|
||||
type cTemplateData struct {
|
||||
BaseName string
|
||||
Functions []phpFunction
|
||||
Classes []phpClass
|
||||
Constants []phpConstant
|
||||
Namespace string
|
||||
Module *phpModule
|
||||
}
|
||||
|
||||
func (cg *cFileGenerator) generate() error {
|
||||
filename := filepath.Join(cg.generator.BuildDir, cg.generator.BaseName+".c")
|
||||
content, err := cg.buildContent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeFile(filename, content)
|
||||
}
|
||||
|
||||
func (cg *cFileGenerator) buildContent() (string, error) {
|
||||
var builder strings.Builder
|
||||
|
||||
templateContent, err := cg.getTemplateContent()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
builder.WriteString(templateContent)
|
||||
|
||||
for _, fn := range cg.generator.Functions {
|
||||
fnGen := PHPFuncGenerator{
|
||||
paramParser: &ParameterParser{},
|
||||
namespace: cg.generator.Namespace,
|
||||
}
|
||||
builder.WriteString(fnGen.generate(fn))
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func (cg *cFileGenerator) getTemplateContent() (string, error) {
|
||||
funcMap := sprig.FuncMap()
|
||||
funcMap["namespacedClassName"] = NamespacedName
|
||||
|
||||
tmpl := template.Must(template.New("cfile").Funcs(funcMap).Parse(cFileContent))
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, cTemplateData{
|
||||
BaseName: cg.generator.BaseName,
|
||||
Functions: cg.generator.Functions,
|
||||
Classes: cg.generator.Classes,
|
||||
Constants: cg.generator.Constants,
|
||||
Namespace: cg.generator.Namespace,
|
||||
Module: cg.generator.Module,
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
130
internal/extgen/cfile_namespace_test.go
Normal file
130
internal/extgen/cfile_namespace_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNamespacedClassName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
className string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no namespace",
|
||||
namespace: "",
|
||||
className: "MySuperClass",
|
||||
expected: "MySuperClass",
|
||||
},
|
||||
{
|
||||
name: "single level namespace",
|
||||
namespace: "MyNamespace",
|
||||
className: "MySuperClass",
|
||||
expected: "MyNamespace_MySuperClass",
|
||||
},
|
||||
{
|
||||
name: "multi level namespace",
|
||||
namespace: `Go\Extension`,
|
||||
className: "MySuperClass",
|
||||
expected: "Go_Extension_MySuperClass",
|
||||
},
|
||||
{
|
||||
name: "deep namespace",
|
||||
namespace: `My\Deep\Nested\Namespace`,
|
||||
className: "TestClass",
|
||||
expected: "My_Deep_Nested_Namespace_TestClass",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NamespacedName(tt.namespace, tt.className)
|
||||
require.Equal(t, tt.expected, result, "expected %q, got %q", tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCFileGenerationWithNamespace(t *testing.T) {
|
||||
content := `package main
|
||||
|
||||
//export_php:namespace Go\Extension
|
||||
|
||||
//export_php:class MySuperClass
|
||||
type MySuperClass struct{}
|
||||
|
||||
//export_php:method MySuperClass test(): string
|
||||
func (m *MySuperClass) Test() string {
|
||||
return "test"
|
||||
}
|
||||
`
|
||||
|
||||
tmpfile, err := os.CreateTemp("", "test_cfile_namespace_*.go")
|
||||
require.NoError(t, err, "Failed to create temp file")
|
||||
defer func() {
|
||||
err := os.Remove(tmpfile.Name())
|
||||
assert.NoError(t, err, "Failed to remove temp file: %v", err)
|
||||
}()
|
||||
|
||||
_, err = tmpfile.Write([]byte(content))
|
||||
require.NoError(t, err, "Failed to write to temp file")
|
||||
|
||||
err = tmpfile.Close()
|
||||
require.NoError(t, err, "Failed to close temp file")
|
||||
|
||||
generator := &Generator{
|
||||
BaseName: "test_extension",
|
||||
SourceFile: tmpfile.Name(),
|
||||
BuildDir: t.TempDir(),
|
||||
Namespace: `Go\Extension`,
|
||||
Classes: []phpClass{
|
||||
{
|
||||
Name: "MySuperClass",
|
||||
GoStruct: "MySuperClass",
|
||||
Methods: []phpClassMethod{
|
||||
{
|
||||
Name: "test",
|
||||
PhpName: "test",
|
||||
Signature: "test(): string",
|
||||
ReturnType: "string",
|
||||
ClassName: "MySuperClass",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cFileGen := cFileGenerator{generator: generator}
|
||||
contentResult, err := cFileGen.getTemplateContent()
|
||||
require.NoError(t, err, "error generating C file")
|
||||
|
||||
expectedCall := "register_class_Go_Extension_MySuperClass()"
|
||||
require.Contains(t, contentResult, expectedCall, "C file should contain the standard function call")
|
||||
|
||||
oldCall := "register_class_MySuperClass()"
|
||||
require.NotContains(t, contentResult, oldCall, "C file should not contain old non-namespaced call")
|
||||
}
|
||||
|
||||
func TestCFileGenerationWithoutNamespace(t *testing.T) {
|
||||
generator := &Generator{
|
||||
BaseName: "test_extension",
|
||||
BuildDir: t.TempDir(),
|
||||
Namespace: "",
|
||||
Classes: []phpClass{
|
||||
{
|
||||
Name: "MySuperClass",
|
||||
GoStruct: "MySuperClass",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cFileGen := cFileGenerator{generator: generator}
|
||||
contentResult, err := cFileGen.getTemplateContent()
|
||||
require.NoError(t, err, "error generating C file")
|
||||
|
||||
expectedCall := "register_class_MySuperClass()"
|
||||
require.Contains(t, contentResult, expectedCall, "C file should not contain the standard function call")
|
||||
}
|
||||
186
internal/extgen/cfile_phpmethod_test.go
Normal file
186
internal/extgen/cfile_phpmethod_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCFile_NamespacedPHPMethods(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
classes []phpClass
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "no namespace - regular PHP_METHOD",
|
||||
namespace: "",
|
||||
classes: []phpClass{
|
||||
{
|
||||
Name: "TestClass",
|
||||
GoStruct: "TestClass",
|
||||
Methods: []phpClassMethod{
|
||||
{Name: "testMethod", PhpName: "testMethod", ClassName: "TestClass"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"PHP_METHOD(TestClass, __construct)",
|
||||
"PHP_METHOD(TestClass, testMethod)",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single level namespace",
|
||||
namespace: "MyNamespace",
|
||||
classes: []phpClass{
|
||||
{
|
||||
Name: "TestClass",
|
||||
GoStruct: "TestClass",
|
||||
Methods: []phpClassMethod{
|
||||
{Name: "testMethod", PhpName: "testMethod", ClassName: "TestClass"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"PHP_METHOD(MyNamespace_TestClass, __construct)",
|
||||
"PHP_METHOD(MyNamespace_TestClass, testMethod)",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multi level namespace",
|
||||
namespace: `Go\Extension`,
|
||||
classes: []phpClass{
|
||||
{
|
||||
Name: "MySuperClass",
|
||||
GoStruct: "MySuperClass",
|
||||
Methods: []phpClassMethod{
|
||||
{Name: "getName", PhpName: "getName", ClassName: "MySuperClass"},
|
||||
{Name: "setName", PhpName: "setName", ClassName: "MySuperClass"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"PHP_METHOD(Go_Extension_MySuperClass, __construct)",
|
||||
"PHP_METHOD(Go_Extension_MySuperClass, getName)",
|
||||
"PHP_METHOD(Go_Extension_MySuperClass, setName)",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple classes with namespace",
|
||||
namespace: `Go\Extension`,
|
||||
classes: []phpClass{
|
||||
{
|
||||
Name: "ClassA",
|
||||
GoStruct: "ClassA",
|
||||
Methods: []phpClassMethod{
|
||||
{Name: "methodA", PhpName: "methodA", ClassName: "ClassA"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ClassB",
|
||||
GoStruct: "ClassB",
|
||||
Methods: []phpClassMethod{
|
||||
{Name: "methodB", PhpName: "methodB", ClassName: "ClassB"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"PHP_METHOD(Go_Extension_ClassA, __construct)",
|
||||
"PHP_METHOD(Go_Extension_ClassA, methodA)",
|
||||
"PHP_METHOD(Go_Extension_ClassB, __construct)",
|
||||
"PHP_METHOD(Go_Extension_ClassB, methodB)",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
generator := &Generator{
|
||||
BaseName: "test_extension",
|
||||
Namespace: tt.namespace,
|
||||
Classes: tt.classes,
|
||||
BuildDir: t.TempDir(),
|
||||
}
|
||||
|
||||
cFileGen := cFileGenerator{generator: generator}
|
||||
content, err := cFileGen.getTemplateContent()
|
||||
require.NoError(t, err, "error generating C template content: %v", err)
|
||||
|
||||
for _, expected := range tt.expected {
|
||||
require.Contains(t, content, expected, "Expected to find %q in C template content", expected)
|
||||
}
|
||||
|
||||
if tt.namespace != "" {
|
||||
for _, class := range tt.classes {
|
||||
oldConstructor := "PHP_METHOD(" + class.Name + ", __construct)"
|
||||
require.NotContains(t, content, oldConstructor, "Did not expect to find old constructor declaration %q in namespaced content", oldConstructor)
|
||||
|
||||
for _, method := range class.Methods {
|
||||
oldMethod := "PHP_METHOD(" + class.Name + ", " + method.PhpName + ")"
|
||||
require.NotContains(t, content, oldMethod, "Did not expect to find old method declaration %q in namespaced content", oldMethod)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCFile_PHP_METHOD_Integration(t *testing.T) {
|
||||
generator := &Generator{
|
||||
BaseName: "test_extension",
|
||||
Namespace: `Go\Extension`,
|
||||
Functions: []phpFunction{
|
||||
{Name: "testFunc", ReturnType: "void"},
|
||||
},
|
||||
Classes: []phpClass{
|
||||
{
|
||||
Name: "MySuperClass",
|
||||
GoStruct: "MySuperClass",
|
||||
Methods: []phpClassMethod{
|
||||
{
|
||||
Name: "getName",
|
||||
PhpName: "getName",
|
||||
ReturnType: "string",
|
||||
ClassName: "MySuperClass",
|
||||
},
|
||||
{
|
||||
Name: "setName",
|
||||
PhpName: "setName",
|
||||
ReturnType: "void",
|
||||
ClassName: "MySuperClass",
|
||||
Params: []phpParameter{
|
||||
{Name: "name", PhpType: "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
BuildDir: t.TempDir(),
|
||||
}
|
||||
|
||||
cFileGen := cFileGenerator{generator: generator}
|
||||
fullContent, err := cFileGen.buildContent()
|
||||
require.NoError(t, err, "error generating full C file: %v", err)
|
||||
|
||||
expectedDeclarations := []string{
|
||||
"PHP_FUNCTION(Go_Extension_testFunc)",
|
||||
"PHP_METHOD(Go_Extension_MySuperClass, __construct)",
|
||||
"PHP_METHOD(Go_Extension_MySuperClass, getName)",
|
||||
"PHP_METHOD(Go_Extension_MySuperClass, setName)",
|
||||
}
|
||||
|
||||
for _, expected := range expectedDeclarations {
|
||||
require.Contains(t, fullContent, expected, "Expected to find %q in full C file content", expected)
|
||||
}
|
||||
|
||||
oldDeclarations := []string{
|
||||
"PHP_FUNCTION(testFunc)",
|
||||
"PHP_METHOD(MySuperClass, __construct)",
|
||||
"PHP_METHOD(MySuperClass, getName)",
|
||||
"PHP_METHOD(MySuperClass, setName)",
|
||||
}
|
||||
|
||||
for _, old := range oldDeclarations {
|
||||
require.NotContains(t, fullContent, old, "Did not expect to find old declaration %q in full C file content", old)
|
||||
}
|
||||
}
|
||||
461
internal/extgen/cfile_test.go
Normal file
461
internal/extgen/cfile_test.go
Normal file
@@ -0,0 +1,461 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCFileGenerator_Generate(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
generator := &Generator{
|
||||
BaseName: "test_extension",
|
||||
BuildDir: tmpDir,
|
||||
Functions: []phpFunction{
|
||||
{
|
||||
Name: "simpleFunction",
|
||||
ReturnType: phpString,
|
||||
Params: []phpParameter{
|
||||
{Name: "input", PhpType: phpString},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "complexFunction",
|
||||
ReturnType: phpArray,
|
||||
Params: []phpParameter{
|
||||
{Name: "data", PhpType: phpString},
|
||||
{Name: "count", PhpType: phpInt, IsNullable: true},
|
||||
{Name: "options", PhpType: phpArray, HasDefault: true, DefaultValue: "[]"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Classes: []phpClass{
|
||||
{
|
||||
Name: "TestClass",
|
||||
GoStruct: "TestStruct",
|
||||
Properties: []phpClassProperty{
|
||||
{Name: "id", PhpType: phpInt},
|
||||
{Name: "name", PhpType: phpString},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cGen := cFileGenerator{generator}
|
||||
require.NoError(t, cGen.generate())
|
||||
|
||||
expectedFile := filepath.Join(tmpDir, "test_extension.c")
|
||||
require.FileExists(t, expectedFile, "Expected C file was not created: %s", expectedFile)
|
||||
|
||||
content, err := readFile(expectedFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCFileBasicStructure(t, content, "test_extension")
|
||||
testCFileFunctions(t, content, generator.Functions)
|
||||
testCFileClasses(t, content, generator.Classes)
|
||||
}
|
||||
|
||||
func TestCFileGenerator_BuildContent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseName string
|
||||
functions []phpFunction
|
||||
classes []phpClass
|
||||
contains []string
|
||||
notContains []string
|
||||
}{
|
||||
{
|
||||
name: "empty extension",
|
||||
baseName: "empty",
|
||||
contains: []string{
|
||||
"#include <php.h>",
|
||||
"#include <Zend/zend_API.h>",
|
||||
`#include "empty.h"`,
|
||||
"PHP_MINIT_FUNCTION(empty)",
|
||||
"empty_module_entry",
|
||||
"return SUCCESS;",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extension with functions only",
|
||||
baseName: "func_only",
|
||||
functions: []phpFunction{
|
||||
{Name: "testFunc", ReturnType: phpString},
|
||||
},
|
||||
contains: []string{
|
||||
"PHP_FUNCTION(testFunc)",
|
||||
`#include "func_only.h"`,
|
||||
"func_only_module_entry",
|
||||
"PHP_MINIT_FUNCTION(func_only)",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extension with classes only",
|
||||
baseName: "class_only",
|
||||
classes: []phpClass{
|
||||
{Name: "MyClass", GoStruct: "MyStruct"},
|
||||
},
|
||||
contains: []string{
|
||||
"register_all_classes()",
|
||||
"register_class_MyClass();",
|
||||
"PHP_METHOD(MyClass, __construct)",
|
||||
`#include "class_only.h"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extension with functions and classes",
|
||||
baseName: "full",
|
||||
functions: []phpFunction{
|
||||
{Name: "doSomething", ReturnType: phpVoid},
|
||||
},
|
||||
classes: []phpClass{
|
||||
{Name: "FullClass", GoStruct: "FullStruct"},
|
||||
},
|
||||
contains: []string{
|
||||
"PHP_FUNCTION(doSomething)",
|
||||
"PHP_METHOD(FullClass, __construct)",
|
||||
"register_all_classes()",
|
||||
"register_class_FullClass();",
|
||||
`#include "full.h"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
generator := &Generator{
|
||||
BaseName: tt.baseName,
|
||||
Functions: tt.functions,
|
||||
Classes: tt.classes,
|
||||
}
|
||||
|
||||
cGen := cFileGenerator{generator}
|
||||
content, err := cGen.buildContent()
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, expected := range tt.contains {
|
||||
assert.Contains(t, content, expected, "Generated C content should contain '%s'", expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCFileGenerator_GetTemplateContent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseName string
|
||||
classes []phpClass
|
||||
contains []string
|
||||
notContains []string
|
||||
}{
|
||||
{
|
||||
name: "extension without classes",
|
||||
baseName: "myext",
|
||||
contains: []string{
|
||||
`#include "myext.h"`,
|
||||
`#include "myext_arginfo.h"`,
|
||||
"PHP_MINIT_FUNCTION(myext)",
|
||||
"myext_module_entry",
|
||||
"return SUCCESS;",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extension with classes",
|
||||
baseName: "complex_name",
|
||||
classes: []phpClass{
|
||||
{Name: "TestClass", GoStruct: "TestStruct"},
|
||||
{Name: "AnotherClass", GoStruct: "AnotherStruct"},
|
||||
},
|
||||
contains: []string{
|
||||
`#include "complex_name.h"`,
|
||||
`#include "complex_name_arginfo.h"`,
|
||||
"PHP_MINIT_FUNCTION(complex_name)",
|
||||
"complex_name_module_entry",
|
||||
"register_all_classes()",
|
||||
"register_class_TestClass();",
|
||||
"register_class_AnotherClass();",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
generator := &Generator{
|
||||
BaseName: tt.baseName,
|
||||
Classes: tt.classes,
|
||||
}
|
||||
cGen := cFileGenerator{generator}
|
||||
content, err := cGen.getTemplateContent()
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, expected := range tt.contains {
|
||||
assert.Contains(t, content, expected, "Template content should contain '%s'", expected)
|
||||
}
|
||||
|
||||
for _, notExpected := range tt.notContains {
|
||||
assert.NotContains(t, content, notExpected, "Template content should NOT contain '%s'", notExpected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCFileIntegrationWithGenerators(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
functions := []phpFunction{
|
||||
{
|
||||
Name: "processData",
|
||||
ReturnType: phpArray,
|
||||
IsReturnNullable: true,
|
||||
Params: []phpParameter{
|
||||
{Name: "input", PhpType: phpString},
|
||||
{Name: "options", PhpType: phpArray, HasDefault: true, DefaultValue: "[]"},
|
||||
{Name: "callback", PhpType: phpObject, IsNullable: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "validateInput",
|
||||
ReturnType: phpBool,
|
||||
Params: []phpParameter{
|
||||
{Name: "data", PhpType: phpString, IsNullable: true},
|
||||
{Name: "strict", PhpType: phpBool, HasDefault: true, DefaultValue: "false"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
classes := []phpClass{
|
||||
{
|
||||
Name: "DataProcessor",
|
||||
GoStruct: "DataProcessorStruct",
|
||||
Properties: []phpClassProperty{
|
||||
{Name: "mode", PhpType: phpString},
|
||||
{Name: "timeout", PhpType: phpInt, IsNullable: true},
|
||||
{Name: "options", PhpType: phpArray},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Result",
|
||||
GoStruct: "ResultStruct",
|
||||
Properties: []phpClassProperty{
|
||||
{Name: "success", PhpType: phpBool},
|
||||
{Name: "data", PhpType: phpMixed, IsNullable: true},
|
||||
{Name: "errors", PhpType: phpArray},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
generator := &Generator{
|
||||
BaseName: "integration_test",
|
||||
BuildDir: tmpDir,
|
||||
Functions: functions,
|
||||
Classes: classes,
|
||||
}
|
||||
|
||||
cGen := cFileGenerator{generator}
|
||||
require.NoError(t, cGen.generate())
|
||||
|
||||
content, err := readFile(filepath.Join(tmpDir, "integration_test.c"))
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, fn := range functions {
|
||||
expectedFunc := "PHP_FUNCTION(" + fn.Name + ")"
|
||||
assert.Contains(t, content, expectedFunc, "Generated C file should contain function: %s", expectedFunc)
|
||||
}
|
||||
|
||||
for _, class := range classes {
|
||||
expectedMethod := "PHP_METHOD(" + class.Name + ", __construct)"
|
||||
assert.Contains(t, content, expectedMethod, "Generated C file should contain class method: %s", expectedMethod)
|
||||
}
|
||||
|
||||
assert.Contains(t, content, "register_all_classes()", "Generated C file should contain class registration call")
|
||||
assert.Contains(t, content, "integration_test_module_entry", "Generated C file should contain integration_test_module_entry")
|
||||
}
|
||||
|
||||
func TestCFileErrorHandling(t *testing.T) {
|
||||
// Test with invalid build directory
|
||||
generator := &Generator{
|
||||
BaseName: "test",
|
||||
BuildDir: "/invalid/readonly/path",
|
||||
Functions: []phpFunction{
|
||||
{Name: "test", ReturnType: phpVoid},
|
||||
},
|
||||
}
|
||||
|
||||
cGen := cFileGenerator{generator}
|
||||
err := cGen.generate()
|
||||
assert.Error(t, err, "Expected error when writing to invalid directory")
|
||||
}
|
||||
|
||||
func TestCFileSpecialCharacters(t *testing.T) {
|
||||
tests := []struct {
|
||||
baseName string
|
||||
expected string
|
||||
}{
|
||||
{"simple", "simple"},
|
||||
{"my_extension", "my_extension"},
|
||||
{"ext-with-dashes", "ext-with-dashes"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.baseName, func(t *testing.T) {
|
||||
generator := &Generator{
|
||||
BaseName: tt.baseName,
|
||||
Functions: []phpFunction{
|
||||
{Name: "test", ReturnType: phpVoid},
|
||||
},
|
||||
}
|
||||
|
||||
cGen := cFileGenerator{generator}
|
||||
content, err := cGen.buildContent()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedInclude := "#include \"" + tt.expected + ".h\""
|
||||
assert.Contains(t, content, expectedInclude, "Content should contain include: %s", expectedInclude)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testCFileBasicStructure(t *testing.T, content, baseName string) {
|
||||
requiredElements := []string{
|
||||
"#include <php.h>",
|
||||
"#include <Zend/zend_API.h>",
|
||||
`#include "_cgo_export.h"`,
|
||||
`#include "` + baseName + `.h"`,
|
||||
`#include "` + baseName + `_arginfo.h"`,
|
||||
"PHP_MINIT_FUNCTION(" + baseName + ")",
|
||||
baseName + "_module_entry",
|
||||
}
|
||||
|
||||
for _, element := range requiredElements {
|
||||
assert.Contains(t, content, element, "C file should contain: %s", element)
|
||||
}
|
||||
}
|
||||
|
||||
func testCFileFunctions(t *testing.T, content string, functions []phpFunction) {
|
||||
for _, fn := range functions {
|
||||
phpFunc := "PHP_FUNCTION(" + fn.Name + ")"
|
||||
assert.Contains(t, content, phpFunc, "C file should contain function declaration: %s", phpFunc)
|
||||
}
|
||||
}
|
||||
|
||||
func testCFileClasses(t *testing.T, content string, classes []phpClass) {
|
||||
if len(classes) == 0 {
|
||||
// Si pas de classes, ne devrait pas contenir register_all_classes
|
||||
assert.NotContains(t, content, "register_all_classes()", "C file should NOT contain register_all_classes call when no classes")
|
||||
return
|
||||
}
|
||||
|
||||
assert.Contains(t, content, "void register_all_classes() {", "C file should contain register_all_classes function")
|
||||
assert.Contains(t, content, "register_all_classes();", "C file should contain register_all_classes call in MINIT")
|
||||
|
||||
for _, class := range classes {
|
||||
expectedCall := "register_class_" + class.Name + "();"
|
||||
assert.Contains(t, content, expectedCall, "C file should contain class registration call: %s", expectedCall)
|
||||
|
||||
constructor := "PHP_METHOD(" + class.Name + ", __construct)"
|
||||
assert.Contains(t, content, constructor, "C file should contain constructor: %s", constructor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCFileContentValidation(t *testing.T) {
|
||||
generator := &Generator{
|
||||
BaseName: "syntax_test",
|
||||
Functions: []phpFunction{
|
||||
{
|
||||
Name: "testFunction",
|
||||
ReturnType: phpString,
|
||||
Params: []phpParameter{
|
||||
{Name: "param", PhpType: phpString},
|
||||
},
|
||||
},
|
||||
},
|
||||
Classes: []phpClass{
|
||||
{Name: "TestClass", GoStruct: "TestStruct"},
|
||||
},
|
||||
}
|
||||
|
||||
cGen := cFileGenerator{generator}
|
||||
content, err := cGen.buildContent()
|
||||
require.NoError(t, err)
|
||||
|
||||
syntaxElements := []string{
|
||||
"{", "}", "(", ")", ";",
|
||||
"static", "void", "int",
|
||||
"#include",
|
||||
}
|
||||
|
||||
for _, element := range syntaxElements {
|
||||
assert.Contains(t, content, element, "Generated C content should contain basic C syntax: %s", element)
|
||||
}
|
||||
|
||||
openBraces := strings.Count(content, "{")
|
||||
closeBraces := strings.Count(content, "}")
|
||||
|
||||
assert.Equal(t, openBraces, closeBraces, "Unbalanced braces in generated C code: %d open, %d close", openBraces, closeBraces)
|
||||
assert.False(t, strings.Contains(content, ";;"), "Generated C code contains double semicolons")
|
||||
assert.False(t, strings.Contains(content, "{{") || strings.Contains(content, "}}"), "Generated C code contains unresolved template syntax")
|
||||
}
|
||||
|
||||
func TestCFileConstants(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseName string
|
||||
constants []phpConstant
|
||||
classes []phpClass
|
||||
contains []string
|
||||
}{
|
||||
{
|
||||
name: "global constants only",
|
||||
baseName: "const_test",
|
||||
constants: []phpConstant{
|
||||
{
|
||||
Name: "GLOBAL_INT",
|
||||
Value: "42",
|
||||
PhpType: phpInt,
|
||||
},
|
||||
{
|
||||
Name: "GLOBAL_STRING",
|
||||
Value: `"test"`,
|
||||
PhpType: phpString,
|
||||
},
|
||||
},
|
||||
contains: []string{
|
||||
"REGISTER_LONG_CONSTANT(\"GLOBAL_INT\", 42, CONST_CS | CONST_PERSISTENT);",
|
||||
"REGISTER_STRING_CONSTANT(\"GLOBAL_STRING\", \"test\", CONST_CS | CONST_PERSISTENT);",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
generator := &Generator{
|
||||
BaseName: tt.baseName,
|
||||
Constants: tt.constants,
|
||||
Classes: tt.classes,
|
||||
}
|
||||
|
||||
cGen := cFileGenerator{generator}
|
||||
content, err := cGen.buildContent()
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, expected := range tt.contains {
|
||||
assert.Contains(t, content, expected, "Generated C content should contain '%s'", expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCFileTemplateErrorHandling(t *testing.T) {
|
||||
generator := &Generator{
|
||||
BaseName: "error_test",
|
||||
}
|
||||
|
||||
cGen := cFileGenerator{generator}
|
||||
|
||||
_, err := cGen.getTemplateContent()
|
||||
assert.NoError(t, err, "getTemplateContent() should not fail with valid template")
|
||||
}
|
||||
390
internal/extgen/classparser.go
Normal file
390
internal/extgen/classparser.go
Normal file
@@ -0,0 +1,390 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var phpClassRegex = regexp.MustCompile(`//\s*export_php:class\s+(\w+)`)
|
||||
var phpMethodRegex = regexp.MustCompile(`//\s*export_php:method\s+(\w+)::([^{}\n]+)(?:\s*{\s*})?`)
|
||||
var methodSignatureRegex = regexp.MustCompile(`(\w+)\s*\(([^)]*)\)\s*:\s*(\??[\w|]+)`)
|
||||
var methodParamTypeNameRegex = regexp.MustCompile(`(\??[\w|]+)\s+\$?(\w+)`)
|
||||
|
||||
type exportDirective struct {
|
||||
line int
|
||||
className string
|
||||
}
|
||||
|
||||
type classParser struct{}
|
||||
|
||||
func (cp *classParser) Parse(filename string) ([]phpClass, error) {
|
||||
return cp.parse(filename)
|
||||
}
|
||||
|
||||
func (cp *classParser) parse(filename string) (classes []phpClass, err error) {
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing file: %w", err)
|
||||
}
|
||||
|
||||
validator := Validator{}
|
||||
|
||||
exportDirectives := cp.collectExportDirectives(node, fset)
|
||||
methods, err := cp.parseMethods(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing methods: %w", err)
|
||||
}
|
||||
|
||||
// match structs to directives
|
||||
matchedDirectives := make(map[int]bool)
|
||||
|
||||
var genDecl *ast.GenDecl
|
||||
var ok bool
|
||||
for _, decl := range node.Decls {
|
||||
if genDecl, ok = decl.(*ast.GenDecl); !ok || genDecl.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, spec := range genDecl.Specs {
|
||||
var typeSpec *ast.TypeSpec
|
||||
if typeSpec, ok = spec.(*ast.TypeSpec); !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var structType *ast.StructType
|
||||
if structType, ok = typeSpec.Type.(*ast.StructType); !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var phpCl string
|
||||
var directiveLine int
|
||||
if phpCl, directiveLine = cp.extractPHPClassCommentWithLine(genDecl.Doc, fset); phpCl == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
matchedDirectives[directiveLine] = true
|
||||
|
||||
class := phpClass{
|
||||
Name: phpCl,
|
||||
GoStruct: typeSpec.Name.Name,
|
||||
}
|
||||
|
||||
class.Properties = cp.parseStructFields(structType.Fields.List)
|
||||
|
||||
// associate methods with this class
|
||||
for _, method := range methods {
|
||||
if method.ClassName == phpCl {
|
||||
class.Methods = append(class.Methods, method)
|
||||
}
|
||||
}
|
||||
|
||||
if err := validator.validateClass(class); err != nil {
|
||||
fmt.Printf("Warning: Invalid class '%s': %v\n", class.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
classes = append(classes, class)
|
||||
}
|
||||
}
|
||||
|
||||
for _, directive := range exportDirectives {
|
||||
if !matchedDirectives[directive.line] {
|
||||
return nil, fmt.Errorf("//export_php class directive at line %d is not followed by a struct declaration", directive.line)
|
||||
}
|
||||
}
|
||||
|
||||
return classes, nil
|
||||
}
|
||||
|
||||
func (cp *classParser) collectExportDirectives(node *ast.File, fset *token.FileSet) []exportDirective {
|
||||
var directives []exportDirective
|
||||
|
||||
for _, commentGroup := range node.Comments {
|
||||
for _, comment := range commentGroup.List {
|
||||
if matches := phpClassRegex.FindStringSubmatch(comment.Text); matches != nil {
|
||||
pos := fset.Position(comment.Pos())
|
||||
directives = append(directives, exportDirective{
|
||||
line: pos.Line,
|
||||
className: matches[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return directives
|
||||
}
|
||||
|
||||
func (cp *classParser) extractPHPClassCommentWithLine(commentGroup *ast.CommentGroup, fset *token.FileSet) (string, int) {
|
||||
if commentGroup == nil {
|
||||
return "", 0
|
||||
}
|
||||
|
||||
for _, comment := range commentGroup.List {
|
||||
if matches := phpClassRegex.FindStringSubmatch(comment.Text); matches != nil {
|
||||
pos := fset.Position(comment.Pos())
|
||||
return matches[1], pos.Line
|
||||
}
|
||||
}
|
||||
|
||||
return "", 0
|
||||
}
|
||||
|
||||
func (cp *classParser) parseStructFields(fields []*ast.Field) []phpClassProperty {
|
||||
var properties []phpClassProperty
|
||||
|
||||
for _, field := range fields {
|
||||
for _, name := range field.Names {
|
||||
prop := cp.parseStructField(name.Name, field)
|
||||
properties = append(properties, prop)
|
||||
}
|
||||
}
|
||||
|
||||
return properties
|
||||
}
|
||||
|
||||
func (cp *classParser) parseStructField(fieldName string, field *ast.Field) phpClassProperty {
|
||||
prop := phpClassProperty{Name: fieldName}
|
||||
|
||||
// check if field is a pointer (nullable)
|
||||
if starExpr, isPointer := field.Type.(*ast.StarExpr); isPointer {
|
||||
prop.IsNullable = true
|
||||
prop.GoType = cp.typeToString(starExpr.X)
|
||||
} else {
|
||||
prop.IsNullable = false
|
||||
prop.GoType = cp.typeToString(field.Type)
|
||||
}
|
||||
|
||||
prop.PhpType = cp.goTypeToPHPType(prop.GoType)
|
||||
|
||||
return prop
|
||||
}
|
||||
|
||||
func (cp *classParser) typeToString(expr ast.Expr) string {
|
||||
switch t := expr.(type) {
|
||||
case *ast.Ident:
|
||||
return t.Name
|
||||
case *ast.StarExpr:
|
||||
return "*" + cp.typeToString(t.X)
|
||||
case *ast.ArrayType:
|
||||
return "[]" + cp.typeToString(t.Elt)
|
||||
case *ast.MapType:
|
||||
return "map[" + cp.typeToString(t.Key) + "]" + cp.typeToString(t.Value)
|
||||
default:
|
||||
return "interface{}"
|
||||
}
|
||||
}
|
||||
|
||||
func (cp *classParser) goTypeToPHPType(goType string) phpType {
|
||||
goType = strings.TrimPrefix(goType, "*")
|
||||
|
||||
typeMap := map[string]phpType{
|
||||
"string": phpString,
|
||||
"int": phpInt, "int64": phpInt, "int32": phpInt, "int16": phpInt, "int8": phpInt,
|
||||
"uint": phpInt, "uint64": phpInt, "uint32": phpInt, "uint16": phpInt, "uint8": phpInt,
|
||||
"float64": phpFloat, "float32": phpFloat,
|
||||
"bool": phpBool,
|
||||
}
|
||||
|
||||
if phpType, exists := typeMap[goType]; exists {
|
||||
return phpType
|
||||
}
|
||||
|
||||
if strings.HasPrefix(goType, "[]") || strings.HasPrefix(goType, "map[") {
|
||||
return phpArray
|
||||
}
|
||||
|
||||
return phpMixed
|
||||
}
|
||||
|
||||
func (cp *classParser) parseMethods(filename string) (methods []phpClassMethod, err error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
e := file.Close()
|
||||
if err != nil {
|
||||
err = e
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
var currentMethod *phpClassMethod
|
||||
|
||||
lineNumber := 0
|
||||
for scanner.Scan() {
|
||||
lineNumber++
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if matches := phpMethodRegex.FindStringSubmatch(line); matches != nil {
|
||||
className := strings.TrimSpace(matches[1])
|
||||
signature := strings.TrimSpace(matches[2])
|
||||
|
||||
method, err := cp.parseMethodSignature(className, signature)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Error parsing method signature %q: %v\n", signature, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
validator := Validator{}
|
||||
phpFunc := phpFunction{
|
||||
Name: method.Name,
|
||||
Signature: method.Signature,
|
||||
Params: method.Params,
|
||||
ReturnType: method.ReturnType,
|
||||
IsReturnNullable: method.isReturnNullable,
|
||||
}
|
||||
|
||||
if err := validator.validateScalarTypes(phpFunc); err != nil {
|
||||
fmt.Printf("Warning: Method \"%s::%s\" uses unsupported types: %v\n", className, method.Name, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
method.lineNumber = lineNumber
|
||||
currentMethod = method
|
||||
}
|
||||
|
||||
if currentMethod != nil && strings.HasPrefix(line, "func ") {
|
||||
goFunc, err := cp.extractGoMethodFunction(scanner, line)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("extracting Go method function: %w", err)
|
||||
}
|
||||
|
||||
currentMethod.GoFunction = goFunc
|
||||
|
||||
validator := Validator{}
|
||||
phpFunc := phpFunction{
|
||||
Name: currentMethod.Name,
|
||||
Signature: currentMethod.Signature,
|
||||
GoFunction: currentMethod.GoFunction,
|
||||
Params: currentMethod.Params,
|
||||
ReturnType: currentMethod.ReturnType,
|
||||
IsReturnNullable: currentMethod.isReturnNullable,
|
||||
}
|
||||
|
||||
if err := validator.validateGoFunctionSignatureWithOptions(phpFunc, true); err != nil {
|
||||
fmt.Printf("Warning: Go method signature mismatch for '%s::%s': %v\n", currentMethod.ClassName, currentMethod.Name, err)
|
||||
currentMethod = nil
|
||||
continue
|
||||
}
|
||||
|
||||
methods = append(methods, *currentMethod)
|
||||
currentMethod = nil
|
||||
}
|
||||
}
|
||||
|
||||
if currentMethod != nil {
|
||||
return nil, fmt.Errorf("//export_php:method directive at line %d is not followed by a function declaration", currentMethod.lineNumber)
|
||||
}
|
||||
|
||||
return methods, scanner.Err()
|
||||
}
|
||||
|
||||
func (cp *classParser) parseMethodSignature(className, signature string) (*phpClassMethod, error) {
|
||||
matches := methodSignatureRegex.FindStringSubmatch(signature)
|
||||
|
||||
if len(matches) != 4 {
|
||||
return nil, fmt.Errorf("invalid method signature format")
|
||||
}
|
||||
|
||||
methodName := matches[1]
|
||||
paramsStr := strings.TrimSpace(matches[2])
|
||||
returnTypeStr := strings.TrimSpace(matches[3])
|
||||
|
||||
isReturnNullable := strings.HasPrefix(returnTypeStr, "?")
|
||||
returnType := strings.TrimPrefix(returnTypeStr, "?")
|
||||
|
||||
var params []phpParameter
|
||||
if paramsStr != "" {
|
||||
paramParts := strings.Split(paramsStr, ",")
|
||||
for _, part := range paramParts {
|
||||
param, err := cp.parseMethodParameter(strings.TrimSpace(part))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing parameter '%s': %w", part, err)
|
||||
}
|
||||
|
||||
params = append(params, param)
|
||||
}
|
||||
}
|
||||
|
||||
return &phpClassMethod{
|
||||
Name: methodName,
|
||||
PhpName: methodName,
|
||||
ClassName: className,
|
||||
Signature: signature,
|
||||
Params: params,
|
||||
ReturnType: phpType(returnType),
|
||||
isReturnNullable: isReturnNullable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cp *classParser) parseMethodParameter(paramStr string) (phpParameter, error) {
|
||||
parts := strings.Split(paramStr, "=")
|
||||
typePart := strings.TrimSpace(parts[0])
|
||||
|
||||
param := phpParameter{HasDefault: len(parts) > 1}
|
||||
|
||||
if param.HasDefault {
|
||||
param.DefaultValue = cp.sanitizeDefaultValue(strings.TrimSpace(parts[1]))
|
||||
}
|
||||
|
||||
matches := methodParamTypeNameRegex.FindStringSubmatch(typePart)
|
||||
|
||||
if len(matches) < 3 {
|
||||
return phpParameter{}, fmt.Errorf("invalid parameter format: %s", paramStr)
|
||||
}
|
||||
|
||||
typeStr := strings.TrimSpace(matches[1])
|
||||
param.Name = strings.TrimSpace(matches[2])
|
||||
param.IsNullable = strings.HasPrefix(typeStr, "?")
|
||||
param.PhpType = phpType(strings.TrimPrefix(typeStr, "?"))
|
||||
|
||||
return param, nil
|
||||
}
|
||||
|
||||
func (cp *classParser) sanitizeDefaultValue(value string) string {
|
||||
if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
|
||||
return value
|
||||
}
|
||||
|
||||
if strings.ToLower(value) == "null" {
|
||||
return "null"
|
||||
}
|
||||
|
||||
return strings.Trim(value, `'"`)
|
||||
}
|
||||
|
||||
func (cp *classParser) extractGoMethodFunction(scanner *bufio.Scanner, firstLine string) (string, error) {
|
||||
goFunc := firstLine + "\n"
|
||||
braceCount := 1
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
goFunc += line + "\n"
|
||||
|
||||
for _, char := range line {
|
||||
switch char {
|
||||
case '{':
|
||||
braceCount++
|
||||
case '}':
|
||||
braceCount--
|
||||
}
|
||||
}
|
||||
|
||||
if braceCount == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return goFunc, nil
|
||||
}
|
||||
641
internal/extgen/classparser_test.go
Normal file
641
internal/extgen/classparser_test.go
Normal file
@@ -0,0 +1,641 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClassParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: "single class",
|
||||
input: `package main
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
name string
|
||||
Age int
|
||||
}`,
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "multiple classes",
|
||||
input: `package main
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
name string
|
||||
Age int
|
||||
}
|
||||
|
||||
//export_php:class Product
|
||||
type ProductStruct struct {
|
||||
Title string
|
||||
Price float64
|
||||
}`,
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
name: "no php classes",
|
||||
input: `package main
|
||||
|
||||
type RegularStruct struct {
|
||||
Data string
|
||||
}`,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "class with nullable fields",
|
||||
input: `package main
|
||||
|
||||
//export_php:class OptionalData
|
||||
type OptionalStruct struct {
|
||||
Required string
|
||||
Optional *string
|
||||
Count *int
|
||||
}`,
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "class with methods",
|
||||
input: `package main
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
name string
|
||||
Age int
|
||||
}
|
||||
|
||||
//export_php:method User::getName(): string
|
||||
func GetUserName(u UserStruct) string {
|
||||
return u.name
|
||||
}
|
||||
|
||||
//export_php:method User::setAge(int $age): void
|
||||
func SetUserAge(u *UserStruct, age int) {
|
||||
u.Age = age
|
||||
}`,
|
||||
expected: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
fileName := filepath.Join(tmpDir, tt.name+".go")
|
||||
require.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))
|
||||
|
||||
parser := classParser{}
|
||||
classes, err := parser.parse(fileName)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, classes, tt.expected, "parse() got wrong number of classes")
|
||||
|
||||
if tt.name == "single class" && len(classes) > 0 {
|
||||
class := classes[0]
|
||||
assert.Equal(t, "User", class.Name, "Expected class name 'User'")
|
||||
assert.Equal(t, "UserStruct", class.GoStruct, "Expected Go struct 'UserStruct'")
|
||||
assert.Len(t, class.Properties, 2, "Expected 2 properties")
|
||||
}
|
||||
|
||||
if tt.name == "class with nullable fields" && len(classes) > 0 {
|
||||
class := classes[0]
|
||||
if len(class.Properties) >= 3 {
|
||||
assert.False(t, class.Properties[0].IsNullable, "Required field should not be nullable")
|
||||
assert.True(t, class.Properties[1].IsNullable, "Optional field should be nullable")
|
||||
assert.True(t, class.Properties[2].IsNullable, "Count field should be nullable")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassMethods(t *testing.T) {
|
||||
var input = []byte(`package main
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
name string
|
||||
Age int
|
||||
}
|
||||
|
||||
//export_php:method User::getName(): string
|
||||
func GetUserName(u UserStruct) unsafe.Pointer {
|
||||
return nil
|
||||
}
|
||||
|
||||
//export_php:method User::setAge(int $age): void
|
||||
func SetUserAge(u *UserStruct, age int64) {
|
||||
u.Age = int(age)
|
||||
}
|
||||
|
||||
//export_php:method User::getInfo(string $prefix = "User"): string
|
||||
func GetUserInfo(u UserStruct, prefix *C.zend_string) unsafe.Pointer {
|
||||
return nil
|
||||
}`)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
fileName := filepath.Join(tmpDir, "test.go")
|
||||
require.NoError(t, os.WriteFile(fileName, input, 0644))
|
||||
|
||||
parser := classParser{}
|
||||
classes, err := parser.parse(fileName)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, classes, 1, "Expected 1 class")
|
||||
|
||||
class := classes[0]
|
||||
require.Len(t, class.Methods, 3, "Expected 3 methods")
|
||||
|
||||
getName := class.Methods[0]
|
||||
assert.Equal(t, "getName", getName.Name, "Expected method name 'getName'")
|
||||
assert.Equal(t, phpString, getName.ReturnType, "Expected return type 'string'")
|
||||
assert.Empty(t, getName.Params, "Expected 0 params")
|
||||
assert.Equal(t, "User", getName.ClassName, "Expected class name 'User'")
|
||||
|
||||
setAge := class.Methods[1]
|
||||
assert.Equal(t, "setAge", setAge.Name, "Expected method name 'setAge'")
|
||||
assert.Equal(t, phpVoid, setAge.ReturnType, "Expected return type 'void'")
|
||||
require.Len(t, setAge.Params, 1, "Expected 1 param")
|
||||
|
||||
param := setAge.Params[0]
|
||||
assert.Equal(t, "age", param.Name, "Expected param name 'age'")
|
||||
assert.Equal(t, phpInt, param.PhpType, "Expected param type 'int'")
|
||||
assert.False(t, param.IsNullable, "Expected param to not be nullable")
|
||||
assert.False(t, param.HasDefault, "Expected param to not have default value")
|
||||
|
||||
getInfo := class.Methods[2]
|
||||
assert.Equal(t, "getInfo", getInfo.Name, "Expected method name 'getInfo'")
|
||||
assert.Equal(t, phpString, getInfo.ReturnType, "Expected return type 'string'")
|
||||
require.Len(t, getInfo.Params, 1, "Expected 1 param")
|
||||
|
||||
param = getInfo.Params[0]
|
||||
assert.Equal(t, "prefix", param.Name, "Expected param name 'prefix'")
|
||||
assert.Equal(t, phpString, param.PhpType, "Expected param type 'string'")
|
||||
assert.True(t, param.HasDefault, "Expected param to have default value")
|
||||
assert.Equal(t, "User", param.DefaultValue, "Expected default value 'User'")
|
||||
}
|
||||
|
||||
func TestMethodParameterParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
paramStr string
|
||||
expectedParam phpParameter
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "simple int parameter",
|
||||
paramStr: "int $age",
|
||||
expectedParam: phpParameter{
|
||||
Name: "age",
|
||||
PhpType: phpInt,
|
||||
IsNullable: false,
|
||||
HasDefault: false,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "nullable string parameter",
|
||||
paramStr: "?string $name",
|
||||
expectedParam: phpParameter{
|
||||
Name: "name",
|
||||
PhpType: phpString,
|
||||
IsNullable: true,
|
||||
HasDefault: false,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "parameter with default value",
|
||||
paramStr: `string $prefix = "default"`,
|
||||
expectedParam: phpParameter{
|
||||
Name: "prefix",
|
||||
PhpType: phpString,
|
||||
IsNullable: false,
|
||||
HasDefault: true,
|
||||
DefaultValue: "default",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "nullable parameter with default null",
|
||||
paramStr: "?int $count = null",
|
||||
expectedParam: phpParameter{
|
||||
Name: "count",
|
||||
PhpType: phpInt,
|
||||
IsNullable: true,
|
||||
HasDefault: true,
|
||||
DefaultValue: "null",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid parameter format",
|
||||
paramStr: "invalid",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
parser := classParser{}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
param, err := parser.parseMethodParameter(tt.paramStr)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err, "Expected error for parameter '%s', but got none", tt.paramStr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err, "parseMethodParameter(%s) error", tt.paramStr)
|
||||
|
||||
assert.Equal(t, tt.expectedParam.Name, param.Name, "Expected name '%s'", tt.expectedParam.Name)
|
||||
assert.Equal(t, tt.expectedParam.PhpType, param.PhpType, "Expected type '%s'", tt.expectedParam.PhpType)
|
||||
assert.Equal(t, tt.expectedParam.IsNullable, param.IsNullable, "Expected isNullable %v", tt.expectedParam.IsNullable)
|
||||
assert.Equal(t, tt.expectedParam.HasDefault, param.HasDefault, "Expected hasDefault %v", tt.expectedParam.HasDefault)
|
||||
assert.Equal(t, tt.expectedParam.DefaultValue, param.DefaultValue, "Expected defaultValue '%s'", tt.expectedParam.DefaultValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoTypeToPHPType(t *testing.T) {
|
||||
tests := []struct {
|
||||
goType string
|
||||
expected phpType
|
||||
}{
|
||||
{"string", phpString},
|
||||
{"*string", phpString},
|
||||
{"int", phpInt},
|
||||
{"int64", phpInt},
|
||||
{"*int", phpInt},
|
||||
{"float64", phpFloat},
|
||||
{"*float32", phpFloat},
|
||||
{"bool", phpBool},
|
||||
{"*bool", phpBool},
|
||||
{"[]string", phpArray},
|
||||
{"map[string]int", phpArray},
|
||||
{"*[]int", phpArray},
|
||||
{"interface{}", phpMixed},
|
||||
{"CustomType", phpMixed},
|
||||
}
|
||||
|
||||
parser := classParser{}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.goType, func(t *testing.T) {
|
||||
result := parser.goTypeToPHPType(tt.goType)
|
||||
assert.Equal(t, tt.expected, result, "goTypeToPHPType(%s) = %s, want %s", tt.goType, result, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeToString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []phpType
|
||||
}{
|
||||
{
|
||||
name: "basic types",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestStruct struct {
|
||||
StringField string
|
||||
IntField int
|
||||
FloatField float64
|
||||
BoolField bool
|
||||
}`,
|
||||
expected: []phpType{phpString, phpInt, phpFloat, phpBool},
|
||||
},
|
||||
{
|
||||
name: "pointer types",
|
||||
input: `package main
|
||||
|
||||
//export_php:class NullableClass
|
||||
type NullableStruct struct {
|
||||
NullableString *string
|
||||
NullableInt *int
|
||||
NullableFloat *float64
|
||||
NullableBool *bool
|
||||
}`,
|
||||
expected: []phpType{phpString, phpInt, phpFloat, phpBool},
|
||||
},
|
||||
{
|
||||
name: "collection types",
|
||||
input: `package main
|
||||
|
||||
//export_php:class CollectionClass
|
||||
type CollectionStruct struct {
|
||||
StringSlice []string
|
||||
IntMap map[string]int
|
||||
MixedSlice []interface{}
|
||||
}`,
|
||||
expected: []phpType{phpArray, phpArray, phpArray},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
fileName := filepath.Join(tmpDir, tt.name+".go")
|
||||
require.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0o644))
|
||||
|
||||
parser := classParser{}
|
||||
classes, err := parser.parse(fileName)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, classes, 1, "Expected 1 class")
|
||||
|
||||
class := classes[0]
|
||||
require.Len(t, class.Properties, len(tt.expected), "Expected %d properties", len(tt.expected))
|
||||
|
||||
for i, expectedType := range tt.expected {
|
||||
assert.Equal(t, expectedType, class.Properties[i].PhpType, "Property %d: expected type %s", i, expectedType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassParserUnsupportedTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedClasses int
|
||||
expectedMethods int
|
||||
hasWarning bool
|
||||
}{
|
||||
{
|
||||
name: "method with array parameter should be rejected",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::arrayMethod(array $data): string
|
||||
func (tc *TestClass) arrayMethod(data interface{}) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 0,
|
||||
hasWarning: true,
|
||||
},
|
||||
{
|
||||
name: "method with object parameter should be rejected",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::objectMethod(object $obj): string
|
||||
func (tc *TestClass) objectMethod(obj interface{}) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 0,
|
||||
hasWarning: true,
|
||||
},
|
||||
{
|
||||
name: "method with mixed parameter should be rejected",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::mixedMethod(mixed $value): string
|
||||
func (tc *TestClass) mixedMethod(value interface{}) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 0,
|
||||
hasWarning: true,
|
||||
},
|
||||
{
|
||||
name: "method with array return type should be rejected",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::arrayReturn(string $name): array
|
||||
func (tc *TestClass) arrayReturn(name *C.zend_string) interface{} {
|
||||
return []string{"result"}
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 0,
|
||||
hasWarning: true,
|
||||
},
|
||||
{
|
||||
name: "method with object return type should be rejected",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::objectReturn(string $name): object
|
||||
func (tc *TestClass) objectReturn(name *C.zend_string) interface{} {
|
||||
return map[string]interface{}{"key": "value"}
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 0,
|
||||
hasWarning: true,
|
||||
},
|
||||
{
|
||||
name: "valid scalar types should pass",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::validMethod(string $name, int $count, float $rate, bool $active): string
|
||||
func validMethod(tc *TestClass, name *C.zend_string, count int64, rate float64, active bool) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 1,
|
||||
hasWarning: false,
|
||||
},
|
||||
{
|
||||
name: "valid void return should pass",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::voidMethod(string $message): void
|
||||
func voidMethod(tc *TestClass, message *C.zend_string) {
|
||||
// Do something
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 1,
|
||||
hasWarning: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
fileName := filepath.Join(tmpDir, tt.name+".go")
|
||||
require.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))
|
||||
|
||||
parser := &classParser{}
|
||||
classes, err := parser.parse(fileName)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, classes, tt.expectedClasses, "parse() got wrong number of classes")
|
||||
if len(classes) > 0 {
|
||||
assert.Len(t, classes[0].Methods, tt.expectedMethods, "parse() got wrong number of methods")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassParserGoTypeMismatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedClasses int
|
||||
expectedMethods int
|
||||
hasWarning bool
|
||||
}{
|
||||
{
|
||||
name: "method parameter count mismatch should be rejected",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::countMismatch(string $name, int $count): string
|
||||
func (tc *TestClass) countMismatch(name *C.zend_string) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 0,
|
||||
hasWarning: true,
|
||||
},
|
||||
{
|
||||
name: "method parameter type mismatch should be rejected",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::typeMismatch(string $name, int $count): string
|
||||
func (tc *TestClass) typeMismatch(name *C.zend_string, count string) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 0,
|
||||
hasWarning: true,
|
||||
},
|
||||
{
|
||||
name: "method return type mismatch should be rejected",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::returnMismatch(string $name): int
|
||||
func (tc *TestClass) returnMismatch(name *C.zend_string) string {
|
||||
return ""
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 0,
|
||||
hasWarning: true,
|
||||
},
|
||||
{
|
||||
name: "valid matching types should pass",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::validMatch(string $name, int $count): string
|
||||
func validMatch(tc *TestClass, name *C.zend_string, count int64) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 1,
|
||||
hasWarning: false,
|
||||
},
|
||||
{
|
||||
name: "valid bool types should pass",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::validBool(bool $flag): bool
|
||||
func validBool(tc *TestClass, flag bool) bool {
|
||||
return flag
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 1,
|
||||
hasWarning: false,
|
||||
},
|
||||
{
|
||||
name: "valid float types should pass",
|
||||
input: `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestClass struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::validFloat(float $value): float
|
||||
func validFloat(tc *TestClass, value float64) float64 {
|
||||
return value
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 1,
|
||||
hasWarning: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
fileName := filepath.Join(tmpDir, tt.name+".go")
|
||||
require.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))
|
||||
|
||||
parser := &classParser{}
|
||||
classes, err := parser.parse(fileName)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, classes, tt.expectedClasses, "parse() got wrong number of classes")
|
||||
if len(classes) > 0 {
|
||||
assert.Len(t, classes[0].Methods, tt.expectedMethods, "parse() got wrong number of methods")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
160
internal/extgen/constants_test.go
Normal file
160
internal/extgen/constants_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConstantsIntegration(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
content := `package main
|
||||
|
||||
//export_php:const
|
||||
const STATUS_OK = iota
|
||||
|
||||
//export_php:const
|
||||
const MAX_CONNECTIONS = 100
|
||||
|
||||
//export_php:const: function test(): void
|
||||
func Test() {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
func main() {}
|
||||
`
|
||||
|
||||
require.NoError(t, os.WriteFile(testFile, []byte(content), 0644))
|
||||
|
||||
generator := &Generator{
|
||||
BaseName: "testext",
|
||||
SourceFile: testFile,
|
||||
BuildDir: filepath.Join(tmpDir, "build"),
|
||||
}
|
||||
|
||||
require.NoError(t, generator.parseSource())
|
||||
assert.Len(t, generator.Constants, 2, "Expected 2 constants")
|
||||
|
||||
expectedConstants := map[string]struct {
|
||||
Value string
|
||||
IsIota bool
|
||||
}{
|
||||
"STATUS_OK": {"0", true},
|
||||
"MAX_CONNECTIONS": {"100", false},
|
||||
}
|
||||
|
||||
for _, constant := range generator.Constants {
|
||||
expected, exists := expectedConstants[constant.Name]
|
||||
assert.True(t, exists, "Unexpected constant: %s", constant.Name)
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
assert.Equal(t, expected.Value, constant.Value, "Constant %s: value mismatch", constant.Name)
|
||||
assert.Equal(t, expected.IsIota, constant.IsIota, "Constant %s: isIota mismatch", constant.Name)
|
||||
}
|
||||
|
||||
require.NoError(t, generator.setupBuildDirectory())
|
||||
require.NoError(t, generator.generateStubFile())
|
||||
|
||||
stubPath := filepath.Join(generator.BuildDir, generator.BaseName+".stub.php")
|
||||
stubContent, err := os.ReadFile(stubPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
stubStr := string(stubContent)
|
||||
|
||||
assert.Contains(t, stubStr, "* @cvalue", "Stub does not contain @cvalue annotation for iota constant")
|
||||
assert.Contains(t, stubStr, "const STATUS_OK = UNKNOWN;", "Stub does not contain STATUS_OK constant with UNKNOWN value")
|
||||
assert.Contains(t, stubStr, "const MAX_CONNECTIONS = 100;", "Stub does not contain MAX_CONNECTIONS constant with explicit value")
|
||||
|
||||
require.NoError(t, generator.generateCFile())
|
||||
|
||||
cPath := filepath.Join(generator.BuildDir, generator.BaseName+".c")
|
||||
cContent, err := os.ReadFile(cPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
cStr := string(cContent)
|
||||
|
||||
assert.Contains(t, cStr, `REGISTER_LONG_CONSTANT("STATUS_OK", STATUS_OK, CONST_CS | CONST_PERSISTENT);`, "C file does not contain STATUS_OK registration")
|
||||
assert.Contains(t, cStr, `REGISTER_LONG_CONSTANT("MAX_CONNECTIONS", 100, CONST_CS | CONST_PERSISTENT);`, "C file does not contain MAX_CONNECTIONS registration")
|
||||
}
|
||||
|
||||
func TestConstantsIntegrationOctal(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
content := `package main
|
||||
|
||||
//export_php:const
|
||||
const FILE_PERM = 0o755
|
||||
|
||||
//export_php:const
|
||||
const OTHER_PERM = 0o644
|
||||
|
||||
//export_php:const
|
||||
const REGULAR_INT = 42
|
||||
|
||||
func main() {}
|
||||
`
|
||||
|
||||
require.NoError(t, os.WriteFile(testFile, []byte(content), 0644))
|
||||
|
||||
generator := &Generator{
|
||||
BaseName: "octalstest",
|
||||
SourceFile: testFile,
|
||||
BuildDir: filepath.Join(tmpDir, "build"),
|
||||
}
|
||||
|
||||
require.NoError(t, generator.parseSource())
|
||||
assert.Len(t, generator.Constants, 3, "Expected 3 constants")
|
||||
|
||||
// Verify CValue conversion
|
||||
for _, constant := range generator.Constants {
|
||||
switch constant.Name {
|
||||
case "FILE_PERM":
|
||||
assert.Equal(t, "0o755", constant.Value, "FILE_PERM value mismatch")
|
||||
assert.Equal(t, "493", constant.CValue(), "FILE_PERM CValue mismatch")
|
||||
case "OTHER_PERM":
|
||||
assert.Equal(t, "0o644", constant.Value, "OTHER_PERM value mismatch")
|
||||
assert.Equal(t, "420", constant.CValue(), "OTHER_PERM CValue mismatch")
|
||||
case "REGULAR_INT":
|
||||
assert.Equal(t, "42", constant.Value, "REGULAR_INT value mismatch")
|
||||
assert.Equal(t, "42", constant.CValue(), "REGULAR_INT CValue mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
require.NoError(t, generator.setupBuildDirectory())
|
||||
|
||||
// Test C file generation
|
||||
require.NoError(t, generator.generateCFile())
|
||||
|
||||
cPath := filepath.Join(generator.BuildDir, generator.BaseName+".c")
|
||||
cContent, err := os.ReadFile(cPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
cStr := string(cContent)
|
||||
|
||||
// Verify C file uses decimal values for octal constants
|
||||
assert.Contains(t, cStr, `REGISTER_LONG_CONSTANT("FILE_PERM", 493, CONST_CS | CONST_PERSISTENT);`, "C file does not contain FILE_PERM registration with decimal value 493")
|
||||
assert.Contains(t, cStr, `REGISTER_LONG_CONSTANT("OTHER_PERM", 420, CONST_CS | CONST_PERSISTENT);`, "C file does not contain OTHER_PERM registration with decimal value 420")
|
||||
assert.Contains(t, cStr, `REGISTER_LONG_CONSTANT("REGULAR_INT", 42, CONST_CS | CONST_PERSISTENT);`, "C file does not contain REGULAR_INT registration with value 42")
|
||||
|
||||
// Test header file generation
|
||||
require.NoError(t, generator.generateHeaderFile())
|
||||
|
||||
hPath := filepath.Join(generator.BuildDir, generator.BaseName+".h")
|
||||
hContent, err := os.ReadFile(hPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
hStr := string(hContent)
|
||||
|
||||
// Verify header file uses decimal values for octal constants in #define
|
||||
assert.Contains(t, hStr, "#define FILE_PERM 493", "Header file does not contain FILE_PERM #define with decimal value 493")
|
||||
assert.Contains(t, hStr, "#define OTHER_PERM 420", "Header file does not contain OTHER_PERM #define with decimal value 420")
|
||||
assert.Contains(t, hStr, "#define REGULAR_INT 42", "Header file does not contain REGULAR_INT #define with value 42")
|
||||
}
|
||||
121
internal/extgen/constparser.go
Normal file
121
internal/extgen/constparser.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var constRegex = regexp.MustCompile(`//\s*export_php:const$`)
|
||||
var classConstRegex = regexp.MustCompile(`//\s*export_php:classconst\s+(\w+)$`)
|
||||
var constDeclRegex = regexp.MustCompile(`const\s+(\w+)\s*=\s*(.+)`)
|
||||
|
||||
type ConstantParser struct{}
|
||||
|
||||
func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
e := file.Close()
|
||||
if err == nil {
|
||||
err = e
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
lineNumber := 0
|
||||
expectConstDecl := false
|
||||
expectClassConstDecl := false
|
||||
currentClassName := ""
|
||||
currentConstantValue := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
lineNumber++
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if constRegex.MatchString(line) {
|
||||
expectConstDecl = true
|
||||
expectClassConstDecl = false
|
||||
currentClassName = ""
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if matches := classConstRegex.FindStringSubmatch(line); len(matches) == 2 {
|
||||
expectClassConstDecl = true
|
||||
expectConstDecl = false
|
||||
currentClassName = matches[1]
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (expectConstDecl || expectClassConstDecl) && strings.HasPrefix(line, "const ") {
|
||||
matches := constDeclRegex.FindStringSubmatch(line)
|
||||
if len(matches) == 3 {
|
||||
name := matches[1]
|
||||
value := strings.TrimSpace(matches[2])
|
||||
|
||||
constant := phpConstant{
|
||||
Name: name,
|
||||
Value: value,
|
||||
IsIota: value == "iota",
|
||||
lineNumber: lineNumber,
|
||||
ClassName: currentClassName,
|
||||
}
|
||||
|
||||
constant.PhpType = determineConstantType(value)
|
||||
|
||||
if constant.IsIota {
|
||||
// affect a default value because user didn't give one
|
||||
constant.Value = fmt.Sprintf("%d", currentConstantValue)
|
||||
constant.PhpType = phpInt
|
||||
currentConstantValue++
|
||||
}
|
||||
|
||||
constants = append(constants, constant)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid constant declaration at line %d: %s", lineNumber, line)
|
||||
}
|
||||
expectConstDecl = false
|
||||
expectClassConstDecl = false
|
||||
} else if (expectConstDecl || expectClassConstDecl) && !strings.HasPrefix(line, "//") && line != "" {
|
||||
// we expected a const declaration but found something else, reset
|
||||
expectConstDecl = false
|
||||
expectClassConstDecl = false
|
||||
currentClassName = ""
|
||||
}
|
||||
}
|
||||
|
||||
return constants, scanner.Err()
|
||||
}
|
||||
|
||||
// determineConstantType analyzes the value and determines its type
|
||||
func determineConstantType(value string) phpType {
|
||||
value = strings.TrimSpace(value)
|
||||
|
||||
if (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) ||
|
||||
(strings.HasPrefix(value, "`") && strings.HasSuffix(value, "`")) {
|
||||
return phpString
|
||||
}
|
||||
|
||||
if value == "true" || value == "false" {
|
||||
return phpBool
|
||||
}
|
||||
|
||||
// check for integer literals, including hex, octal, binary
|
||||
if _, err := strconv.ParseInt(value, 0, 64); err == nil {
|
||||
return phpInt
|
||||
}
|
||||
|
||||
if _, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return phpFloat
|
||||
}
|
||||
|
||||
return phpInt
|
||||
}
|
||||
552
internal/extgen/constparser_test.go
Normal file
552
internal/extgen/constparser_test.go
Normal file
@@ -0,0 +1,552 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConstantParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: "single constant",
|
||||
input: `package main
|
||||
|
||||
//export_php:const
|
||||
const MyConstant = "test_value"`,
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "multiple constants",
|
||||
input: `package main
|
||||
|
||||
//export_php:const
|
||||
const FirstConstant = "first"
|
||||
|
||||
//export_php:const
|
||||
const SecondConstant = 42
|
||||
|
||||
//export_php:const
|
||||
const ThirdConstant = true`,
|
||||
expected: 3,
|
||||
},
|
||||
{
|
||||
name: "iota constant",
|
||||
input: `package main
|
||||
|
||||
//export_php:const
|
||||
const IotaConstant = iota`,
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "mixed constants and iota",
|
||||
input: `package main
|
||||
|
||||
//export_php:const
|
||||
const StringConst = "hello"
|
||||
|
||||
//export_php:const
|
||||
const IotaConst = iota
|
||||
|
||||
//export_php:const
|
||||
const IntConst = 123`,
|
||||
expected: 3,
|
||||
},
|
||||
{
|
||||
name: "no php constants",
|
||||
input: `package main
|
||||
|
||||
const RegularConstant = "not exported"
|
||||
|
||||
func someFunction() {
|
||||
// Just regular code
|
||||
}`,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "constant with complex value",
|
||||
input: `package main
|
||||
|
||||
//export_php:const
|
||||
const ComplexConstant = "string with spaces and symbols !@#$%"`,
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "directive without constant",
|
||||
input: `package main
|
||||
|
||||
//export_php:const
|
||||
var notAConstant = "this is a variable"`,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "mixed export and non-export constants",
|
||||
input: `package main
|
||||
|
||||
const RegularConst = "regular"
|
||||
|
||||
//export_php:const
|
||||
const ExportedConst = "exported"
|
||||
|
||||
const AnotherRegular = 456
|
||||
|
||||
//export_php:const
|
||||
const AnotherExported = 789`,
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
name: "numeric constants",
|
||||
input: `package main
|
||||
|
||||
//export_php:const
|
||||
const IntConstant = 42
|
||||
|
||||
//export_php:const
|
||||
const FloatConstant = 3.14
|
||||
|
||||
//export_php:const
|
||||
const HexConstant = 0xFF`,
|
||||
expected: 3,
|
||||
},
|
||||
{
|
||||
name: "boolean constants",
|
||||
input: `package main
|
||||
|
||||
//export_php:const
|
||||
const TrueConstant = true
|
||||
|
||||
//export_php:const
|
||||
const FalseConstant = false`,
|
||||
expected: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, tt.name+".go")
|
||||
require.NoError(t, os.WriteFile(tmpFile, []byte(tt.input), 0644))
|
||||
|
||||
parser := &ConstantParser{}
|
||||
constants, err := parser.parse(tmpFile)
|
||||
assert.NoError(t, err, "parse() error")
|
||||
|
||||
assert.Len(t, constants, tt.expected, "parse() got wrong number of constants")
|
||||
|
||||
if tt.name == "single constant" && len(constants) > 0 {
|
||||
c := constants[0]
|
||||
assert.Equal(t, "MyConstant", c.Name, "Expected constant name 'MyConstant'")
|
||||
assert.Equal(t, `"test_value"`, c.Value, `Expected constant value '"test_value"'`)
|
||||
assert.Equal(t, phpString, c.PhpType, "Expected constant type 'string'")
|
||||
assert.False(t, c.IsIota, "Expected isIota to be false for string constant")
|
||||
}
|
||||
|
||||
if tt.name == "iota constant" && len(constants) > 0 {
|
||||
c := constants[0]
|
||||
assert.Equal(t, "IotaConstant", c.Name, "Expected constant name 'IotaConstant'")
|
||||
assert.True(t, c.IsIota, "Expected isIota to be true")
|
||||
assert.Equal(t, "0", c.Value, "Expected iota constant value to be '0'")
|
||||
}
|
||||
|
||||
if tt.name == "multiple constants" && len(constants) == 3 {
|
||||
expectedNames := []string{"FirstConstant", "SecondConstant", "ThirdConstant"}
|
||||
expectedValues := []string{`"first"`, "42", "true"}
|
||||
expectedTypes := []phpType{phpString, phpInt, phpBool}
|
||||
|
||||
for i, c := range constants {
|
||||
assert.Equal(t, expectedNames[i], c.Name, "Expected constant name '%s'", expectedNames[i])
|
||||
assert.Equal(t, expectedValues[i], c.Value, "Expected constant value '%s'", expectedValues[i])
|
||||
assert.Equal(t, expectedTypes[i], c.PhpType, "Expected constant type '%s'", expectedTypes[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantParserErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "invalid constant declaration",
|
||||
input: `package main
|
||||
|
||||
//export_php:const
|
||||
const = "missing name"`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "malformed constant",
|
||||
input: `package main
|
||||
|
||||
//export_php:const
|
||||
const InvalidSyntax`,
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, tt.name+".go")
|
||||
require.NoError(t, os.WriteFile(tmpFile, []byte(tt.input), 0644))
|
||||
|
||||
parser := &ConstantParser{}
|
||||
_, err := parser.parse(tmpFile)
|
||||
require.NotNil(t, err)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err, "Expected error but got none")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantParserIotaSequence(t *testing.T) {
|
||||
input := `package main
|
||||
|
||||
//export_php:const
|
||||
const FirstIota = iota
|
||||
|
||||
//export_php:const
|
||||
const SecondIota = iota
|
||||
|
||||
//export_php:const
|
||||
const ThirdIota = iota`
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
fileName := filepath.Join(tmpDir, "test.go")
|
||||
require.NoError(t, os.WriteFile(fileName, []byte(input), 0644))
|
||||
|
||||
parser := &ConstantParser{}
|
||||
constants, err := parser.parse(fileName)
|
||||
assert.NoError(t, err, "parse() error")
|
||||
|
||||
assert.Len(t, constants, 3, "Expected 3 constants")
|
||||
|
||||
expectedValues := []string{"0", "1", "2"}
|
||||
for i, c := range constants {
|
||||
assert.True(t, c.IsIota, "Expected constant %d to be iota", i)
|
||||
assert.Equal(t, expectedValues[i], c.Value, "Expected constant %d value to be '%s'", i, expectedValues[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantParserTypeDetection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
expectedType phpType
|
||||
}{
|
||||
{"string with double quotes", "\"hello world\"", phpString},
|
||||
{"string with backticks", "`hello world`", phpString},
|
||||
{"boolean true", "true", phpBool},
|
||||
{"boolean false", "false", phpBool},
|
||||
{"integer", "42", phpInt},
|
||||
{"negative integer", "-42", phpInt},
|
||||
{"hex integer", "0xFF", phpInt},
|
||||
{"octal integer", "0755", phpInt},
|
||||
{"go octal integer", "0o755", phpInt},
|
||||
{"binary integer", "0b1010", phpInt},
|
||||
{"float", "3.14", phpFloat},
|
||||
{"negative float", "-3.14", phpFloat},
|
||||
{"scientific notation", "1e10", phpFloat},
|
||||
{"unknown type", "someFunction()", phpInt},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := determineConstantType(tt.value)
|
||||
assert.Equal(t, tt.expectedType, result, "determineConstantType(%s) expected %s", tt.value, tt.expectedType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantParserClassConstants(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: "single class constant",
|
||||
input: `package main
|
||||
|
||||
//export_php:classconst MyClass
|
||||
const STATUS_ACTIVE = 1`,
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "multiple class constants",
|
||||
input: `package main
|
||||
|
||||
//export_php:classconst User
|
||||
const STATUS_ACTIVE = "active"
|
||||
|
||||
//export_php:classconst User
|
||||
const STATUS_INACTIVE = "inactive"
|
||||
|
||||
//export_php:classconst Order
|
||||
const STATE_PENDING = 0`,
|
||||
expected: 3,
|
||||
},
|
||||
{
|
||||
name: "mixed global and class constants",
|
||||
input: `package main
|
||||
|
||||
//export_php:const
|
||||
const GLOBAL_CONST = "global"
|
||||
|
||||
//export_php:classconst MyClass
|
||||
const CLASS_CONST = 42
|
||||
|
||||
//export_php:const
|
||||
const ANOTHER_GLOBAL = true`,
|
||||
expected: 3,
|
||||
},
|
||||
{
|
||||
name: "class constant with iota",
|
||||
input: `package main
|
||||
|
||||
//export_php:classconst Status
|
||||
const FIRST = iota
|
||||
|
||||
//export_php:classconst Status
|
||||
const SECOND = iota`,
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
name: "invalid class constant directive",
|
||||
input: `package main
|
||||
|
||||
//export_php:classconst
|
||||
const INVALID = "missing class name"`,
|
||||
expected: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, tt.name+".go")
|
||||
require.NoError(t, os.WriteFile(tmpFile, []byte(tt.input), 0644))
|
||||
|
||||
parser := &ConstantParser{}
|
||||
constants, err := parser.parse(tmpFile)
|
||||
assert.NoError(t, err, "parse() error")
|
||||
|
||||
assert.Len(t, constants, tt.expected, "parse() got wrong number of constants")
|
||||
|
||||
if tt.name == "single class constant" && len(constants) > 0 {
|
||||
c := constants[0]
|
||||
assert.Equal(t, "STATUS_ACTIVE", c.Name, "Expected constant name 'STATUS_ACTIVE'")
|
||||
assert.Equal(t, "MyClass", c.ClassName, "Expected class name 'MyClass'")
|
||||
assert.Equal(t, "1", c.Value, "Expected constant value '1'")
|
||||
assert.Equal(t, phpInt, c.PhpType, "Expected constant type 'int'")
|
||||
}
|
||||
|
||||
if tt.name == "multiple class constants" && len(constants) == 3 {
|
||||
expectedClasses := []string{"User", "User", "Order"}
|
||||
expectedNames := []string{"STATUS_ACTIVE", "STATUS_INACTIVE", "STATE_PENDING"}
|
||||
expectedValues := []string{`"active"`, `"inactive"`, "0"}
|
||||
|
||||
for i, c := range constants {
|
||||
assert.Equal(t, expectedClasses[i], c.ClassName, "Expected class name '%s'", expectedClasses[i])
|
||||
assert.Equal(t, expectedNames[i], c.Name, "Expected constant name '%s'", expectedNames[i])
|
||||
assert.Equal(t, expectedValues[i], c.Value, "Expected constant value '%s'", expectedValues[i])
|
||||
}
|
||||
}
|
||||
|
||||
if tt.name == "mixed global and class constants" && len(constants) == 3 {
|
||||
assert.Empty(t, constants[0].ClassName, "First constant should be global")
|
||||
assert.Equal(t, "MyClass", constants[1].ClassName, "Second constant should belong to MyClass")
|
||||
assert.Empty(t, constants[2].ClassName, "Third constant should be global")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantParserRegexMatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
line string
|
||||
expected bool
|
||||
}{
|
||||
{"//export_php:const", true},
|
||||
{"// export_php:const", true},
|
||||
{"// export_php:const", true},
|
||||
{"//export_php:const ", false}, // should not match with trailing content
|
||||
{"//export_php", false},
|
||||
{"//export_php:function", false},
|
||||
{"//export_php:class", false},
|
||||
{"// some other comment", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.line, func(t *testing.T) {
|
||||
matches := constRegex.MatchString(tc.line)
|
||||
assert.Equal(t, tc.expected, matches, "Expected regex match for line '%s'", tc.line)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantParserClassConstRegex(t *testing.T) {
|
||||
testCases := []struct {
|
||||
line string
|
||||
shouldMatch bool
|
||||
className string
|
||||
}{
|
||||
{"//export_php:classconst MyClass", true, "MyClass"},
|
||||
{"// export_php:classconst User", true, "User"},
|
||||
{"// export_php:classconst Status", true, "Status"},
|
||||
{"//export_php:classconst Order123", true, "Order123"},
|
||||
{"//export_php:classconst", false, ""},
|
||||
{"//export_php:classconst ", false, ""},
|
||||
{"//export_php:classconst MyClass extra", false, ""},
|
||||
{"//export_php:const", false, ""},
|
||||
{"//export_php:function", false, ""},
|
||||
{"// some other comment", false, ""},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.line, func(t *testing.T) {
|
||||
matches := classConstRegex.FindStringSubmatch(tc.line)
|
||||
|
||||
if tc.shouldMatch {
|
||||
assert.Len(t, matches, 2, "Expected 2 matches for line '%s'", tc.line)
|
||||
if len(matches) != 2 {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tc.className, matches[1], "Expected class name '%s'", tc.className)
|
||||
} else {
|
||||
assert.Empty(t, matches, "Expected no matches for line '%s'", tc.line)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantParserDeclRegex(t *testing.T) {
|
||||
testCases := []struct {
|
||||
line string
|
||||
shouldMatch bool
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{"const MyConst = \"value\"", true, "MyConst", "\"value\""},
|
||||
{"const IntConst = 42", true, "IntConst", "42"},
|
||||
{"const BoolConst = true", true, "BoolConst", "true"},
|
||||
{"const IotaConst = iota", true, "IotaConst", "iota"},
|
||||
{"const ComplexValue = someFunction()", true, "ComplexValue", "someFunction()"},
|
||||
{"const SpacedName = \"with spaces\"", true, "SpacedName", "\"with spaces\""},
|
||||
{"var notAConst = \"value\"", false, "", ""},
|
||||
{"const", false, "", ""},
|
||||
{"const =", false, "", ""},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.line, func(t *testing.T) {
|
||||
matches := constDeclRegex.FindStringSubmatch(tc.line)
|
||||
|
||||
if tc.shouldMatch {
|
||||
assert.Len(t, matches, 3, "Expected 3 matches for line '%s'", tc.line)
|
||||
if len(matches) != 3 {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tc.name, matches[1], "Expected name '%s'", tc.name)
|
||||
assert.Equal(t, tc.value, matches[2], "Expected value '%s'", tc.value)
|
||||
} else {
|
||||
assert.Empty(t, matches, "Expected no matches for line '%s'", tc.line)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPHPConstantCValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
constant phpConstant
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "octal notation 0o35",
|
||||
constant: phpConstant{
|
||||
Name: "OctalConst",
|
||||
Value: "0o35",
|
||||
PhpType: phpInt,
|
||||
},
|
||||
expected: "29", // 0o35 = 29 in decimal
|
||||
},
|
||||
{
|
||||
name: "octal notation 0o755",
|
||||
constant: phpConstant{
|
||||
Name: "OctalPerm",
|
||||
Value: "0o755",
|
||||
PhpType: phpInt,
|
||||
},
|
||||
expected: "493", // 0o755 = 493 in decimal
|
||||
},
|
||||
{
|
||||
name: "regular integer",
|
||||
constant: phpConstant{
|
||||
Name: "RegularInt",
|
||||
Value: "42",
|
||||
PhpType: phpInt,
|
||||
},
|
||||
expected: "42",
|
||||
},
|
||||
{
|
||||
name: "hex integer",
|
||||
constant: phpConstant{
|
||||
Name: "HexInt",
|
||||
Value: "0xFF",
|
||||
PhpType: phpInt,
|
||||
},
|
||||
expected: "0xFF", // hex should remain unchanged
|
||||
},
|
||||
{
|
||||
name: "string constant",
|
||||
constant: phpConstant{
|
||||
Name: "StringConst",
|
||||
Value: "\"hello\"",
|
||||
PhpType: phpString,
|
||||
},
|
||||
expected: "\"hello\"", // strings should remain unchanged
|
||||
},
|
||||
{
|
||||
name: "boolean constant",
|
||||
constant: phpConstant{
|
||||
Name: "BoolConst",
|
||||
Value: "true",
|
||||
PhpType: phpBool,
|
||||
},
|
||||
expected: "true", // booleans should remain unchanged
|
||||
},
|
||||
{
|
||||
name: "float constant",
|
||||
constant: phpConstant{
|
||||
Name: "FloatConst",
|
||||
Value: "3.14",
|
||||
PhpType: phpFloat,
|
||||
},
|
||||
expected: "3.14", // floats should remain unchanged
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.constant.CValue()
|
||||
assert.Equal(t, tt.expected, result, "CValue() expected %s", tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
46
internal/extgen/docs.go
Normal file
46
internal/extgen/docs.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//go:embed templates/README.md.tpl
|
||||
var docFileContent string
|
||||
|
||||
type DocumentationGenerator struct {
|
||||
generator *Generator
|
||||
}
|
||||
|
||||
type DocTemplateData struct {
|
||||
BaseName string
|
||||
Functions []phpFunction
|
||||
Classes []phpClass
|
||||
}
|
||||
|
||||
func (dg *DocumentationGenerator) generate() error {
|
||||
filename := filepath.Join(dg.generator.BuildDir, "README.md")
|
||||
content, err := dg.generateMarkdown()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeFile(filename, content)
|
||||
}
|
||||
|
||||
func (dg *DocumentationGenerator) generateMarkdown() (string, error) {
|
||||
tmpl := template.Must(template.New("readme").Parse(docFileContent))
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, DocTemplateData{
|
||||
BaseName: dg.generator.BaseName,
|
||||
Functions: dg.generator.Functions,
|
||||
Classes: dg.generator.Classes,
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
386
internal/extgen/docs_test.go
Normal file
386
internal/extgen/docs_test.go
Normal file
@@ -0,0 +1,386 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDocumentationGenerator_Generate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
generator *Generator
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "simple extension with functions",
|
||||
generator: &Generator{
|
||||
BaseName: "testextension",
|
||||
BuildDir: "",
|
||||
Functions: []phpFunction{
|
||||
{
|
||||
Name: "greet",
|
||||
ReturnType: phpString,
|
||||
Params: []phpParameter{
|
||||
{Name: "name", PhpType: phpString},
|
||||
},
|
||||
Signature: "greet(string $name): string",
|
||||
},
|
||||
},
|
||||
Classes: []phpClass{},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "extension with classes",
|
||||
generator: &Generator{
|
||||
BaseName: "classextension",
|
||||
BuildDir: "",
|
||||
Functions: []phpFunction{},
|
||||
Classes: []phpClass{
|
||||
{
|
||||
Name: "TestClass",
|
||||
Properties: []phpClassProperty{
|
||||
{Name: "name", PhpType: phpString},
|
||||
{Name: "count", PhpType: phpInt, IsNullable: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "extension with both functions and classes",
|
||||
generator: &Generator{
|
||||
BaseName: "fullextension",
|
||||
BuildDir: "",
|
||||
Functions: []phpFunction{
|
||||
{
|
||||
Name: "calculate",
|
||||
ReturnType: phpInt,
|
||||
IsReturnNullable: true,
|
||||
Params: []phpParameter{
|
||||
{Name: "base", PhpType: phpInt},
|
||||
{Name: "multiplier", PhpType: phpInt, HasDefault: true, DefaultValue: "2", IsNullable: true},
|
||||
},
|
||||
Signature: "calculate(int $base, ?int $multiplier = 2): ?int",
|
||||
},
|
||||
},
|
||||
Classes: []phpClass{
|
||||
{
|
||||
Name: "Calculator",
|
||||
Properties: []phpClassProperty{
|
||||
{Name: "precision", PhpType: phpInt},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty extension",
|
||||
generator: &Generator{
|
||||
BaseName: "emptyextension",
|
||||
BuildDir: "",
|
||||
Functions: []phpFunction{},
|
||||
Classes: []phpClass{},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
tt.generator.BuildDir = tempDir
|
||||
|
||||
docGen := &DocumentationGenerator{
|
||||
generator: tt.generator,
|
||||
}
|
||||
|
||||
err := docGen.generate()
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err, "generate() expected error but got none")
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err, "generate() unexpected error")
|
||||
|
||||
readmePath := filepath.Join(tempDir, "README.md")
|
||||
require.FileExists(t, readmePath)
|
||||
|
||||
content, err := os.ReadFile(readmePath)
|
||||
require.NoError(t, err, "Failed to read generated README.md")
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
assert.Contains(t, contentStr, "# "+tt.generator.BaseName+" Extension", "README should contain extension title")
|
||||
assert.Contains(t, contentStr, "Auto-generated PHP extension from Go code.", "README should contain description")
|
||||
|
||||
if len(tt.generator.Functions) > 0 {
|
||||
assert.Contains(t, contentStr, "## Functions", "README should contain functions section when functions exist")
|
||||
|
||||
for _, fn := range tt.generator.Functions {
|
||||
assert.Contains(t, contentStr, "### "+fn.Name, "README should contain function %s", fn.Name)
|
||||
assert.Contains(t, contentStr, fn.Signature, "README should contain function signature for %s", fn.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tt.generator.Classes) > 0 {
|
||||
assert.Contains(t, contentStr, "## Classes", "README should contain classes section when classes exist")
|
||||
|
||||
for _, class := range tt.generator.Classes {
|
||||
assert.Contains(t, contentStr, "### "+class.Name, "README should contain class %s", class.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentationGenerator_GenerateMarkdown(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
generator *Generator
|
||||
contains []string
|
||||
notContains []string
|
||||
}{
|
||||
{
|
||||
name: "function with parameters",
|
||||
generator: &Generator{
|
||||
BaseName: "testextension",
|
||||
Functions: []phpFunction{
|
||||
{
|
||||
Name: "processData",
|
||||
ReturnType: phpArray,
|
||||
Params: []phpParameter{
|
||||
{Name: "data", PhpType: phpString},
|
||||
{Name: "options", PhpType: phpArray, IsNullable: true},
|
||||
{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "10"},
|
||||
},
|
||||
Signature: "processData(string $data, ?array $options, int $count = 10): array",
|
||||
},
|
||||
},
|
||||
Classes: []phpClass{},
|
||||
},
|
||||
contains: []string{
|
||||
"# testextension Extension",
|
||||
"## Functions",
|
||||
"### processData",
|
||||
"**Parameters:**",
|
||||
"- `data` (string)",
|
||||
"- `options` (array) (nullable)",
|
||||
"- `count` (int) (default: 10)",
|
||||
"**Returns:** array",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nullable return type",
|
||||
generator: &Generator{
|
||||
BaseName: "nullableext",
|
||||
Functions: []phpFunction{
|
||||
{
|
||||
Name: "maybeGetValue",
|
||||
ReturnType: phpString,
|
||||
IsReturnNullable: true,
|
||||
Params: []phpParameter{},
|
||||
Signature: "maybeGetValue(): ?string",
|
||||
},
|
||||
},
|
||||
Classes: []phpClass{},
|
||||
},
|
||||
contains: []string{
|
||||
"**Returns:** string (nullable)",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "class with properties",
|
||||
generator: &Generator{
|
||||
BaseName: "classext",
|
||||
Functions: []phpFunction{},
|
||||
Classes: []phpClass{
|
||||
{
|
||||
Name: "DataProcessor",
|
||||
Properties: []phpClassProperty{
|
||||
{Name: "name", PhpType: phpString},
|
||||
{Name: "config", PhpType: phpArray, IsNullable: true},
|
||||
{Name: "enabled", PhpType: phpBool},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
contains: []string{
|
||||
"## Classes",
|
||||
"### DataProcessor",
|
||||
"**Properties:**",
|
||||
"- `name`: string",
|
||||
"- `config`: array (nullable)",
|
||||
"- `enabled`: bool",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extension with no functions or classes",
|
||||
generator: &Generator{
|
||||
BaseName: "emptyext",
|
||||
Functions: []phpFunction{},
|
||||
Classes: []phpClass{},
|
||||
},
|
||||
contains: []string{
|
||||
"# emptyext Extension",
|
||||
"Auto-generated PHP extension from Go code.",
|
||||
},
|
||||
notContains: []string{
|
||||
"## Functions",
|
||||
"## Classes",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "function with no parameters",
|
||||
generator: &Generator{
|
||||
BaseName: "noparamext",
|
||||
Functions: []phpFunction{
|
||||
{
|
||||
Name: "getCurrentTime",
|
||||
ReturnType: phpInt,
|
||||
Params: []phpParameter{},
|
||||
Signature: "getCurrentTime(): int",
|
||||
},
|
||||
},
|
||||
Classes: []phpClass{},
|
||||
},
|
||||
contains: []string{
|
||||
"### getCurrentTime",
|
||||
"**Returns:** int",
|
||||
},
|
||||
notContains: []string{
|
||||
"**Parameters:**",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "class with no properties",
|
||||
generator: &Generator{
|
||||
BaseName: "nopropsext",
|
||||
Functions: []phpFunction{},
|
||||
Classes: []phpClass{
|
||||
{
|
||||
Name: "EmptyClass",
|
||||
Properties: []phpClassProperty{},
|
||||
},
|
||||
},
|
||||
},
|
||||
contains: []string{
|
||||
"### EmptyClass",
|
||||
},
|
||||
notContains: []string{
|
||||
"**Properties:**",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
docGen := &DocumentationGenerator{
|
||||
generator: tt.generator,
|
||||
}
|
||||
|
||||
result, err := docGen.generateMarkdown()
|
||||
if !assert.NoError(t, err, "generateMarkdown() unexpected error") {
|
||||
return
|
||||
}
|
||||
|
||||
for _, expected := range tt.contains {
|
||||
assert.Contains(t, result, expected, "generateMarkdown() should contain '%s'", expected)
|
||||
}
|
||||
|
||||
for _, notExpected := range tt.notContains {
|
||||
assert.NotContains(t, result, notExpected, "generateMarkdown() should NOT contain '%s'", notExpected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentationGenerator_Generate_InvalidDirectory(t *testing.T) {
|
||||
generator := &Generator{
|
||||
BaseName: "test",
|
||||
BuildDir: "/nonexistent/directory",
|
||||
Functions: []phpFunction{},
|
||||
Classes: []phpClass{},
|
||||
}
|
||||
|
||||
docGen := &DocumentationGenerator{
|
||||
generator: generator,
|
||||
}
|
||||
|
||||
err := docGen.generate()
|
||||
assert.Error(t, err, "generate() expected error for invalid directory but got none")
|
||||
}
|
||||
|
||||
func TestDocumentationGenerator_TemplateError(t *testing.T) {
|
||||
generator := &Generator{
|
||||
BaseName: "test",
|
||||
Functions: []phpFunction{
|
||||
{
|
||||
Name: "test",
|
||||
ReturnType: phpString,
|
||||
Signature: "test(): string",
|
||||
},
|
||||
},
|
||||
Classes: []phpClass{},
|
||||
}
|
||||
|
||||
docGen := &DocumentationGenerator{
|
||||
generator: generator,
|
||||
}
|
||||
|
||||
result, err := docGen.generateMarkdown()
|
||||
assert.NoError(t, err, "generateMarkdown() unexpected error")
|
||||
assert.NotEmpty(t, result, "generateMarkdown() returned empty result")
|
||||
}
|
||||
|
||||
func BenchmarkDocumentationGenerator_GenerateMarkdown(b *testing.B) {
|
||||
generator := &Generator{
|
||||
BaseName: "benchext",
|
||||
Functions: []phpFunction{
|
||||
{
|
||||
Name: "function1",
|
||||
ReturnType: phpString,
|
||||
Params: []phpParameter{
|
||||
{Name: "param1", PhpType: phpString},
|
||||
{Name: "param2", PhpType: phpInt, HasDefault: true, DefaultValue: "0"},
|
||||
},
|
||||
Signature: "function1(string $param1, int $param2 = 0): string",
|
||||
},
|
||||
{
|
||||
Name: "function2",
|
||||
ReturnType: phpArray,
|
||||
IsReturnNullable: true,
|
||||
Params: []phpParameter{
|
||||
{Name: "data", PhpType: phpArray, IsNullable: true},
|
||||
},
|
||||
Signature: "function2(?array $data): ?array",
|
||||
},
|
||||
},
|
||||
Classes: []phpClass{
|
||||
{
|
||||
Name: "TestClass",
|
||||
Properties: []phpClassProperty{
|
||||
{Name: "prop1", PhpType: phpString},
|
||||
{Name: "prop2", PhpType: phpInt, IsNullable: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
docGen := &DocumentationGenerator{
|
||||
generator: generator,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := docGen.generateMarkdown()
|
||||
assert.NoError(b, err)
|
||||
}
|
||||
}
|
||||
17
internal/extgen/errors.go
Normal file
17
internal/extgen/errors.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package extgen
|
||||
|
||||
import "fmt"
|
||||
|
||||
type GeneratorError struct {
|
||||
Stage string
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *GeneratorError) Error() string {
|
||||
if e.Err == nil {
|
||||
return fmt.Sprintf("generator error at %s: %s", e.Stage, e.Message)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("generator error at %s: %s: %v", e.Stage, e.Message, e.Err)
|
||||
}
|
||||
183
internal/extgen/funcparser.go
Normal file
183
internal/extgen/funcparser.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var phpFuncRegex = regexp.MustCompile(`//\s*export_php:function\s+([^{}\n]+)(?:\s*{\s*})?`)
|
||||
var signatureRegex = regexp.MustCompile(`(\w+)\s*\(([^)]*)\)\s*:\s*(\??[\w|]+)`)
|
||||
var typeNameRegex = regexp.MustCompile(`(\??[\w|]+)\s+\$?(\w+)`)
|
||||
|
||||
type FuncParser struct{}
|
||||
|
||||
func (fp *FuncParser) parse(filename string) (functions []phpFunction, err error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
e := file.Close()
|
||||
if err == nil {
|
||||
err = e
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
var currentPHPFunc *phpFunction
|
||||
validator := Validator{}
|
||||
|
||||
lineNumber := 0
|
||||
for scanner.Scan() {
|
||||
lineNumber++
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if matches := phpFuncRegex.FindStringSubmatch(line); matches != nil {
|
||||
signature := strings.TrimSpace(matches[1])
|
||||
phpFunc, err := fp.parseSignature(signature)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Error parsing signature '%s': %v\n", signature, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err := validator.validateFunction(*phpFunc); err != nil {
|
||||
fmt.Printf("Warning: Invalid function '%s': %v\n", phpFunc.Name, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err := validator.validateScalarTypes(*phpFunc); err != nil {
|
||||
fmt.Printf("Warning: Function '%s' uses unsupported types: %v\n", phpFunc.Name, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
phpFunc.lineNumber = lineNumber
|
||||
currentPHPFunc = phpFunc
|
||||
}
|
||||
|
||||
if currentPHPFunc != nil && strings.HasPrefix(line, "func ") {
|
||||
goFunc, err := fp.extractGoFunction(scanner, line)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("extracting Go function: %w", err)
|
||||
}
|
||||
|
||||
currentPHPFunc.GoFunction = goFunc
|
||||
|
||||
if err := validator.validateGoFunctionSignatureWithOptions(*currentPHPFunc, false); err != nil {
|
||||
fmt.Printf("Warning: Go function signature mismatch for %q: %v\n", currentPHPFunc.Name, err)
|
||||
currentPHPFunc = nil
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
functions = append(functions, *currentPHPFunc)
|
||||
currentPHPFunc = nil
|
||||
}
|
||||
}
|
||||
|
||||
if currentPHPFunc != nil {
|
||||
return nil, fmt.Errorf("//export_php function directive at line %d is not followed by a function declaration", currentPHPFunc.lineNumber)
|
||||
}
|
||||
|
||||
return functions, scanner.Err()
|
||||
}
|
||||
|
||||
func (fp *FuncParser) extractGoFunction(scanner *bufio.Scanner, firstLine string) (string, error) {
|
||||
goFunc := firstLine + "\n"
|
||||
braceCount := 1
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
goFunc += line + "\n"
|
||||
|
||||
for _, char := range line {
|
||||
switch char {
|
||||
case '{':
|
||||
braceCount++
|
||||
case '}':
|
||||
braceCount--
|
||||
}
|
||||
}
|
||||
|
||||
if braceCount == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return goFunc, nil
|
||||
}
|
||||
|
||||
func (fp *FuncParser) parseSignature(signature string) (*phpFunction, error) {
|
||||
matches := signatureRegex.FindStringSubmatch(signature)
|
||||
|
||||
if len(matches) != 4 {
|
||||
return nil, fmt.Errorf("invalid signature format")
|
||||
}
|
||||
|
||||
name := matches[1]
|
||||
paramsStr := strings.TrimSpace(matches[2])
|
||||
returnTypeStr := strings.TrimSpace(matches[3])
|
||||
|
||||
isReturnNullable := strings.HasPrefix(returnTypeStr, "?")
|
||||
returnType := strings.TrimPrefix(returnTypeStr, "?")
|
||||
|
||||
var params []phpParameter
|
||||
if paramsStr != "" {
|
||||
paramParts := strings.Split(paramsStr, ",")
|
||||
for _, part := range paramParts {
|
||||
param, err := fp.parseParameter(strings.TrimSpace(part))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing parameter '%s': %w", part, err)
|
||||
}
|
||||
params = append(params, param)
|
||||
}
|
||||
}
|
||||
|
||||
return &phpFunction{
|
||||
Name: name,
|
||||
Signature: signature,
|
||||
Params: params,
|
||||
ReturnType: phpType(returnType),
|
||||
IsReturnNullable: isReturnNullable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fp *FuncParser) parseParameter(paramStr string) (phpParameter, error) {
|
||||
parts := strings.Split(paramStr, "=")
|
||||
typePart := strings.TrimSpace(parts[0])
|
||||
|
||||
param := phpParameter{HasDefault: len(parts) > 1}
|
||||
|
||||
if param.HasDefault {
|
||||
param.DefaultValue = fp.sanitizeDefaultValue(strings.TrimSpace(parts[1]))
|
||||
}
|
||||
|
||||
matches := typeNameRegex.FindStringSubmatch(typePart)
|
||||
|
||||
if len(matches) < 3 {
|
||||
return phpParameter{}, fmt.Errorf("invalid parameter format: %s", paramStr)
|
||||
}
|
||||
|
||||
typeStr := strings.TrimSpace(matches[1])
|
||||
param.Name = strings.TrimSpace(matches[2])
|
||||
param.IsNullable = strings.HasPrefix(typeStr, "?")
|
||||
param.PhpType = phpType(strings.TrimPrefix(typeStr, "?"))
|
||||
|
||||
return param, nil
|
||||
}
|
||||
|
||||
func (fp *FuncParser) sanitizeDefaultValue(value string) string {
|
||||
if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
|
||||
return value
|
||||
}
|
||||
if strings.ToLower(value) == "null" {
|
||||
return "null"
|
||||
}
|
||||
|
||||
return strings.Trim(value, `'"`)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user