Compare commits

...

76 Commits

Author SHA1 Message Date
Robert Landers
12311107f4 error when there is more than one module:init or module:shutdown
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:57:23 +02:00
Robert Landers
c57e1c6d2a combine var
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:52:24 +02:00
Robert Landers
44d58e3590 use literals
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:52:11 +02:00
Robert Landers
36cdb72536 fix newlines
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:34:32 +02:00
Robert Landers
17eba05bcd remove EXPERIMENTAL
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:33:34 +02:00
Robert Landers
50208aa818 fix the regex
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:33:20 +02:00
Robert Landers
f76fd14c3f only import runtime/cgo when it needs to
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:42 +02:00
Robert Landers
6c208e2753 Revert "remove import that causes issues"
This reverts commit 3d9d672248778852ffa47c9bad238b2587832077.
2025-08-11 19:06:41 +02:00
Robert Landers
31e045bb75 remove import that causes issues
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:40 +02:00
Robert Landers
0d9dda91e9 handle file.close error
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:39 +02:00
Robert Landers
74e9e9aa19 handle init function case
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:38 +02:00
Robert Landers
327a20ce63 update to handle tagging specific functions
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:37 +02:00
Robert Landers
8efbc6c1e2 add tests
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:36 +02:00
Robert Landers
7ea6e7c093 add ability to specify startup/shutdown functions
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:35 +02:00
Rob Landers
c7bc5a3778 handle extensions in cli mode (#1798)
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 11:00:13 +02:00
Alexander Stecher
9e4a6b789b refactor: remove some duplications in tests (#1783)
* Removes test duplications.

* Adds t.Helper().

* Fixes tests.

* UUpdate frankenphp_test.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-08-09 22:45:56 +02:00
Marc
8d148a16e2 feat(static): add iconv extension (#1793) 2025-08-07 12:50:47 +02:00
Alexandre Daubois
1d0169d321 fix(types): better zval handling to avoid leaks with arrays (#1780) 2025-08-04 19:00:13 +02:00
WeidiDeng
365eae1a99 fix(caddy): check if http app fails to provision due to not configured or invalid configuration (#1774) 2025-07-30 11:43:48 +02:00
Alexander Stecher
2a41fc183a fix: dev Docker image build (#1769)
* Fixes build command.

* Rests ti go.sh
2025-07-27 15:31:23 +02:00
Alexandre Daubois
8175ae7e8c chore: miscellaneous fix in C code (#1766) 2025-07-24 10:24:38 +02:00
Cthulhux
cd16da248a Update feature_request.yaml (#1765)
docs: fix typo in issue template
2025-07-23 16:23:56 +02:00
Kévin Dunglas
f224ffaece docs: add extensions to the TOC 2025-07-18 18:22:52 +02:00
Kévin Dunglas
50b438f978 chore: prepare release 1.9.0 2025-07-18 12:13:24 +02:00
Kévin Dunglas
f7ea33d328 chore: upgrade Mercure to v0.20 2025-07-18 12:11:51 +02:00
Emmanuel Barat
ce9620b5be feat: add pdo_sqlsrv extension to static binary 2025-07-18 01:33:15 +02:00
Kévin Dunglas
6e120283e9 chore: add support for GITHUB_TOKEN in static-builder-gnu 2025-07-17 16:52:54 +02:00
Kévin Dunglas
1da2ba1f28 fix(ci): Docker builds 2025-07-17 10:14:18 +02:00
Kévin Dunglas
0c25b2488c chore: bump deps 2025-07-16 13:29:04 +02:00
Marc
3e542576f6 chore: remove system include locations from frankenphp.go (#1734)
* add "nosys" tag to not pull in system include locations

* rename to "nosysinc"

* Revert "rename to "nosysinc""

This reverts commit a7ff2a0fd9.

* remove paths all together

* bring back rpath for macos

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-07-16 13:27:24 +02:00
Alexandre Daubois
34fbfd467b chore(extgen): remove useless constructors 2025-07-16 12:06:23 +02:00
Alexandre Daubois
8df41236d9 feat(extgen): add support for arrays as parameters and return types (#1724)
* feat(extgen): add support for arrays as parameters and return types

* cs

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-07-16 12:05:29 +02:00
Alexandre Daubois
1804e36b93 feat(extgen): add support for //export_php:namespace (#1721) 2025-07-16 12:01:39 +02:00
Marc
63c742648d feat(static): add password-argon2 to default extensions (#1732) 2025-07-16 11:59:52 +02:00
Alexander Stecher
a19fcdb38d fix: forwards php_server root to try_files (#1729)
* Adds 'root' to try_files.

* Formatting.

* Fixes test with wrong assumption.

* Adds more test cases.

* Prevents conflicts with other tests.
2025-07-16 11:58:36 +02:00
Alexander Stecher
a161af26ae fix: allow headers without whitespace after colon (#1741)
* Allows headers without whitespace after colon.

* Makes headers faster.

* Optimizes header splitting.

* Formatting.
2025-07-16 08:57:37 +02:00
Alexander Stecher
23073b6626 ci: remove the prefix from the latest tag (#1745) 2025-07-15 04:08:37 +02:00
Alexander Stecher
d5544bbca4 ci: compare Docker images with latest release tag (#1736)
* Compares docker images with latest release version.

* Fixes variable.

* Makes linter happy
2025-07-09 23:41:28 +02:00
Alexandre Daubois
6ce99f251a chore(extgen): unexport more symbols (#1719) 2025-07-07 05:55:09 +02:00
Marc
1ba19ae09e docs: bring back note for php_server -> root (#1726) 2025-07-05 21:44:02 +02:00
Alexandre Daubois
b80cb6cdea chore: cleanup duplication in sapi_cli_register_variables() (#1716) 2025-07-05 19:05:38 +02:00
Alexandre Daubois
23c493dfcf chore(ci): only trigger time consuming steps when relevant (#1714) 2025-07-05 18:37:59 +02:00
Kévin Dunglas
6be261169a chore: prepare release 1.8.0 2025-07-01 15:43:12 +02:00
Kévin Dunglas
292e98cd3d chore: better errors for GNU builds (#1712) 2025-07-01 15:17:52 +02:00
Marc
e9d8923c6a fix: g++ not found error in GNU static builds (#1713)
* add brotli and xz extensions

* temporary fix for g++ not found
2025-07-01 14:16:48 +02:00
Luffy
ac900e0df4 docs: update repository links and sync cn readme (#1711) 2025-07-01 10:29:55 +02:00
Kévin Dunglas
40ee7929a1 chore: bump deps (#1708) 2025-07-01 10:29:21 +02:00
Alexander Stecher
fb10b1e8f0 feat: worker matching (#1646)
* Adds 'match' configuration

* test

* Adds Caddy's matcher.

* Adds no-fileserver test.

* Prevents duplicate path calculations and optimizes worker access.

* trigger

* Changes worker->match to match->worker

* Adjusts tests.

* formatting

* Resets implementation to worker->match

* Provisions match path rules.

* Allows matching multiple paths

* Fixes var

* Formatting.

* refactoring.

* Adds 'match' configuration

* test

* Adds Caddy's matcher.

* Adds no-fileserver test.

* Prevents duplicate path calculations and optimizes worker access.

* trigger

* Changes worker->match to match->worker

* Adjusts tests.

* formatting

* Resets implementation to worker->match

* Provisions match path rules.

* Allows matching multiple paths

* Fixes var

* Formatting.

* refactoring.

* Update frankenphp.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Update caddy/workerconfig.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Update caddy/workerconfig.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Update caddy/module.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Update caddy/module.go

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* Fixes suggestion

* Refactoring.

* Adds 'match' configuration

* test

* Adds Caddy's matcher.

* Adds no-fileserver test.

* Prevents duplicate path calculations and optimizes worker access.

* trigger

* Changes worker->match to match->worker

* Adjusts tests.

* formatting

* Resets implementation to worker->match

* Provisions match path rules.

* Allows matching multiple paths

* Fixes var

* Formatting.

* refactoring.

* Adds docs.

* Fixes merge removal.

* Update config.md

* go fmt.

* Adds line ending to static.txt and fixes tests.

* Trigger CI

* fix Markdown CS

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-07-01 10:27:11 +02:00
demouth
94c3fac556 docs: removed unnecessary triple backtick block (#1709) 2025-07-01 07:01:15 +02:00
Thérage Kévin
fcc5299a20 docs: add precision on where to add max_wait_time configuration (#1640)
* Adding precision on where to add max_wait_time configuration

* Adding precision on where to add max_wait_time configuration

* fix review
2025-06-30 22:08:08 +02:00
Alexandre Daubois
92abb16bc0 docs: add French translation for extensions (#1705) 2025-06-30 16:54:43 +02:00
Alexandre Daubois
94ac4b4935 chore: use modern ZPP macros in the extension generator (#1703) 2025-06-30 14:50:36 +02:00
Alexandre Daubois
29c88c0fec feat: use modern ZEND_PARSE_PARAMETERS_NONE() macro (#1704) 2025-06-30 14:50:11 +02:00
Kévin Dunglas
80de1f8bc7 chore: bump deps (#1702) 2025-06-30 14:36:26 +02:00
Alexandre Daubois
93f2384749 docs: add extension and extension generator docs (#1652) 2025-06-30 14:29:25 +02:00
Marc
db9c8446ef feat(static): add brotli and xz extensions (#1647) 2025-06-30 13:32:46 +02:00
Alexandre Daubois
995c829247 feat: add logs on up and down scaling threads (#1695) 2025-06-30 12:01:25 +02:00
Alexandre Daubois
96400a85d0 feat(worker): make maximum consecutive failures configurable (#1692) 2025-06-30 09:38:18 +02:00
Alexandre Daubois
58fde42654 fix: improve generated C extension code (#1698) 2025-06-30 09:23:21 +02:00
Kévin Dunglas
291dd4eed9 chore!: uniformize thread attribute name in logs (#1699) 2025-06-29 09:33:06 +02:00
Alexandre Daubois
30ef5f6657 chore: use filepath.Separator instead of hardcoded separator (#1685) 2025-06-27 14:36:31 +02:00
Alexandre Daubois
8d88c13795 chore: remove TODO items not relevant anymore (#1694) 2025-06-27 14:36:09 +02:00
Alexandre Daubois
d2a1b619a5 feat: expose SSL_CIPHER env var (#1693) 2025-06-27 14:27:20 +02:00
Alexandre Daubois
9e3b47c52f fix(extgen): capitalize cgo handle function call (#1696) 2025-06-27 14:26:09 +02:00
Kévin Dunglas
abfd893d88 feat: FrankenPHP extensions (#1651)
* feat: add helpers to create PHP extensions (#1644)

* feat: add helpers to create PHP extensions

* cs

* feat: GoString

* test

* add test for RegisterExtension

* cs

* optimize includes

* fix

* feat(extensions): add the PHP extension generator (#1649)

* feat(extensions): add the PHP extension generator

* unexport many types

* unexport more symbols

* cleanup some tests

* unexport more symbols

* fix

* revert types files

* revert

* add better validation and fix templates

* remove GoStringCopy

* small fixes

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

* try to fix tests

* fix CS

* try some workarounds

* try some workarounds

* ingore TestRegisterExtension

* exclude cgo tests in Docker images

* fix

* workaround...

* race detector

* simplify tests and code

* make linter happy

* feat(gofile): use templates to generate the Go file (#1666)

---------

Co-authored-by: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com>
2025-06-25 10:18:22 +02:00
Alexandre Daubois
bbc3e49d6f ci: remove unneeded echoes (#1683) 2025-06-25 10:17:53 +02:00
Alexandre Daubois
2712876e95 ci(docker): authenticate GitHub API calls (#1680) 2025-06-24 16:51:21 +02:00
Alexandre Daubois
b2435183f4 feat: add support for SERVER_ROOT to provide a different app root (#1678) 2025-06-23 20:47:58 +02:00
Kévin Dunglas
cfb9d9f895 chore: bump deps (#1665) 2025-06-20 17:36:05 +02:00
Kévin Dunglas
5c69109011 chore: remove SPC_PHP_DEFAULT_OPTIMIZE_CFLAGS which doesn't exist annymore (#1669)
* chore(static): remove SPC_PHP_DEFAULT_OPTIMIZE_CFLAGS, which doesn't exist anymore

* dont use pre-built packages on ARM

* CS
2025-06-20 17:35:30 +02:00
Kévin Dunglas
12f469e701 chore: bump deps (#1643) 2025-06-19 19:10:59 +02:00
Max
71aebbe0e7 perf: add popular proxy headers (#1661)
* perf: add popular proxy headers

* X-Real-IP => X-Real-Ip
2025-06-19 14:05:26 +02:00
Marc
34a0255c15 fix: fix php-server command bug when built with nobrotli tag 2025-06-18 09:43:54 +02:00
Marc
9bd314d2fb feat(static): add HTTP/3 support for curl, add amqp and lz4 extensions (#1631)
* add amqp, ast, brotli, lz and xz extensions

* brotli and xz are not released yet

* retrigger CI
2025-06-10 10:51:41 +02:00
Rob Landers
3afb709f02 link directly to try_files in performance docs (#1633) 2025-06-09 16:57:48 +02:00
Alexander Stecher
82aeb128bc refactor: split caddy.go (#1629)
* Splits modules.

* trigger build

---------

Co-authored-by: Alliballibaba <alliballibaba@gmail.com>
2025-06-07 11:09:41 +02:00
162 changed files with 15003 additions and 1683 deletions

View File

@@ -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:

View File

@@ -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.

View File

@@ -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/

View File

@@ -1,5 +1,8 @@
---
name: Lint Code Base
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on:
pull_request:
branches:

View File

@@ -1,5 +1,8 @@
---
name: Sanitizers
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on:
pull_request:
branches:

View File

@@ -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

View File

@@ -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 ./...

View File

@@ -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 && \

View File

@@ -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/)

View File

@@ -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.

View File

@@ -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 && \

View File

@@ -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.

View File

@@ -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
View 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
}

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -1,4 +1,4 @@
package caddy
package caddy
import (
"testing"

53
caddy/extinit.go Normal file
View 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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
View 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
View 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")
}

View File

@@ -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
View 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
View File

@@ -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
View 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"

View File

@@ -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

View File

@@ -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" ]

View File

@@ -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" ]

View File

@@ -116,6 +116,7 @@ target "default" {
args = {
FRANKENPHP_VERSION = VERSION
}
secret = ["id=github-token,env=GITHUB_TOKEN"]
}
target "static-builder-musl" {

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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 libcAlpine 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\'"'`
> (根据的应用需求更改堆栈大小)。
> (根据的应用需求更改堆栈大小)。

View File

@@ -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>

View File

@@ -77,7 +77,7 @@ FrankenPHP 提供的 `builder` 镜像包含 libphp 的编译版本。
> [!TIP]
>
> 如果你的系统基于 musl libcAlpine Linux 上默认使用)并搭配 Symfony 使用,
> 可能需要 [增加默认堆栈大小](compile.md#使用-xcaddy)。
> 可能需要 [增加默认堆栈大小](compile.md#使用-xcaddy)。
## 默认启用 worker 模式

View File

@@ -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

View File

@@ -1,7 +1,7 @@
# 使用 GitHub Actions
此存储库构建 Docker 镜像并将其部署到 [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) 上
每个批准的拉取请求或设置后在自己的分支上。
每个批准的拉取请求或设置后在自己的分支上。
## 设置 GitHub Actions

View File

@@ -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 以尝试找出问题:

View File

@@ -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

View File

@@ -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 框架来使用)。

View File

@@ -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”部分中选择一个计划来满足的需求。
![使用 Docker 在 DigitalOcean 上部署 FrankenPHP](../digitalocean-droplet.png)
可以保留其他设置的默认值,也可以根据需要进行调整。
不要忘记添加的 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]

View File

@@ -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
```

View File

@@ -96,7 +96,7 @@ docker run \
```
默认情况下,每个 CPU 启动一个 worker。
还可以配置要启动的 worker 数:
还可以配置要启动的 worker 数:
```console
docker run \

View File

@@ -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
```

View File

@@ -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

View File

@@ -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
View 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.

View File

@@ -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)

View File

@@ -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`.

View File

@@ -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
```

View File

@@ -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`

View File

@@ -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
View 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.

View File

@@ -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.

View File

@@ -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"

View File

@@ -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
```

View File

@@ -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`...)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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"

View File

@@ -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

View File

@@ -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
```

View File

@@ -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>

View File

@@ -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
```

View File

@@ -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 пока не поддерживает.

View File

@@ -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
```

View File

@@ -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
```

View File

@@ -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

View File

@@ -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
```

View File

@@ -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>

View File

@@ -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 \

View File

@@ -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.

View File

@@ -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

View File

@@ -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
```

View File

@@ -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
View 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
})
}

View File

@@ -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;
}

View File

@@ -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))

View File

@@ -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

View File

@@ -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
View File

@@ -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
View 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
View File

@@ -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=

View File

@@ -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}"

View 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
View 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
}

View 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")
}

View 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)
}
}

View 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")
}

View 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
}

View 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")
}
})
}
}

View 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")
}

View 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
}

View 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
View 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
}

View 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
View 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)
}

View 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