Full Code of php/frankenphp for AI

main 33fcc4d5c08a cached
457 files
2.0 MB
555.3k tokens
1128 symbols
1 requests
Download .txt
Showing preview only (2,214K chars total). Download the full file or copy to clipboard to get everything.
Repository: php/frankenphp
Branch: main
Commit: 33fcc4d5c08a
Files: 457
Total size: 2.0 MB

Directory structure:
gitextract_hy3ir2xo/

├── .clang-format-ignore
├── .codespellrc
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yaml
│   │   └── feature_request.yaml
│   ├── actions/
│   │   └── watcher/
│   │       └── action.yaml
│   ├── dependabot.yaml
│   ├── scripts/
│   │   ├── docker-compute-fingerprints.sh
│   │   └── docker-verify-fingerprints.sh
│   └── workflows/
│       ├── docker.yaml
│       ├── docs.yaml
│       ├── lint.yaml
│       ├── sanitizers.yaml
│       ├── static.yaml
│       ├── tests.yaml
│       ├── translate.yaml
│       ├── windows.yaml
│       └── wrap-issue-details.yaml
├── .gitignore
├── .gitleaksignore
├── .golangci.yaml
├── .hadolint.yaml
├── .markdown-lint.yaml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── SECURITY.md
├── alpine.Dockerfile
├── app_checksum.txt
├── build-static.sh
├── caddy/
│   ├── admin.go
│   ├── admin_test.go
│   ├── app.go
│   ├── br-skip.go
│   ├── br.go
│   ├── caddy.go
│   ├── caddy_test.go
│   ├── config_test.go
│   ├── extinit.go
│   ├── frankenphp/
│   │   ├── Caddyfile
│   │   ├── cbrotli.go
│   │   └── main.go
│   ├── go.mod
│   ├── go.sum
│   ├── hotreload-skip.go
│   ├── hotreload.go
│   ├── hotreload_test.go
│   ├── mercure-skip.go
│   ├── mercure.go
│   ├── module.go
│   ├── module_test.go
│   ├── php-cli.go
│   ├── php-server.go
│   ├── watcher_test.go
│   └── workerconfig.go
├── cgi.go
├── cgi_test.go
├── cgo.go
├── cli.go
├── cli_test.go
├── context.go
├── debugstate.go
├── dev-alpine.Dockerfile
├── dev.Dockerfile
├── docker-bake.hcl
├── docs/
│   ├── classic.md
│   ├── cn/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── classic.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── extensions.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── mercure.md
│   │   ├── metrics.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   ├── worker.md
│   │   └── x-sendfile.md
│   ├── compile.md
│   ├── config.md
│   ├── docker.md
│   ├── early-hints.md
│   ├── embed.md
│   ├── es/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── classic.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── extensions.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── logging.md
│   │   ├── mercure.md
│   │   ├── metrics.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   ├── wordpress.md
│   │   ├── worker.md
│   │   └── x-sendfile.md
│   ├── extension-workers.md
│   ├── extensions.md
│   ├── fr/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── classic.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── extensions.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── mercure.md
│   │   ├── metrics.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   ├── worker.md
│   │   └── x-sendfile.md
│   ├── github-actions.md
│   ├── hot-reload.md
│   ├── ja/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── classic.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── extensions.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── mercure.md
│   │   ├── metrics.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   ├── worker.md
│   │   └── x-sendfile.md
│   ├── known-issues.md
│   ├── laravel.md
│   ├── logging.md
│   ├── mercure.md
│   ├── metrics.md
│   ├── performance.md
│   ├── production.md
│   ├── pt-br/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── classic.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── extensions.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── mercure.md
│   │   ├── metrics.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   ├── worker.md
│   │   └── x-sendfile.md
│   ├── ru/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── mercure.md
│   │   ├── metrics.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   └── worker.md
│   ├── static.md
│   ├── tr/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── mercure.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   └── worker.md
│   ├── translate.php
│   ├── wordpress.md
│   ├── worker.md
│   └── x-sendfile.md
├── embed.go
├── env.go
├── ext.go
├── frankenphp.c
├── frankenphp.go
├── frankenphp.h
├── frankenphp.stub.php
├── frankenphp_arginfo.h
├── frankenphp_test.go
├── go.mod
├── go.sh
├── go.sum
├── hotreload.go
├── install.ps1
├── install.sh
├── internal/
│   ├── cpu/
│   │   ├── cpu_unix.go
│   │   └── cpu_windows.go
│   ├── extgen/
│   │   ├── arginfo.go
│   │   ├── cfile.go
│   │   ├── cfile_namespace_test.go
│   │   ├── cfile_phpmethod_test.go
│   │   ├── cfile_test.go
│   │   ├── classparser.go
│   │   ├── classparser_test.go
│   │   ├── constants_test.go
│   │   ├── constparser.go
│   │   ├── constparser_test.go
│   │   ├── docs.go
│   │   ├── docs_test.go
│   │   ├── errors.go
│   │   ├── funcparser.go
│   │   ├── funcparser_test.go
│   │   ├── generator.go
│   │   ├── gofile.go
│   │   ├── gofile_test.go
│   │   ├── hfile.go
│   │   ├── hfile_test.go
│   │   ├── integration_test.go
│   │   ├── namespace_test.go
│   │   ├── nodes.go
│   │   ├── nsparser.go
│   │   ├── paramparser.go
│   │   ├── paramparser_test.go
│   │   ├── parser.go
│   │   ├── phpfunc.go
│   │   ├── phpfunc_namespace_test.go
│   │   ├── phpfunc_test.go
│   │   ├── srcanalyzer.go
│   │   ├── srcanalyzer_test.go
│   │   ├── stub.go
│   │   ├── stub_test.go
│   │   ├── templates/
│   │   │   ├── README.md.tpl
│   │   │   ├── extension.c.tpl
│   │   │   ├── extension.go.tpl
│   │   │   ├── extension.h.tpl
│   │   │   └── stub.php.tpl
│   │   ├── utils.go
│   │   ├── utils_namespace_test.go
│   │   ├── utils_test.go
│   │   ├── validator.go
│   │   └── validator_test.go
│   ├── fastabs/
│   │   ├── filepath.go
│   │   └── filepath_unix.go
│   ├── memory/
│   │   ├── memory_linux.go
│   │   └── memory_others.go
│   ├── phpheaders/
│   │   ├── phpheaders.go
│   │   └── phpheaders_test.go
│   ├── state/
│   │   ├── state.go
│   │   └── state_test.go
│   ├── testcli/
│   │   └── main.go
│   ├── testext/
│   │   ├── ext_test.go
│   │   ├── extension.h
│   │   ├── extensions.c
│   │   ├── exttest.go
│   │   └── testdata/
│   │       └── index.php
│   ├── testserver/
│   │   └── main.go
│   └── watcher/
│       ├── pattern.go
│       ├── pattern_test.go
│       └── watcher.go
├── log_test.go
├── mercure-skip.go
├── mercure.go
├── mercure_test.go
├── metrics.go
├── metrics_test.go
├── options.go
├── package/
│   ├── Caddyfile
│   ├── alpine/
│   │   ├── frankenphp.openrc
│   │   ├── post-deinstall.sh
│   │   ├── post-install.sh
│   │   └── pre-deinstall.sh
│   ├── content/
│   │   └── index.php
│   ├── debian/
│   │   ├── frankenphp.service
│   │   ├── postinst.sh
│   │   ├── postrm.sh
│   │   └── prerm.sh
│   └── rhel/
│       ├── frankenphp.service
│       ├── postinstall.sh
│       ├── postuninstall.sh
│       ├── preinstall.sh
│       └── preuninstall.sh
├── phpmainthread.go
├── phpmainthread_test.go
├── phpthread.go
├── recorder_test.go
├── release.sh
├── reload_test.sh
├── requestoptions.go
├── requestoptions_test.go
├── scaling.go
├── scaling_test.go
├── static-builder-gnu.Dockerfile
├── static-builder-musl.Dockerfile
├── testdata/
│   ├── Caddyfile
│   ├── _executor.php
│   ├── autoloader-require.php
│   ├── autoloader.php
│   ├── benchmark.Caddyfile
│   ├── command.php
│   ├── connection_status.php
│   ├── cookies.php
│   ├── dd.php
│   ├── die.php
│   ├── dirindex/
│   │   └── index.php
│   ├── early-hints.php
│   ├── echo.php
│   ├── env/
│   │   ├── env.php
│   │   ├── import-env.php
│   │   ├── overwrite-env.php
│   │   ├── putenv.php
│   │   ├── remember-env.php
│   │   └── test-env.php
│   ├── exception.php
│   ├── failing-worker.php
│   ├── fiber-basic.php
│   ├── fiber-no-cgo.php
│   ├── file-stream.php
│   ├── file-stream.txt
│   ├── file-upload.php
│   ├── files/
│   │   ├── .gitignore
│   │   └── static.txt
│   ├── finish-request.php
│   ├── flush.php
│   ├── headers.php
│   ├── hello.php
│   ├── hello.txt
│   ├── index.php
│   ├── ini.php
│   ├── input.php
│   ├── integration/
│   │   ├── basic_function.go
│   │   ├── callable.go
│   │   ├── class_methods.go
│   │   ├── constants.go
│   │   ├── invalid_signature.go
│   │   ├── namespace.go
│   │   └── type_mismatch.go
│   ├── large-request.php
│   ├── large-response.php
│   ├── load-test.js
│   ├── log-error_log.php
│   ├── log-frankenphp_log.php
│   ├── mercure-publish.php
│   ├── message-worker.php
│   ├── non-worker.php
│   ├── only-headers.php
│   ├── performance/
│   │   ├── api.js
│   │   ├── computation.js
│   │   ├── database.js
│   │   ├── flamegraph.sh
│   │   ├── hanging-requests.js
│   │   ├── hello-world.js
│   │   ├── k6.Caddyfile
│   │   ├── perf-test.sh
│   │   ├── performance-testing.md
│   │   ├── start-server.sh
│   │   └── timeouts.js
│   ├── persistent-object-require.php
│   ├── persistent-object.php
│   ├── phpinfo.php
│   ├── preload-check.php
│   ├── preload.php
│   ├── request-headers.php
│   ├── request-superglobal-conditional-include.php
│   ├── request-superglobal-conditional.php
│   ├── request-superglobal.php
│   ├── response-headers.php
│   ├── server-all-vars-ordered.php
│   ├── server-all-vars-ordered.txt
│   ├── server-variable.php
│   ├── session-handler.php
│   ├── session-leak.php
│   ├── session.php
│   ├── sleep.php
│   ├── super-globals.php
│   ├── symlinks/
│   │   └── test/
│   │       ├── document-root.php
│   │       ├── index.php
│   │       └── nested/
│   │           └── index.php
│   ├── timeout.php
│   ├── transition-regular.php
│   ├── transition-worker-1.php
│   ├── transition-worker-2.php
│   ├── worker-env.php
│   ├── worker-getopt.php
│   ├── worker-restart.php
│   ├── worker-with-counter.php
│   ├── worker-with-env.php
│   ├── worker-with-session-handler.php
│   └── worker.php
├── threadinactive.go
├── threadregular.go
├── threadtasks_test.go
├── threadworker.go
├── types.c
├── types.go
├── types.h
├── types_test.go
├── vcpkg.json
├── watcher-skip.go
├── watcher.go
├── watcher_test.go
├── worker.go
├── worker_test.go
├── workerextension.go
├── workerextension_test.go
└── zizmor.yaml

================================================
FILE CONTENTS
================================================

================================================
FILE: .clang-format-ignore
================================================
frankenphp_arginfo.h


================================================
FILE: .codespellrc
================================================
[codespell]
check-hidden =
skip = .git,docs/*/*,docs,*/go.mod,*/go.sum,./internal/phpheaders/phpheaders.go


================================================
FILE: .dockerignore
================================================
/caddy/frankenphp/frankenphp
/internal/testserver/testserver
/internal/testcli/testcli
/dist
.DS_Store
.idea/
.vscode/
__debug_bin
frankenphp.test
caddy/frankenphp/Build
*.log


================================================
FILE: .editorconfig
================================================
root = true

[*]
end_of_line = lf
insert_final_newline = true

[*.{sh,Dockerfile}]
indent_style = tab
tab_width = 4

[*.{yaml,yml}]
indent_style = space
tab_width = 2


================================================
FILE: .gitattributes
================================================
* text=auto eol=lf


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yaml
================================================
---
name: Bug Report
description: File a bug report
labels: [bug]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to fill out this bug report!

        Before submitting, please ensure that your issue:

        * Is not [a known issue](https://frankenphp.dev/docs/known-issues/).
        * Has not [already been reported](https://github.com/php/frankenphp/issues).
        * Is not caused by a dependency (like Caddy or PHP itself). If the issue is with a dependency, please report it to the upstream project directly.
  - type: textarea
    id: what-happened
    attributes:
      label: What happened?
      description: |
        Tell us what you do, what you get, and what you expected.
        Provide us with some step-by-step instructions to reproduce the issue.
    validations:
      required: true
  - type: dropdown
    id: build
    attributes:
      label: Build Type
      description: What build of FrankenPHP do you use?
      options:
        - Docker (Debian Trixie)
        - Docker (Debian Bookworm)
        - Docker (Alpine)
        - apk packages
        - deb packages
        - RPM packages
        - Static binary
        - Custom (tell us more in the description)
      default: 0
    validations:
      required: true
  - type: dropdown
    id: worker
    attributes:
      label: Worker Mode
      description: Does the problem happen only when using the worker mode?
      options:
        - "Yes"
        - "No"
      default: 0
    validations:
      required: true
  - type: dropdown
    id: os
    attributes:
      label: Operating System
      description: What operating system are you executing FrankenPHP with?
      options:
        - GNU/Linux
        - macOS
        - Windows
        - FreeBSD
        - Other (tell us more in the description)
      default: 0
    validations:
      required: true
  - type: dropdown
    id: arch
    attributes:
      label: CPU Architecture
      description: What CPU architecture are you using?
      options:
        - x86_64
        - Apple Silicon
        - x86
        - aarch64
        - Other (tell us more in the description)
      default: 0
  - type: textarea
    id: php
    attributes:
      label: PHP configuration
      description: |
        Please copy and paste the output of the `phpinfo()` function -- remember to remove **sensitive information** like passwords, API keys, etc.
      render: shell
    validations:
      required: true
  - type: textarea
    id: logs
    attributes:
      label: Relevant log output
      description: |
        Please copy and paste any relevant log output.
        This will be automatically formatted into code,
        so no need for backticks.
      render: shell


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yaml
================================================
---
name: Feature Request
description: Suggest an idea for this project
labels: [enhancement]
body:
  - type: textarea
    id: description
    attributes:
      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.
        Ex. I'm always frustrated when [...]

        **Describe the solution you'd like**
        A clear and concise description of what you want to happen.

        **Describe alternatives you've considered**
        A clear and concise description of any alternative solutions
        or features you've considered.


================================================
FILE: .github/actions/watcher/action.yaml
================================================
name: watcher
description: Install e-dant/watcher
runs:
  using: composite
  steps:
    - name: Determine e-dant/watcher version
      id: determine-watcher-version
      run: echo version="$(gh release view --repo e-dant/watcher --json tagName --template '{{ .tagName }}')" >> "${GITHUB_OUTPUT}"
      shell: bash
      env:
        GH_TOKEN: ${{ github.token }}
    - name: Cache e-dant/watcher
      id: cache-watcher
      uses: actions/cache@v4
      with:
        path: watcher/target
        key: watcher-${{ runner.os }}-${{ runner.arch }}-${{ steps.determine-watcher-version.outputs.version }}-${{ env.CC && env.CC || 'gcc' }}
    - if: steps.cache-watcher.outputs.cache-hit != 'true'
      name: Compile e-dant/watcher
      run: |
        mkdir watcher
        gh release download --repo e-dant/watcher -A tar.gz -O - | tar -xz -C watcher --strip-components 1
        cd watcher
        cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
        cmake --build build
        sudo cmake --install build --prefix target
      shell: bash
      env:
        GH_TOKEN: ${{ github.token }}
    - name: Update LD_LIBRARY_PATH
      run: |
        sudo sh -c "echo ${PWD}/watcher/target/lib > /etc/ld.so.conf.d/watcher.conf"
        sudo ldconfig
      shell: bash


================================================
FILE: .github/dependabot.yaml
================================================
---
version: 2
updates:
  - package-ecosystem: gomod
    directory: /
    schedule:
      interval: weekly
    commit-message:
      prefix: chore
    groups:
      go-modules:
        patterns:
          - "*"
    cooldown:
      default-days: 7
  - package-ecosystem: gomod
    directory: /caddy
    schedule:
      interval: weekly
    commit-message:
      prefix: chore(caddy)
    groups:
      go-modules:
        patterns:
          - "*"
    cooldown:
      default-days: 7
  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly
    commit-message:
      prefix: ci
    groups:
      github-actions:
        patterns:
          - "*"
    cooldown:
      default-days: 7


================================================
FILE: .github/scripts/docker-compute-fingerprints.sh
================================================
#!/usr/bin/env bash
set -euo pipefail

write_output() {
	if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
		echo "$1" >>"${GITHUB_OUTPUT}"
	else
		echo "$1"
	fi
}

get_php_version() {
	local version="$1"
	skopeo inspect "docker://docker.io/library/php:${version}" \
		--override-os linux \
		--override-arch amd64 |
		jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")'
}

PHP_82_LATEST="$(get_php_version 8.2)"
PHP_83_LATEST="$(get_php_version 8.3)"
PHP_84_LATEST="$(get_php_version 8.4)"
PHP_85_LATEST="$(get_php_version 8.5)"

PHP_VERSION="${PHP_82_LATEST},${PHP_83_LATEST},${PHP_84_LATEST},${PHP_85_LATEST}"
write_output "php_version=${PHP_VERSION}"
write_output "php82_version=${PHP_82_LATEST//./-}"
write_output "php83_version=${PHP_83_LATEST//./-}"
write_output "php84_version=${PHP_84_LATEST//./-}"
write_output "php85_version=${PHP_85_LATEST//./-}"

if [[ "${GITHUB_EVENT_NAME:-}" == "schedule" ]]; then
	FRANKENPHP_LATEST_TAG="$(gh release view --repo php/frankenphp --json tagName --jq '.tagName')"
	git checkout "${FRANKENPHP_LATEST_TAG}"
fi

METADATA="$(PHP_VERSION="${PHP_VERSION}" docker buildx bake --print | jq -c)"

BASE_IMAGES=()
while IFS= read -r image; do
	BASE_IMAGES+=("${image}")
done < <(jq -r '
	.target[]?.contexts? | to_entries[]?
	| select(.value | startswith("docker-image://"))
	| .value
	| sub("^docker-image://"; "")
' <<<"${METADATA}" | sort -u)

BASE_IMAGE_DIGESTS=()
for image in "${BASE_IMAGES[@]}"; do
	if [[ "${image}" == */* ]]; then
		ref="docker://docker.io/${image}"
	else
		ref="docker://docker.io/library/${image}"
	fi
	digest="$(skopeo inspect "${ref}" \
		--override-os linux \
		--override-arch amd64 \
		--format '{{.Digest}}')"
	BASE_IMAGE_DIGESTS+=("${image}@${digest}")
done

BASE_FINGERPRINT="$(printf '%s\n' "${BASE_IMAGE_DIGESTS[@]}" | sort | sha256sum | awk '{print $1}')"
write_output "base_fingerprint=${BASE_FINGERPRINT}"

if [[ "${GITHUB_EVENT_NAME:-}" != "schedule" ]]; then
	write_output "skip=false"
	exit 0
fi

FRANKENPHP_LATEST_TAG_NO_PREFIX="${FRANKENPHP_LATEST_TAG#v}"
EXISTING_FINGERPRINT=$(
	skopeo inspect "docker://docker.io/dunglas/frankenphp:${FRANKENPHP_LATEST_TAG_NO_PREFIX}" \
		--override-os linux \
		--override-arch amd64 |
		jq -r '.Labels["dev.frankenphp.base.fingerprint"] // empty'
)

if [[ -n "${EXISTING_FINGERPRINT}" ]] && [[ "${EXISTING_FINGERPRINT}" == "${BASE_FINGERPRINT}" ]]; then
	write_output "skip=true"
	exit 0
fi

write_output "ref=${FRANKENPHP_LATEST_TAG}"
write_output "skip=false"


================================================
FILE: .github/scripts/docker-verify-fingerprints.sh
================================================
#!/usr/bin/env bash
set -euo pipefail

PHP_VERSION="${PHP_VERSION:-}"
GO_VERSION="${GO_VERSION:-}"
USE_LATEST_PHP="${USE_LATEST_PHP:-0}"

if [[ -z "${GO_VERSION}" ]]; then
	GO_VERSION="$(awk -F'"' '/variable "GO_VERSION"/ {f=1} f && /default/ {print $2; exit}' docker-bake.hcl)"
	GO_VERSION="${GO_VERSION:-1.26}"
fi

if [[ -z "${PHP_VERSION}" ]]; then
	PHP_VERSION="$(awk -F'"' '/variable "PHP_VERSION"/ {f=1} f && /default/ {print $2; exit}' docker-bake.hcl)"
	PHP_VERSION="${PHP_VERSION:-8.2,8.3,8.4,8.5}"
fi

if [[ "${USE_LATEST_PHP}" == "1" ]]; then
	PHP_82_LATEST=$(skopeo inspect docker://docker.io/library/php:8.2 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
	PHP_83_LATEST=$(skopeo inspect docker://docker.io/library/php:8.3 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
	PHP_84_LATEST=$(skopeo inspect docker://docker.io/library/php:8.4 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
	PHP_85_LATEST=$(skopeo inspect docker://docker.io/library/php:8.5 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
	PHP_VERSION="${PHP_82_LATEST},${PHP_83_LATEST},${PHP_84_LATEST},${PHP_85_LATEST}"
fi

OS_LIST=()
while IFS= read -r os; do
	OS_LIST+=("${os}")
done < <(
	python3 - <<'PY'
import re

with open("docker-bake.hcl", "r", encoding="utf-8") as f:
	data = f.read()

# Find the first "os = [ ... ]" block and extract quoted values
m = re.search(r'os\s*=\s*\[(.*?)\]', data, re.S)
if not m:
	raise SystemExit(1)

vals = re.findall(r'"([^"]+)"', m.group(1))
for v in vals:
	print(v)
PY
)

IFS=',' read -r -a PHP_VERSIONS <<<"${PHP_VERSION}"

BASE_IMAGES=()
for os in "${OS_LIST[@]}"; do
	BASE_IMAGES+=("golang:${GO_VERSION}-${os}")
	for pv in "${PHP_VERSIONS[@]}"; do
		BASE_IMAGES+=("php:${pv}-zts-${os}")
	done
done

mapfile -t BASE_IMAGES < <(printf '%s\n' "${BASE_IMAGES[@]}" | sort -u)

BASE_IMAGE_DIGESTS=()
for image in "${BASE_IMAGES[@]}"; do
	if [[ "${image}" == */* ]]; then
		ref="docker://docker.io/${image}"
	else
		ref="docker://docker.io/library/${image}"
	fi
	digest="$(skopeo inspect "${ref}" --override-os linux --override-arch amd64 --format '{{.Digest}}')"
	BASE_IMAGE_DIGESTS+=("${image}@${digest}")
done

hash_cmd="sha256sum"
if ! command -v "${hash_cmd}" >/dev/null 2>&1; then
	hash_cmd="shasum -a 256"
fi

fingerprint="$(printf '%s\n' "${BASE_IMAGE_DIGESTS[@]}" | sort | ${hash_cmd} | awk '{print $1}')"

echo "PHP_VERSION=${PHP_VERSION}"
echo "GO_VERSION=${GO_VERSION}"
echo "OS_LIST=${OS_LIST[*]}"
echo "Base images:"
printf '  %s\n' "${BASE_IMAGES[@]}"
echo "Fingerprint: ${fingerprint}"


================================================
FILE: .github/workflows/docker.yaml
================================================
---
name: Build Docker Images
concurrency:
  cancel-in-progress: true
  group: ${{ github.workflow }}-${{ github.ref }}
on:
  pull_request:
    branches:
      - main
    paths:
      - "docker-bake.hcl"
      - ".github/workflows/docker.yaml"
      - "**cgo.go"
      - "**Dockerfile"
      - "**.c"
      - "**.h"
      - "**.sh"
      - "**.stub.php"
  push:
    branches:
      - main
    tags:
      - v*.*.*
  workflow_dispatch:
    inputs:
      #checkov:skip=CKV_GHA_7
      version:
        description: "FrankenPHP version"
        required: false
        type: string
  schedule:
    - cron: "0 4 * * *"
permissions:
  contents: read
env:
  IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
jobs:
  prepare:
    runs-on: ubuntu-24.04
    outputs:
      # Push if it's a scheduled job, a tag, or if we're committing to the main branch
      push: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false }}
      variants: ${{ steps.matrix.outputs.variants }}
      platforms: ${{ steps.matrix.outputs.platforms }}
      metadata: ${{ steps.matrix.outputs.metadata }}
      php_version: ${{ steps.check.outputs.php_version }}
      php82_version: ${{ steps.check.outputs.php82_version }}
      php83_version: ${{ steps.check.outputs.php83_version }}
      php84_version: ${{ steps.check.outputs.php84_version }}
      php85_version: ${{ steps.check.outputs.php85_version }}
      skip: ${{ steps.check.outputs.skip }}
      ref: ${{ steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}
      base_fingerprint: ${{ steps.check.outputs.base_fingerprint }}
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
      - name: Check PHP versions and base image fingerprint
        id: check
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: ./.github/scripts/docker-compute-fingerprints.sh
      - name: Create variants matrix
        if: ${{ !fromJson(steps.check.outputs.skip) }}
        id: matrix
        shell: bash
        run: |
          set -e
          METADATA="$(docker buildx bake --print | jq -c)"
          {
            echo metadata="${METADATA}"
            echo variants="$(jq -c '.group.default.targets|map(sub("runner-|builder-"; ""))|unique' <<< "${METADATA}")"
            echo platforms="$(jq -c 'first(.target[]) | .platforms' <<< "${METADATA}")"
          } >> "${GITHUB_OUTPUT}"
        env:
          SHA: ${{ github.sha }}
          VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || steps.check.outputs.ref || 'dev' }}
          PHP_VERSION: ${{ steps.check.outputs.php_version }}
  build:
    runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
    needs:
      - prepare
    if: ${{ !fromJson(needs.prepare.outputs.skip) }}
    strategy:
      fail-fast: false
      matrix:
        variant: ${{ fromJson(needs.prepare.outputs.variants) }}
        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
        include:
          - race: ""
          - platform: linux/amd64
            race: "-race" # The Go race detector is only supported on amd64
        exclude:
          # arm/v6 is only available for Alpine: https://github.com/docker-library/golang/issues/502
          - variant: php-${{ needs.prepare.outputs.php82_version }}-trixie
            platform: linux/arm/v6
          - variant: php-${{ needs.prepare.outputs.php83_version }}-trixie
            platform: linux/arm/v6
          - variant: php-${{ needs.prepare.outputs.php84_version }}-trixie
            platform: linux/arm/v6
          - variant: php-${{ needs.prepare.outputs.php85_version }}-trixie
            platform: linux/arm/v6
          - variant: php-${{ needs.prepare.outputs.php82_version }}-bookworm
            platform: linux/arm/v6
          - variant: php-${{ needs.prepare.outputs.php83_version }}-bookworm
            platform: linux/arm/v6
          - variant: php-${{ needs.prepare.outputs.php84_version }}-bookworm
            platform: linux/arm/v6
          - variant: php-${{ needs.prepare.outputs.php85_version }}-bookworm
            platform: linux/arm/v6
    steps:
      - name: Prepare
        id: prepare
        run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
        env:
          PLATFORM: ${{ matrix.platform }}
      - uses: actions/checkout@v6
        with:
          ref: ${{ needs.prepare.outputs.ref }}
          persist-credentials: false
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
        with:
          platforms: ${{ matrix.platform }}
      - name: Login to DockerHub
        uses: docker/login-action@v4
        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
        with:
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      - name: Build
        id: build
        uses: docker/bake-action@v7
        with:
          pull: true
          load: ${{ !fromJson(needs.prepare.outputs.push) }}
          source: .
          targets: |
            builder-${{ matrix.variant }}
            runner-${{ matrix.variant }}
          # Remove tags to prevent "can't push tagged ref [...] by digest" error
          set: |
            ${{ (github.event_name == 'pull_request') && '*.args.NO_COMPRESS=1' || '' }}
            *.tags=
            *.platform=${{ matrix.platform }}
            ${{ fromJson(needs.prepare.outputs.push) && '' || format('builder-{0}.cache-from=type=gha,scope=builder-{0}-{1}-{2}', matrix.variant, needs.prepare.outputs.ref || github.ref, matrix.platform) }}
            ${{ fromJson(needs.prepare.outputs.push) && '' || format('builder-{0}.cache-from=type=gha,scope=refs/heads/main-builder-{0}-{1}', matrix.variant, matrix.platform) }}
            ${{ fromJson(needs.prepare.outputs.push) && '' || format('builder-{0}.cache-to=type=gha,scope=builder-{0}-{1}-{2},ignore-error=true', matrix.variant, needs.prepare.outputs.ref || github.ref, matrix.platform) }}
            ${{ fromJson(needs.prepare.outputs.push) && '' || format('runner-{0}.cache-from=type=gha,scope=runner-{0}-{1}-{2}', matrix.variant, needs.prepare.outputs.ref || github.ref, matrix.platform) }}
            ${{ fromJson(needs.prepare.outputs.push) && '' || format('runner-{0}.cache-from=type=gha,scope=refs/heads/main-runner-{0}-{1}', matrix.variant, matrix.platform) }}
            ${{ fromJson(needs.prepare.outputs.push) && '' || format('runner-{0}.cache-to=type=gha,scope=runner-{0}-{1}-{2},ignore-error=true', matrix.variant, needs.prepare.outputs.ref || github.ref, matrix.platform) }}
            ${{ fromJson(needs.prepare.outputs.push) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
        env:
          SHA: ${{ github.sha }}
          VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || 'dev' }}
          PHP_VERSION: ${{ needs.prepare.outputs.php_version }}
          BASE_FINGERPRINT: ${{ needs.prepare.outputs.base_fingerprint }}
      - # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
        name: Export metadata
        if: fromJson(needs.prepare.outputs.push)
        run: |
          mkdir -p /tmp/metadata/builder /tmp/metadata/runner

          builderDigest=$(jq -r ".\"builder-${VARIANT}\".\"containerimage.digest\"" <<< "${METADATA}")
          touch "/tmp/metadata/builder/${builderDigest#sha256:}"

          runnerDigest=$(jq -r ".\"runner-${VARIANT}\".\"containerimage.digest\"" <<< "${METADATA}")
          touch "/tmp/metadata/runner/${runnerDigest#sha256:}"
        env:
          METADATA: ${{ steps.build.outputs.metadata }}
          VARIANT: ${{ matrix.variant }}
      - name: Upload builder metadata
        if: fromJson(needs.prepare.outputs.push)
        uses: actions/upload-artifact@v7
        with:
          name: metadata-builder-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}
          path: /tmp/metadata/builder/*
          if-no-files-found: error
          retention-days: 1
      - name: Upload runner metadata
        if: fromJson(needs.prepare.outputs.push)
        uses: actions/upload-artifact@v7
        with:
          name: metadata-runner-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}
          path: /tmp/metadata/runner/*
          if-no-files-found: error
          retention-days: 1
      - name: Run tests
        if: ${{ !fromJson(needs.prepare.outputs.push) }}
        run: |
          # TODO: remove "containerimage.config.digest" fallback once all runners use buildx v0.18+
          # which replaced it with "containerimage.digest" and "containerimage.descriptor"
          docker run --platform="${PLATFORM}" --rm \
            "$(jq -r ".\"builder-${VARIANT}\" | .\"containerimage.config.digest\" // .\"containerimage.digest\"" <<< "${METADATA}")" \
            sh -c "./go.sh test ${RACE} -v $(./go.sh list ./... | grep -v github.com/dunglas/frankenphp/internal/testext | grep -v github.com/dunglas/frankenphp/internal/extgen | tr '\n' ' ') && cd caddy && ../go.sh test ${RACE} -v ./..."
        env:
          METADATA: ${{ steps.build.outputs.metadata }}
          PLATFORM: ${{ matrix.platform }}
          VARIANT: ${{ matrix.variant }}
          RACE: ${{ matrix.race }}

  # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
  push:
    runs-on: ubuntu-24.04
    needs:
      - prepare
      - build
    if: fromJson(needs.prepare.outputs.push)
    strategy:
      fail-fast: false
      matrix:
        variant: ${{ fromJson(needs.prepare.outputs.variants) }}
        target: ["builder", "runner"]
    steps:
      - name: Download metadata
        uses: actions/download-artifact@v8
        with:
          pattern: metadata-${{ matrix.target }}-${{ matrix.variant }}-*
          path: /tmp/metadata
          merge-multiple: true
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
      - name: Login to DockerHub
        uses: docker/login-action@v4
        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
        with:
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      - name: Create manifest list and push
        working-directory: /tmp/metadata
        run: |
          set -x
          # shellcheck disable=SC2046,SC2086
          docker buildx imagetools create $(jq -cr ".target.\"${TARGET}-${VARIANT}\".tags | map(\"-t \" + .) | join(\" \")" <<< ${METADATA}) \
            $(printf "${IMAGE_NAME}@sha256:%s " *)
        env:
          METADATA: ${{ needs.prepare.outputs.metadata }}
          TARGET: ${{ matrix.target }}
          VARIANT: ${{ matrix.variant }}
      - name: Inspect image
        run: |
          # shellcheck disable=SC2046,SC2086
          docker buildx imagetools inspect $(jq -cr ".target.\"${TARGET}-${VARIANT}\".tags | first" <<< ${METADATA})
        env:
          METADATA: ${{ needs.prepare.outputs.metadata }}
          TARGET: ${{ matrix.target }}
          VARIANT: ${{ matrix.variant }}


================================================
FILE: .github/workflows/docs.yaml
================================================
---
name: Deploy Docs
on:
  push:
    branches:
      - main
    paths:
      - "docs/**"
      - "README.md"
      - "CONTRIBUTING.md"
      - "install.ps1"
      - "install.sh"
permissions: {}
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
jobs:
  deploy:
    runs-on: ubuntu-slim
    steps:
      - name: Trigger website deployment
        env:
          GH_TOKEN: ${{ secrets.WEBSITE_DEPLOY_TOKEN }}
        run: gh api repos/dunglas/frankenphp-website/actions/workflows/hugo.yaml/dispatches -f ref=main


================================================
FILE: .github/workflows/lint.yaml
================================================
---
name: Lint Code Base
concurrency:
  cancel-in-progress: true
  group: ${{ github.workflow }}-${{ github.ref }}
on:
  pull_request:
    branches:
      - main
  push:
    branches:
      - main
permissions:
  contents: read
  packages: read
  statuses: write
jobs:
  build:
    name: Lint Code Base
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v6
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: Lint Code Base
        uses: super-linter/super-linter/slim@v8
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          LINTER_RULES_PATH: /
          MARKDOWN_CONFIG_FILE: .markdown-lint.yaml
          FILTER_REGEX_EXCLUDE: docs/(cn|es|fr|ja|pt-br|ru|tr)/
          VALIDATE_CPP: false
          VALIDATE_JSCPD: false
          VALIDATE_GO: false
          VALIDATE_GO_MODULES: false
          VALIDATE_PHP_PHPCS: false
          VALIDATE_PHP_PHPSTAN: false
          VALIDATE_PHP_PSALM: false
          VALIDATE_TERRAGRUNT: false
          VALIDATE_DOCKERFILE_HADOLINT: false
          VALIDATE_TRIVY: false
          # Prettier, Biome and StandardJS are incompatible
          VALIDATE_JAVASCRIPT_PRETTIER: false
          VALIDATE_TYPESCRIPT_PRETTIER: false
          VALIDATE_BIOME_FORMAT: false
          VALIDATE_BIOME_LINT: false
          # Conflicts with MARKDOWN
          VALIDATE_MARKDOWN_PRETTIER: false
          # To re-enable when https://github.com/super-linter/super-linter/issues/7466 will be closed
          VALIDATE_SPELL_CODESPELL: false


================================================
FILE: .github/workflows/sanitizers.yaml
================================================
---
name: Sanitizers
concurrency:
  cancel-in-progress: true
  group: ${{ github.workflow }}-${{ github.ref }}
on:
  pull_request:
    branches:
      - main
    paths-ignore:
      - "docs/**"
  push:
    branches:
      - main
    paths-ignore:
      - "docs/**"
permissions:
  contents: read
env:
  GOTOOLCHAIN: local
jobs:
  # Adapted from https://github.com/beberlei/hdrhistogram-php
  sanitizers:
    name: ${{ matrix.sanitizer }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        sanitizer: ["asan", "msan"]
    env:
      CFLAGS: -g -O0 -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }} -DZEND_TRACK_ARENA_ALLOC
      LDFLAGS: -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }}
      CC: clang
      CXX: clang++
      USE_ZEND_ALLOC: 0
      LIBRARY_PATH: ${{ github.workspace }}/php/target/lib:${{ github.workspace }}/watcher/target/lib
      LD_LIBRARY_PATH: ${{ github.workspace }}/php/target/lib
      # PHP doesn't free some memory on purpose, we have to disable leaks detection: https://go.dev/doc/go1.26#go-command
      ASAN_OPTIONS: detect_leaks=0
    steps:
      - name: Remove local PHP
        run: sudo apt-get remove --purge --autoremove 'php*' 'libmemcached*'
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - uses: actions/setup-go@v6
        with:
          go-version: "1.26"
          cache-dependency-path: |
            go.sum 
            caddy/go.sum
      - name: Determine PHP version
        id: determine-php-version
        run: |
          curl -fsSL 'https://www.php.net/releases/index.php?json&max=1&version=8.5' -o version.json 2>/dev/null || curl -fsSL 'https://phpmirror.static-php.dev/releases/index.php?json&max=1&version=8.5' -o version.json
          echo version="$(jq -r 'keys[0]' version.json)" >> "$GITHUB_OUTPUT"
          echo archive="$(jq -r '.[] .source[] | select(.filename |endswith(".xz")) | "https://www.php.net/distributions/" + .filename' version.json)" >> "$GITHUB_OUTPUT"
      - name: Cache PHP
        id: cache-php
        uses: actions/cache@v5
        with:
          path: php/target
          key: php-sanitizers-${{ matrix.sanitizer }}-${{ runner.arch }}-${{ steps.determine-php-version.outputs.version }}
      - if: steps.cache-php.outputs.cache-hit != 'true'
        name: Compile PHP
        run: |
          mkdir php/
          MIRROR_URL=${URL/https:\/\/www.php.net/https:\/\/phpmirror.static-php.dev}
          (curl -fsSL "${URL}" || curl -fsSL "${MIRROR_URL}") | tar -Jx -C php --strip-components=1
          cd php/
          ./configure \
            CFLAGS="$CFLAGS" \
            LDFLAGS="$LDFLAGS" \
            --enable-debug \
            --enable-embed \
            --enable-zts \
            --enable-option-checking=fatal \
            --disable-zend-signals \
            --without-sqlite3 \
            --without-pdo-sqlite \
            --without-libxml \
            --disable-dom \
            --disable-simplexml \
            --disable-xml \
            --disable-xmlreader \
            --disable-xmlwriter \
            --without-pcre-jit \
            --disable-opcache-jit \
            --disable-cli \
            --disable-cgi \
            --disable-phpdbg \
            --without-pear \
            --disable-mbregex \
            --enable-werror \
            ${{ matrix.sanitizer == 'msan' && '--enable-memory-sanitizer' || '' }} \
            --prefix="$(pwd)/target/"
          make -j"$(getconf _NPROCESSORS_ONLN)"
          make install
        env:
          URL: ${{ steps.determine-php-version.outputs.archive }}
      - name: Add PHP to the PATH
        run: echo "$(pwd)/php/target/bin" >> "$GITHUB_PATH"
      - name: Install e-dant/watcher
        uses: ./.github/actions/watcher
      - name: Set Set CGO flags
        run: |
          {
            echo "CGO_CFLAGS=$CFLAGS -I${PWD}/watcher/target/include $(php-config --includes)"
            echo "CGO_LDFLAGS=$LDFLAGS $(php-config --ldflags) $(php-config --libs)"
          } >> "$GITHUB_ENV"
      - name: Compile tests
        run: go test ${{ matrix.sanitizer == 'msan' && '-tags=nowatcher' || '' }} -${{ matrix.sanitizer }} -v -x -c
      - name: Run tests
        run: ./frankenphp.test -test.v


================================================
FILE: .github/workflows/static.yaml
================================================
---
name: Build binary releases
concurrency:
  cancel-in-progress: true
  group: ${{ github.workflow }}-${{ github.ref }}

on:
  pull_request:
    branches:
      - main
    paths:
      - "docker-bake.hcl"
      - ".github/workflows/static.yaml"
      - "**cgo.go"
      - "**Dockerfile"
      - "**.c"
      - "**.h"
      - "**.sh"
      - "**.stub.php"
  push:
    branches:
      - main
    tags:
      - v*.*.*
  workflow_dispatch:
    inputs:
      #checkov:skip=CKV_GHA_7
      version:
        description: "FrankenPHP version"
        required: false
        type: string
  schedule:
    - cron: "0 0 * * *"

permissions:
  contents: read

env:
  IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
  SPC_OPT_BUILD_ARGS: --debug
  GOTOOLCHAIN: local

jobs:
  prepare:
    runs-on: ubuntu-24.04
    outputs:
      push: ${{ toJson((steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false) }}
      platforms: ${{ steps.matrix.outputs.platforms }}
      metadata: ${{ steps.matrix.outputs.metadata }}
      gnu_metadata: ${{ steps.matrix.outputs.gnu_metadata }}
      ref: ${{ steps.check.outputs.ref }}
    steps:
      - name: Get version
        id: check
        if: github.event_name == 'schedule'
        run: |
          ref="${REF}"
          if [[ -z "${ref}" ]]; then
            ref="$(gh release view --repo dunglas/frankenphp --json tagName --jq '.tagName')"
          fi

          echo "ref=${ref}" >> "${GITHUB_OUTPUT}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REF: ${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}
      - uses: actions/checkout@v6
        with:
          ref: ${{ steps.check.outputs.ref }}
          persist-credentials: false
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
      - name: Create platforms matrix
        id: matrix
        run: |
          METADATA="$(docker buildx bake --print static-builder-musl | jq -c)"
          GNU_METADATA="$(docker buildx bake --print static-builder-gnu | jq -c)"
          {
            echo metadata="${METADATA}"
            echo platforms="$(jq -c 'first(.target[]) | .platforms' <<< "${METADATA}")"
            echo gnu_metadata="${GNU_METADATA}"
          } >> "${GITHUB_OUTPUT}"
        env:
          SHA: ${{ github.sha }}
          VERSION: ${{ steps.check.outputs.ref || 'dev' }}

  build-linux-musl:
    permissions:
      contents: write
      id-token: write
      attestations: write
    strategy:
      fail-fast: false
      matrix:
        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
        debug: [false]
        mimalloc: [false]
        include:
          - platform: linux/amd64
          - platform: linux/amd64
            debug: true
          - platform: linux/amd64
            mimalloc: true
    name: Build ${{ matrix.platform }} static musl binary${{ matrix.debug && ' (debug)' || '' }}${{ matrix.mimalloc && ' (mimalloc)' || '' }}
    runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
    needs: [prepare]
    steps:
      - name: Prepare
        id: prepare
        run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
        env:
          PLATFORM: ${{ matrix.platform }}
      - uses: actions/checkout@v6
        with:
          ref: ${{ needs.prepare.outputs.ref }}
          persist-credentials: false
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
        with:
          platforms: ${{ matrix.platform }}
      - name: Login to DockerHub
        uses: docker/login-action@v4
        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
        with:
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      - name: Set VERSION
        run: |
          if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
            export VERSION=${GITHUB_REF_NAME:1}
          elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
            export VERSION="${REF}"
          else
            export VERSION=${GITHUB_SHA}
          fi

          echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
        env:
          REF: ${{ needs.prepare.outputs.ref }}
      - name: Build
        id: build
        uses: docker/bake-action@v7
        with:
          pull: true
          load: ${{ !fromJson(needs.prepare.outputs.push) || matrix.debug || matrix.mimalloc }}
          source: .
          targets: static-builder-musl
          set: |
            ${{ matrix.debug && 'static-builder-musl.args.DEBUG_SYMBOLS=1' || '' }}
            ${{ matrix.mimalloc && 'static-builder-musl.args.MIMALLOC=1' || '' }}
            ${{ (github.event_name == 'pull_request' || matrix.platform == 'linux/arm64') && 'static-builder-musl.args.NO_COMPRESS=1' || '' }}
            *.tags=
            *.platform=${{ matrix.platform }}
            ${{ fromJson(needs.prepare.outputs.push) && '' || format('*.cache-from=type=gha,scope={0}-static-builder-musl{1}{2}', needs.prepare.outputs.ref || github.ref, matrix.debug && '-debug' || '', matrix.mimalloc && '-mimalloc' || '') }}
            ${{ fromJson(needs.prepare.outputs.push) && '' || format('*.cache-from=type=gha,scope=refs/heads/main-static-builder-musl{0}{1}', matrix.debug && '-debug' || '', matrix.mimalloc && '-mimalloc' || '') }}
            ${{ fromJson(needs.prepare.outputs.push) && '' || format('*.cache-to=type=gha,scope={0}-static-builder-musl{1}{2},ignore-error=true', needs.prepare.outputs.ref || github.ref, matrix.debug && '-debug' || '', matrix.mimalloc && '-mimalloc' || '') }}
            ${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
        env:
          SHA: ${{ github.sha }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
        name: Export metadata
        if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc
        run: |
          mkdir -p /tmp/metadata

          # shellcheck disable=SC2086
          digest=$(jq -r '."static-builder-musl"."containerimage.digest"' <<< ${METADATA})
          touch "/tmp/metadata/${digest#sha256:}"
        env:
          METADATA: ${{ steps.build.outputs.metadata }}
      - name: Upload metadata
        if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc
        uses: actions/upload-artifact@v7
        with:
          name: metadata-static-builder-musl-${{ steps.prepare.outputs.sanitized_platform }}
          path: /tmp/metadata/*
          if-no-files-found: error
          retention-days: 1
      - name: Copy binary
        run: |
          # shellcheck disable=SC2034
          # TODO: remove "containerimage.config.digest" fallback once all runners use buildx v0.18+
          # which replaced it with "containerimage.digest" and "containerimage.descriptor"
          digest=$(jq -r '."static-builder-musl" | ${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && '."containerimage.digest"' || '(."containerimage.config.digest" // ."containerimage.digest")' }}' <<< "${METADATA}")
          docker create --platform="${PLATFORM}" --name static-builder-musl "${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && '${IMAGE_NAME}@${digest}' || '${digest}' }}"
          docker cp "static-builder-musl:/go/src/app/dist/${BINARY}" "${BINARY}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}"
        env:
          METADATA: ${{ steps.build.outputs.metadata }}
          BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
          PLATFORM: ${{ matrix.platform }}
      - name: Upload artifact
        if: ${{ !fromJson(needs.prepare.outputs.push) }}
        uses: actions/upload-artifact@v7
        with:
          name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
          path: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
          compression-level: 0
      - name: Upload assets
        if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
        run: gh release upload "${REF}" frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }} --repo dunglas/frankenphp --clobber
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REF: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}
      - if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
        uses: actions/attest-build-provenance@v4
        with:
          subject-path: ${{ github.workspace }}/frankenphp-linux-*
      - name: Run sanity checks
        run: |
          "${BINARY}" version
          "${BINARY}" build-info
          "${BINARY}" list-modules | grep frankenphp
          "${BINARY}" list-modules | grep http.encoders.br
          "${BINARY}" list-modules | grep http.handlers.mercure
          "${BINARY}" list-modules | grep http.handlers.mercure
          "${BINARY}" list-modules | grep http.handlers.vulcain
          "${BINARY}" php-cli -r "echo 'Sanity check passed';"
        env:
          BINARY: ./frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}

  build-linux-gnu:
    permissions:
      contents: write
      id-token: write
      attestations: write
    strategy:
      fail-fast: false
      matrix:
        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
    name: Build ${{ matrix.platform }} static GNU binary
    runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
    needs: [prepare]
    steps:
      # Inspired by https://gist.github.com/antiphishfish/1e3fbc3f64ef6f1ab2f47457d2da5d9d and https://github.com/apache/flink/blob/master/tools/azure-pipelines/free_disk_space.sh
      - name: Free disk space
        run: |
          set -xe
          sudo rm -rf /usr/share/dotnet
          sudo rm -rf /usr/share/swift
          sudo rm -rf /usr/local/lib/android
          sudo rm -rf /opt/ghc
          sudo rm -rf /usr/local/.ghcup
          sudo rm -rf "/usr/local/share/boost"
          sudo rm -rf "$AGENT_TOOLSDIRECTORY"
          sudo rm -rf /opt/hostedtoolcache/
          sudo rm -rf /usr/local/graalvm/
          sudo rm -rf /usr/local/share/powershell
          sudo rm -rf /usr/local/share/chromium
          sudo rm -rf /usr/local/lib/node_modules
          sudo docker image prune --all --force

          APT_PARAMS='sudo apt -y -qq -o=Dpkg::Use-Pty=0'
          $APT_PARAMS remove -y '^dotnet-.*'
          $APT_PARAMS remove -y '^llvm-.*'
          $APT_PARAMS remove -y '^php.*'
          $APT_PARAMS remove -y '^mongodb-.*'
          $APT_PARAMS remove -y '^mysql-.*'
          $APT_PARAMS remove -y azure-cli firefox powershell mono-devel libgl1-mesa-dri
          $APT_PARAMS autoremove --purge -y
          $APT_PARAMS autoclean
          $APT_PARAMS clean
      - name: Prepare
        id: prepare
        run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
        env:
          PLATFORM: ${{ matrix.platform }}
      - uses: actions/checkout@v6
        with:
          ref: ${{ needs.prepare.outputs.ref }}
          persist-credentials: false
      - name: Set VERSION
        run: |
          if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
            export VERSION=${GITHUB_REF_NAME:1}
          elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
            export VERSION="${REF}"
          else
            export VERSION=${GITHUB_SHA}
          fi

          echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
        env:
          REF: ${{ needs.prepare.outputs.ref }}
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
        with:
          platforms: ${{ matrix.platform }}
      - name: Login to DockerHub
        uses: docker/login-action@v4
        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
        with:
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      - name: Build
        id: build
        uses: docker/bake-action@v7
        with:
          pull: true
          load: ${{ !fromJson(needs.prepare.outputs.push) }}
          source: .
          targets: static-builder-gnu
          set: |
            ${{ (github.event_name == 'pull_request' || matrix.platform == 'linux/arm64') && 'static-builder-gnu.args.NO_COMPRESS=1' || '' }}
            *.tags=
            *.platform=${{ matrix.platform }}
            ${{ fromJson(needs.prepare.outputs.push) && '' || format('*.cache-from=type=gha,scope={0}-static-builder-gnu', needs.prepare.outputs.ref || github.ref) }}
            ${{ fromJson(needs.prepare.outputs.push) && '' || '*.cache-from=type=gha,scope=refs/heads/main-static-builder-gnu' }}
            ${{ fromJson(needs.prepare.outputs.push) && '' || format('*.cache-to=type=gha,scope={0}-static-builder-gnu,ignore-error=true', needs.prepare.outputs.ref || github.ref) }}
            ${{ fromJson(needs.prepare.outputs.push) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
        env:
          SHA: ${{ github.sha }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
        name: Export metadata
        if: fromJson(needs.prepare.outputs.push)
        run: |
          mkdir -p /tmp/metadata-gnu

          # shellcheck disable=SC2086
          digest=$(jq -r '."static-builder-gnu"."containerimage.digest"' <<< ${METADATA})
          touch "/tmp/metadata-gnu/${digest#sha256:}"
        env:
          METADATA: ${{ steps.build.outputs.metadata }}
      - name: Upload metadata
        if: fromJson(needs.prepare.outputs.push)
        uses: actions/upload-artifact@v7
        with:
          name: metadata-static-builder-gnu-${{ steps.prepare.outputs.sanitized_platform }}
          path: /tmp/metadata-gnu/*
          if-no-files-found: error
          retention-days: 1
      - name: Copy all frankenphp* files
        run: |
          # shellcheck disable=SC2034
          # TODO: remove "containerimage.config.digest" fallback once all runners use buildx v0.18+
          # which replaced it with "containerimage.digest" and "containerimage.descriptor"
          digest=$(jq -r '."static-builder-gnu" | ${{ fromJson(needs.prepare.outputs.push) && '."containerimage.digest"' || '(."containerimage.config.digest" // ."containerimage.digest")' }}' <<< "${METADATA}")
          container_id=$(docker create --platform="${PLATFORM}" "${{ fromJson(needs.prepare.outputs.push) && '${IMAGE_NAME}@${digest}' || '${digest}' }}")
          mkdir -p gh-output
          cd gh-output
          for file in $(docker run --rm "${{ fromJson(needs.prepare.outputs.push) && '${IMAGE_NAME}@${digest}' || '${digest}' }}" sh -c "ls /go/src/app/dist | grep '^frankenphp'"); do
            docker cp "${container_id}:/go/src/app/dist/${file}" "./${file}"
          done
          docker rm "${container_id}"
          mv "${BINARY}" "${BINARY}-gnu"
        env:
          METADATA: ${{ steps.build.outputs.metadata }}
          BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
          PLATFORM: ${{ matrix.platform }}
      - name: Upload artifact
        if: ${{ !fromJson(needs.prepare.outputs.push) }}
        uses: actions/upload-artifact@v7
        with:
          name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}-gnu-files
          path: gh-output/*
      - name: Upload assets
        if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
        run: gh release upload "${REF}" gh-output/* --repo dunglas/frankenphp --clobber
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REF: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}
      - if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
        uses: actions/attest-build-provenance@v4
        with:
          subject-path: ${{ github.workspace }}/gh-output/frankenphp-linux-*-gnu
      - name: Run sanity checks
        run: |
          "${BINARY}" version
          "${BINARY}" list-modules | grep frankenphp
          "${BINARY}" list-modules | grep http.encoders.br
          "${BINARY}" list-modules | grep http.handlers.mercure
          "${BINARY}" list-modules | grep http.handlers.mercure
          "${BINARY}" list-modules | grep http.handlers.vulcain
          "${BINARY}" php-cli -r "echo 'Sanity check passed';"
        env:
          BINARY: ./gh-output/frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}-gnu

  # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
  push:
    runs-on: ubuntu-24.04
    needs:
      - prepare
      - build-linux-musl
      - build-linux-gnu
    if: fromJson(needs.prepare.outputs.push)
    steps:
      - name: Download metadata
        uses: actions/download-artifact@v8
        with:
          pattern: metadata-static-builder-musl-*
          path: /tmp/metadata
          merge-multiple: true
      - name: Download GNU metadata
        uses: actions/download-artifact@v8
        with:
          pattern: metadata-static-builder-gnu-*
          path: /tmp/metadata-gnu
          merge-multiple: true
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
      - name: Login to DockerHub
        uses: docker/login-action@v4
        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
        with:
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      - name: Create manifest list and push
        working-directory: /tmp/metadata
        run: |
          # shellcheck disable=SC2046,SC2086
          docker buildx imagetools create $(jq -cr '.target."static-builder-musl".tags | map("-t " + .) | join(" ")' <<< "${METADATA}") \
            $(printf "${IMAGE_NAME}@sha256:%s " *)
        env:
          METADATA: ${{ needs.prepare.outputs.metadata }}
      - name: Create GNU manifest list and push
        working-directory: /tmp/metadata-gnu
        run: |
          # shellcheck disable=SC2046,SC2086
          docker buildx imagetools create $(jq -cr '.target."static-builder-gnu".tags | map("-t " + .) | join(" ")' <<< "${GNU_METADATA}") \
            $(printf "${IMAGE_NAME}@sha256:%s " *)
        env:
          GNU_METADATA: ${{ needs.prepare.outputs.gnu_metadata }}
      - name: Inspect image
        run: |
          # shellcheck disable=SC2046,SC2086
          docker buildx imagetools inspect "$(jq -cr '.target."static-builder-musl".tags | first' <<< "${METADATA}")"
        env:
          METADATA: ${{ needs.prepare.outputs.metadata }}
      - name: Inspect GNU image
        run: |
          # shellcheck disable=SC2046,SC2086
          docker buildx imagetools inspect "$(jq -cr '.target."static-builder-gnu".tags | first' <<< "${GNU_METADATA}")-gnu"
        env:
          GNU_METADATA: ${{ needs.prepare.outputs.gnu_metadata }}

  build-mac:
    permissions:
      contents: write
      id-token: write
      attestations: write
    strategy:
      fail-fast: false
      matrix:
        platform: ["arm64", "x86_64"]
    name: Build macOS ${{ matrix.platform }} binaries
    runs-on: ${{ matrix.platform == 'arm64' && 'macos-15' || 'macos-15-intel' }}
    needs: [prepare]
    env:
      HOMEBREW_NO_AUTO_UPDATE: 1
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ needs.prepare.outputs.ref }}
          persist-credentials: false
      - uses: actions/setup-go@v6
        with: # zizmor: ignore[cache-poisoning]
          go-version: "1.26"
          cache-dependency-path: |
            go.sum
            caddy/go.sum
          cache: ${{ github.event_name != 'release' }}
      - name: Set FRANKENPHP_VERSION
        run: |
          if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
            export FRANKENPHP_VERSION=${GITHUB_REF_NAME:1}
          elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
            export FRANKENPHP_VERSION="${REF}"
          else
            export FRANKENPHP_VERSION=${GITHUB_SHA}
          fi

          echo "FRANKENPHP_VERSION=${FRANKENPHP_VERSION}" >> "${GITHUB_ENV}"
        env:
          REF: ${{ needs.prepare.outputs.ref }}
      - name: Build FrankenPHP
        run: ./build-static.sh
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          RELEASE: ${{ (needs.prepare.outputs.ref || github.ref_type == 'tag') && '1' || '' }}
          NO_COMPRESS: ${{ github.event_name == 'pull_request' && '1' || '' }}
      - name: Upload logs
        if: ${{ failure() }}
        uses: actions/upload-artifact@v7
        with:
          path: dist/static-php-cli/log
          name: static-php-cli-log-${{ matrix.platform }}-${{ github.sha }}
      - if: needs.prepare.outputs.ref || github.ref_type == 'tag'
        uses: actions/attest-build-provenance@v4
        with:
          subject-path: ${{ github.workspace }}/dist/frankenphp-mac-*
      - name: Upload artifact
        if: github.ref_type == 'branch'
        uses: actions/upload-artifact@v7
        with:
          name: frankenphp-mac-${{ matrix.platform }}
          path: dist/frankenphp-mac-${{ matrix.platform }}
          compression-level: 0
      - name: Run sanity checks
        run: |
          "${BINARY}" version
          "${BINARY}" build-info
          "${BINARY}" list-modules | grep frankenphp
          "${BINARY}" list-modules | grep http.encoders.br
          "${BINARY}" list-modules | grep http.handlers.mercure
          "${BINARY}" list-modules | grep http.handlers.mercure
          "${BINARY}" list-modules | grep http.handlers.vulcain
          "${BINARY}" php-cli -r "echo 'Sanity check passed';"
        env:
          BINARY: dist/frankenphp-mac-${{ matrix.platform }}


================================================
FILE: .github/workflows/tests.yaml
================================================
---
name: Tests
concurrency:
  cancel-in-progress: true
  group: ${{ github.workflow }}-${{ github.ref }}
on:
  pull_request:
    branches:
      - main
    paths-ignore:
      - "docs/**"
  push:
    branches:
      - main
    paths-ignore:
      - "docs/**"
permissions:
  contents: read
env:
  GOTOOLCHAIN: local
  GOEXPERIMENT: cgocheck2
jobs:
  tests-linux:
    name: Tests (Linux, PHP ${{ matrix.php-versions }})
    runs-on: ubuntu-latest
    continue-on-error: false
    strategy:
      fail-fast: false
      matrix:
        include:
          - php-versions: "8.2"
          - php-versions: "8.3"
          - php-versions: "8.4"
          - php-versions: "8.5"
    env:
      GOMAXPROCS: 10
      LIBRARY_PATH: ${{ github.workspace }}/watcher/target/lib
      GOFLAGS: "-tags=nobadger,nomysql,nopgx"
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - uses: actions/setup-go@v6
        with:
          go-version: "1.26"
          cache-dependency-path: |
            go.sum 
            caddy/go.sum
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php-versions }}
          ini-file: development
          coverage: none
          tools: none
        env:
          phpts: ts
          debug: true
      - name: Install e-dant/watcher
        uses: ./.github/actions/watcher
      - name: Set CGO flags
        run: echo "CGO_CFLAGS=-I${PWD}/watcher/target/include $(php-config --includes)" >> "${GITHUB_ENV}"
      - name: Build
        run: go build
      - name: Build testcli binary
        working-directory: internal/testcli/
        run: go build
      - name: Compile library tests
        run: go test -race -v -x -c
      - name: Run library tests
        run: ./frankenphp.test -test.v
      - name: Run Caddy module tests
        working-directory: caddy/
        run: go test -race -v ./...
      - name: Run Fuzzing Tests
        working-directory: caddy/
        run: go test -fuzz FuzzRequest -fuzztime 20s
      - name: Build the server
        working-directory: caddy/frankenphp/
        run: go build
      - name: Start the server
        working-directory: testdata/
        run: sudo ../caddy/frankenphp/frankenphp start
      - name: Run integrations tests
        run: ./reload_test.sh
      - name: Lint Go code
        uses: golangci/golangci-lint-action@v9
        if: matrix.php-versions == '8.5'
        with:
          version: latest
      - name: Ensure go.mod is tidy
        if: matrix.php-versions == '8.5'
        run: go mod tidy -diff
      - name: Ensure caddy/go.mod is tidy
        if: matrix.php-versions == '8.5'
        run: go mod tidy -diff
        working-directory: caddy/
  integration-tests:
    name: Integration Tests (Linux, PHP ${{ matrix.php-versions }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php-versions: ["8.3", "8.4", "8.5"]
    env:
      XCADDY_GO_BUILD_FLAGS: "-tags=nobadger,nomysql,nopgx"
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - uses: actions/setup-go@v6
        with:
          go-version: "1.26"
          cache-dependency-path: |
            go.sum
            caddy/go.sum
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php-versions }}
          ini-file: development
          coverage: none
          tools: none
        env:
          phpts: ts
          debug: true
      - name: Install PHP development libraries
        run: sudo apt-get update && sudo apt-get install -y libkrb5-dev libsodium-dev libargon2-dev
      - name: Install xcaddy
        run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
      - name: Download PHP sources
        run: |
          PHP_VERSION=$(php -r "echo PHP_VERSION;")
          wget -q "https://www.php.net/distributions/php-${PHP_VERSION}.tar.gz" || wget -q "https://phpmirror.static-php.dev/distributions/php-${PHP_VERSION}.tar.gz"
          tar xzf "php-${PHP_VERSION}.tar.gz"
          echo "GEN_STUB_SCRIPT=${PWD}/php-${PHP_VERSION}/build/gen_stub.php" >> "${GITHUB_ENV}"
      - name: Set CGO flags
        run: |
          echo "CGO_CFLAGS=$(php-config --includes)" >> "${GITHUB_ENV}"
          echo "CGO_LDFLAGS=$(php-config --ldflags) $(php-config --libs)" >> "${GITHUB_ENV}"
      - name: Run integration tests
        working-directory: internal/extgen/
        run: go test -tags integration -v -timeout 30m
  tests-mac:
    name: Tests (macOS, PHP 8.5)
    runs-on: macos-latest
    env:
      HOMEBREW_NO_AUTO_UPDATE: 1
      GOFLAGS: "-tags=nowatcher,nobadger,nomysql,nopgx"
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - uses: actions/setup-go@v6
        with:
          go-version: "1.26"
          cache-dependency-path: |
            go.sum
            caddy/go.sum
      - uses: shivammathur/setup-php@v2
        with:
          php-version: 8.5
          ini-file: development
          coverage: none
          tools: none
        env:
          phpts: ts
          debug: true
      - name: Set Set CGO flags
        run: |
          {
           echo "CGO_CFLAGS=-I/opt/homebrew/include/ $(php-config --includes)"
           echo "CGO_LDFLAGS=-L/opt/homebrew/lib/ $(php-config --ldflags) $(php-config --libs)"
          } >> "${GITHUB_ENV}"
      - name: Build
        run: go build -tags nowatcher
      - name: Run library tests
        run: go test -tags nowatcher -race -v ./...
      - name: Run Caddy module tests
        working-directory: caddy/
        run: go test -race -v ./...


================================================
FILE: .github/workflows/translate.yaml
================================================
name: Translate Docs
concurrency:
  cancel-in-progress: true
  group: ${{ github.workflow }}-${{ github.ref }}
on:
  push:
    branches:
      - main
    paths:
      - "docs/*"
permissions:
  contents: write
  pull-requests: write
jobs:
  build:
    name: Translate Docs
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v6
        with:
          fetch-depth: 0
          # zizmor: ignore[artipacked]
          # persist-credentials is intentionally left enabled (unlike other workflows)
          # because this workflow needs to push a branch via git push
      - id: md_files
        run: |
          FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'docs/*.md' ':(exclude)docs/*/*.md')
          FILES=$(echo "$FILES" | xargs -n1 basename | tr '\n' ' ')
          [ -z "$FILES" ] && echo "found=false" >> "$GITHUB_OUTPUT" || echo "found=true" >> "$GITHUB_OUTPUT"
          echo "files=$FILES" >> "$GITHUB_OUTPUT"
      - name: Set up PHP
        if: steps.md_files.outputs.found == 'true'
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.5"
      - name: run translation script
        if: steps.md_files.outputs.found == 'true'
        env:
          GEMINI_API_KEY: "${{ secrets.GEMINI_API_KEY }}"
          MD_FILES: "${{ steps.md_files.outputs.files }}"
        run: |
          php ./docs/translate.php "$MD_FILES"
      - name: Run Linter
        if: steps.md_files.outputs.found == 'true'
        continue-on-error: true
        uses: super-linter/super-linter/slim@v8
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          LINTER_RULES_PATH: /
          MARKDOWN_CONFIG_FILE: .markdown-lint.yaml
          VALIDATE_NATURAL_LANGUAGE: false
          FIX_MARKDOWN: true
      - name: Create Pull Request
        if: steps.md_files.outputs.found == 'true'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ACTOR: ${{ github.actor }}
          ACTOR_ID: ${{ github.actor_id }}
          RUN_ID: ${{ github.run_id }}
          MD_FILES_LIST: ${{ steps.md_files.outputs.files }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          BRANCH="translations/$RUN_ID"
          git checkout -b "$BRANCH"
          git add docs/
          git diff --cached --quiet && exit 0
          git commit -m "docs: update translations" --author="$ACTOR <$ACTOR_ID+$ACTOR@users.noreply.github.com>"
          git push origin "$BRANCH"
          gh pr create \
            --title "docs: update translations" \
            --body "Translation updates for: $MD_FILES_LIST." \
            --label "translations" \
            --label "bot"


================================================
FILE: .github/workflows/windows.yaml
================================================
---
name: Build Windows release

concurrency:
  cancel-in-progress: true
  group: ${{ github.workflow }}-${{ github.ref }}

on:
  pull_request:
    branches:
      - main
    paths-ignore:
      - "docs/**"
  push:
    branches:
      - main
    tags:
      - v*.*.*
    paths-ignore:
      - "docs/**"
  workflow_dispatch:
    inputs:
      #checkov:skip=CKV_GHA_7
      version:
        description: "FrankenPHP version"
        required: false
        type: string
  schedule:
    - cron: "0 8 * * *"

permissions:
  contents: read

env:
  GOTOOLCHAIN: local
  GOFLAGS: "-ldflags=-extldflags=-fuse-ld=lld -tags=nobadger,nomysql,nopgx"
  PHP_DOWNLOAD_BASE: "https://downloads.php.net/~windows/releases/"
  CC: clang
  CXX: clang++

jobs:
  build:
    permissions:
      contents: write
    runs-on: windows-latest
    steps:
      - name: Determine ref
        run: |
          $ref = $env:REF
          if (-not $ref -and $env:GITHUB_EVENT_NAME -eq "schedule") {
            $ref = (gh release view --repo php/frankenphp --json tagName --jq '.tagName')
          }

          "REF=$ref" >> $env:GITHUB_ENV
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REF: ${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}

      - name: Configure Git
        run: |
          git config --global core.autocrlf false
          git config --global core.eol lf

      - name: Checkout Code
        uses: actions/checkout@v6
        with:
          ref: ${{ env.REF || '' }}
          path: frankenphp
          persist-credentials: false

      - name: Set FRANKENPHP_VERSION
        run: |
          $ref = $env:REF

          if ($env:GITHUB_REF_TYPE -eq "tag") {
            $frankenphpVersion = $env:GITHUB_REF_NAME.Substring(1)
          } elseif ($ref) {
            if ($ref.StartsWith("v")) {
              $frankenphpVersion = $ref.Substring(1)
            } else {
              $frankenphpVersion = $ref
            }
          } else {
            $frankenphpVersion = $env:GITHUB_SHA
          }

          "FRANKENPHP_VERSION=$frankenphpVersion" >> $env:GITHUB_ENV

      - name: Setup Go
        uses: actions/setup-go@v6
        with: # zizmor: ignore[cache-poisoning]
          go-version: "1.26"
          cache-dependency-path: |
            frankenphp/go.sum
            frankenphp/caddy/go.sum
          cache: ${{ !startsWith(github.ref, 'refs/tags/') }}
          check-latest: true

      - name: Install Vcpkg Libraries
        working-directory: frankenphp
        run: "vcpkg install"

      - name: Download Watcher
        run: |
          $latestTag = gh release list --repo e-dant/watcher --limit 1 --exclude-drafts --exclude-pre-releases --json tagName --jq '.[0].tagName'
          Write-Host "Latest Watcher version: $latestTag"

          gh release download $latestTag --repo e-dant/watcher --pattern "*x86_64-pc-windows-msvc.tar" -O watcher.tar

          tar -xf "watcher.tar" -C "$env:GITHUB_WORKSPACE"
          Rename-Item -Path "$env:GITHUB_WORKSPACE\x86_64-pc-windows-msvc" -NewName "watcher"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Download PHP
        run: |
          $webContent = Invoke-WebRequest -Uri $env:PHP_DOWNLOAD_BASE
          $links = $webContent.Links.Href | Where-Object { $_ -match "php-\d+\.\d+\.\d+-Win32-vs17-x64\.zip$" }

          if (-not $links) { throw "Could not find PHP zip files at $env:PHP_DOWNLOAD_BASE" }

          $latestFile = $links | Sort-Object { if ($_ -match '(\d+\.\d+\.\d+)') { [version]$matches[1] } } | Select-Object -Last 1

          $version = if ($latestFile -match '(\d+\.\d+\.\d+)') { $matches[1] }
          Write-Host "Detected latest PHP version: $version"

          "PHP_VERSION=$version" >> $env:GITHUB_ENV

          $phpZip = "php-$version-Win32-vs17-x64.zip"
          $develZip = "php-devel-pack-$version-Win32-vs17-x64.zip"

          $dirName = "frankenphp-windows-x86_64"

          "DIR_NAME=$dirName" >> $env:GITHUB_ENV

          Invoke-WebRequest -Uri "$env:PHP_DOWNLOAD_BASE/$phpZip" -OutFile "$env:TEMP\php.zip"
          Expand-Archive -Path "$env:TEMP\php.zip" -DestinationPath "$env:GITHUB_WORKSPACE\$dirName"

          Invoke-WebRequest -Uri "$env:PHP_DOWNLOAD_BASE/$develZip" -OutFile "$env:TEMP\php-devel.zip"
          Expand-Archive -Path "$env:TEMP\php-devel.zip" -DestinationPath "$env:GITHUB_WORKSPACE\php-devel"

      - name: Prepare env
        run: |
          $vcpkgRoot = "$env:GITHUB_WORKSPACE\frankenphp\vcpkg_installed\x64-windows"
          $watcherRoot = "$env:GITHUB_WORKSPACE\watcher"
          $phpBin = "$env:GITHUB_WORKSPACE\$env:DIR_NAME"
          $phpDevel = "$env:GITHUB_WORKSPACE\php-devel\php-$env:PHP_VERSION-devel-vs17-x64"

          "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\Llvm\bin" >> $env:GITHUB_PATH
          "$vcpkgRoot\bin" >> $env:GITHUB_PATH
          "$watcherRoot" >> $env:GITHUB_PATH
          "$phpBin" >> $env:GITHUB_PATH

          "CGO_CFLAGS=-DFRANKENPHP_VERSION=$env:FRANKENPHP_VERSION -I$vcpkgRoot\include -I$watcherRoot -I$phpDevel\include -I$phpDevel\include\main -I$phpDevel\include\TSRM -I$phpDevel\include\Zend -I$phpDevel\include\ext" >> $env:GITHUB_ENV
          "CGO_LDFLAGS=-L$vcpkgRoot\lib -lbrotlienc -L$watcherRoot -llibwatcher-c -L$phpBin -L$phpDevel\lib -lphp8ts -lphp8embed" >> $env:GITHUB_ENV

      - name: Embed Windows icon and metadata
        working-directory: frankenphp\caddy\frankenphp
        run: |
          go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest

          $major = 0; $minor = 0; $patch = 0; $build = 0
          if ($env:FRANKENPHP_VERSION -match '^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)$') {
            $major = [int]$Matches['major']
            $minor = [int]$Matches['minor']
            $patch = [int]$Matches['patch']
          }

          $json = @{
            FixedFileInfo = @{
              FileVersion = @{ Major = $major; Minor = $minor; Patch = $patch; Build = $build }
              ProductVersion = @{ Major = $major; Minor = $minor; Patch = $patch; Build = $build }
            }
            StringFileInfo = @{
              CompanyName = "FrankenPHP"
              FileDescription = "The modern PHP app server"
              FileVersion = $env:FRANKENPHP_VERSION
              InternalName = "frankenphp"
              OriginalFilename = "frankenphp.exe"
              LegalCopyright = "(c) 2022 Kévin Dunglas, MIT License"
              ProductName = "FrankenPHP"
              ProductVersion = $env:FRANKENPHP_VERSION
              Comments = "https://frankenphp.dev/"
            }
            VarFileInfo = @{
              Translation = @{ LangID = 9; CharsetID = 1200 }
            }
          } | ConvertTo-Json -Depth 10
          $json | Set-Content "versioninfo.json"

          goversioninfo -64 -icon ..\..\frankenphp.ico versioninfo.json -o resource.syso

      - name: Build FrankenPHP
        run: |
          $customVersion = "FrankenPHP $env:FRANKENPHP_VERSION PHP $env:PHP_VERSION Caddy"
          go build -ldflags="-extldflags=-fuse-ld=lld -X 'github.com/caddyserver/caddy/v2.CustomVersion=$customVersion' -X 'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp' -X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy'"
        working-directory: frankenphp\caddy\frankenphp

      - name: Create Directory
        run: |
          Copy-Item frankenphp\caddy\frankenphp\frankenphp.exe $env:DIR_NAME
          Copy-Item watcher\libwatcher-c.dll $env:DIR_NAME
          Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\brotlienc.dll $env:DIR_NAME
          Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\brotlidec.dll $env:DIR_NAME
          Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\brotlicommon.dll $env:DIR_NAME
          Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\pthreadVC3.dll $env:DIR_NAME

      - name: Upload Artifact
        if: ${{ !env.REF }}
        uses: actions/upload-artifact@v7
        with:
          name: ${{ env.DIR_NAME }}
          path: ${{ env.DIR_NAME }}
          if-no-files-found: error

      - name: Zip Release Artifact
        if: ${{ env.REF }}
        run: Compress-Archive -Path "$env:DIR_NAME\*" -DestinationPath "$env:DIR_NAME.zip"

      - name: Upload Release Asset
        if: ${{ env.REF }}
        run: gh release upload "$env:REF" "$env:DIR_NAME.zip" --repo php/frankenphp --clobber
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Run Tests
        run: |
          "opcache.enable=0`r`nopcache.enable_cli=0" | Out-File php.ini
          $env:PHPRC = Get-Location

          go test -race ./...
          cd caddy
          go test -race ./...
        working-directory: ${{ github.workspace }}\frankenphp


================================================
FILE: .github/workflows/wrap-issue-details.yaml
================================================
name: Wrap Issue Content
on:
  issues:
    types: [opened, edited]

permissions:
  contents: read

jobs:
  wrap_content:
    runs-on: ubuntu-latest
    permissions:
      issues: write
    steps:
      - uses: actions/github-script@v8
        with:
          script: |
            const body = context.payload.issue.body;

            const wrapSection = (inputBody, marker, summary) => {
              const regex = new RegExp(`(${marker})\\s*([\\s\\S]*?)(?=\\n### |$)`);

              return inputBody.replace(regex, (match, header, content) => {
                const trimmed = content.trim();
                if (!trimmed || trimmed.includes("<details>")) return match;

                return `${header}\n\n<details>\n<summary>${summary}</summary>\n\n${trimmed}\n\n</details>\n`;
              });
            };

            let newBody = body;
            newBody = wrapSection(newBody, "### PHP configuration", "phpinfo() output");
            newBody = wrapSection(newBody, "### Relevant log output", "Relevant log output");

            if (newBody !== body) {
              await github.rest.issues.update({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                body: newBody
              });
            }


================================================
FILE: .gitignore
================================================
/caddy/frankenphp/Build
/caddy/frankenphp/frankenphp
/caddy/frankenphp/frankenphp.exe
/dist
/github_conf
/internal/testserver/testserver
/internal/testcli/testcli
/package/etc/php.ini
/super-linter-output
/vcpkg_installed/
.DS_Store
.idea/
.vscode/
__debug_bin
frankenphp.test
*.log
compile_flags.txt


================================================
FILE: .gitleaksignore
================================================
/github/workspace/docs/mercure.md:jwt:88
/github/workspace/docs/mercure.md:jwt:90


================================================
FILE: .golangci.yaml
================================================
---
version: "2"
run:
  build-tags:
    - nobadger
    - nomysql
    - nopgx


================================================
FILE: .hadolint.yaml
================================================
---
ignored:
  - DL3006
  - DL3008
  - DL3018
  - DL3022


================================================
FILE: .markdown-lint.yaml
================================================
---
MD010: false
MD013: false
MD033: false
MD060: false


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

## Compiling PHP

### With Docker (Linux)

Build the dev Docker image:

```console
docker build -t frankenphp-dev -f dev.Dockerfile .
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev
```

The image contains the usual development tools (Go, GDB, Valgrind, Neovim...) and uses the following php setting locations

- php.ini: `/etc/frankenphp/php.ini` A php.ini file with development presets is provided by default.
- additional configuration files: `/etc/frankenphp/php.d/*.ini`
- php extensions: `/usr/lib/frankenphp/modules/`

If your Docker version is lower than 23.0, the build will fail due to dockerignore [pattern issue](https://github.com/moby/moby/pull/42676). Add directories to `.dockerignore`:

```patch
 !testdata/*.php
 !testdata/*.txt
+!caddy
+!internal
```

### Without Docker (Linux and macOS)

[Follow the instructions to compile from sources](https://frankenphp.dev/docs/compile/) and pass the `--debug` configuration flag.

## Running the Test Suite

```console
export CGO_CFLAGS=-O0 -g $(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)"
go test -race -v ./...
```

## Caddy Module

Build Caddy with the FrankenPHP Caddy module:

```console
cd caddy/frankenphp/
go build -tags nobadger,nomysql,nopgx
cd ../../
```

Run the Caddy with the FrankenPHP Caddy module:

```console
cd testdata/
../caddy/frankenphp/frankenphp run
```

The server is listening on `127.0.0.1:80`:

> [!NOTE]
> If you are using Docker, you will have to either bind container port 80 or execute from inside the container

```console
curl -vk http://127.0.0.1/phpinfo.php
```

## Minimal Test Server

Build the minimal test server:

```console
cd internal/testserver/
go build
cd ../../
```

Run the test server:

```console
cd testdata/
../internal/testserver/testserver
```

The server is listening on `127.0.0.1:8080`:

```console
curl -v http://127.0.0.1:8080/phpinfo.php
```

## Windows Development

1. Configure Git to always use `lf` line endings

    ```powershell
    git config --global core.autocrlf false
    git config --global core.eol lf
    ```

2. Install Visual Studio, Git, and Go:

    ```powershell
    winget install -e --id Microsoft.VisualStudio.2022.Community --override "--passive --wait --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --includeRecommended"
    winget install -e --id GoLang.Go
    winget install -e --id Git.Git
    ```

3. Install vcpkg:

    ```powershell
    cd C:\
    git clone https://github.com/microsoft/vcpkg
    .\vcpkg\bootstrap-vcpkg.bat
    ```

4. [Download the latest version of the watcher library for Windows](https://github.com/e-dant/watcher/releases) and extract it to a directory named `C:\watcher`
5. [Download the latest **Thread Safe** version of PHP and of the PHP SDK for Windows](https://windows.php.net/download/), extract them in directories named `C:\php` and `C:\php-devel`
6. Clone the FrankenPHP Git repository:

    ```powershell
    git clone https://github.com/php/frankenphp C:\frankenphp
    cd C:\frankenphp
    ```

7. Install the dependencies:

    ```powershell
    C:\vcpkg\vcpkg.exe install
    ```

8. Configure the needed environment variables (PowerShell):

    ```powershell
    $env:PATH += ';C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\bin'
    $env:CC = 'clang'
    $env:CXX = 'clang++'
    $env:CGO_CFLAGS = "-O0 -g -IC:\frankenphp\vcpkg_installed\x64-windows\include -IC:\watcher -IC:\php-devel\include -IC:\php-devel\include\main -IC:\php-devel\include\TSRM -IC:\php-devel\include\Zend -IC:\php-devel\include\ext"
    $env:CGO_LDFLAGS = '-LC:\frankenphp\vcpkg_installed\x64-windows\lib -lbrotlienc -LC:\watcher -llibwatcher-c -LC:\php -LC:\php-devel\lib -lphp8ts -lphp8embed'
    ```

9. Run the tests:

    ```powershell
    go test -race -ldflags '-extldflags="-fuse-ld=lld"' ./...
    cd caddy
    go test -race -ldflags '-extldflags="-fuse-ld=lld"' -tags nobadger,nomysql,nopgx ./...
    cd ..
    ```

10. Build the binary:

    ```powershell
    cd caddy/frankenphp
    go build -ldflags '-extldflags="-fuse-ld=lld"' -tags nobadger,nomysql,nopgx
    cd ../..
    ```

## Building Docker Images Locally

Print Bake plan:

```console
docker buildx bake -f docker-bake.hcl --print
```

Build FrankenPHP images for amd64 locally:

```console
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/amd64"
```

Build FrankenPHP images for arm64 locally:

```console
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/arm64"
```

Build FrankenPHP images from scratch for arm64 & amd64 and push to Docker Hub:

```console
docker buildx bake -f docker-bake.hcl --pull --no-cache --push
```

## Debugging Segmentation Faults With Static Builds

1. Download the debug version of the FrankenPHP binary from GitHub or create your custom static build including debug symbols:

   ```console
   docker buildx bake \
       --load \
       --set static-builder.args.DEBUG_SYMBOLS=1 \
       --set "static-builder.platform=linux/amd64" \
       static-builder
   docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp
   ```

2. Replace your current version of `frankenphp` with the debug FrankenPHP executable
3. Start FrankenPHP as usual (alternatively, you can directly start FrankenPHP with GDB: `gdb --args frankenphp run`)
4. Attach to the process with GDB:

   ```console
   gdb -p `pidof frankenphp`
   ```

5. If necessary, type `continue` in the GDB shell
6. Make FrankenPHP crash
7. Type `bt` in the GDB shell
8. Copy the output

## Debugging Segmentation Faults in GitHub Actions

1. Open `.github/workflows/tests.yml`
2. Enable PHP debug symbols

   ```patch
       - uses: shivammathur/setup-php@v2
         # ...
         env:
           phpts: ts
   +       debug: true
   ```

3. Enable `tmate` to connect to the container

   ```patch
       - name: Set CGO flags
         run: echo "CGO_CFLAGS=-O0 -g $(php-config --includes)" >> "$GITHUB_ENV"
   +   - run: |
   +       sudo apt install gdb
   +       mkdir -p /home/runner/.config/gdb/
   +       printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit
   +   - uses: mxschmitt/action-tmate@v3
   ```

4. Connect to the container
5. Open `frankenphp.go`
6. Enable `cgosymbolizer`

   ```patch
   -	//_ "github.com/ianlancetaylor/cgosymbolizer"
   +	_ "github.com/ianlancetaylor/cgosymbolizer"
   ```

7. Download the module: `go get`
8. In the container, you can use GDB and the like:

   ```console
   go test -tags -c -ldflags=-w
   gdb --args frankenphp.test -test.run ^MyTest$
   ```

9. When the bug is fixed, revert all these changes

## Development Environment Setup (WSL/Unix)

### Initial setup

Follow the instructions in [compiling from sources](https://frankenphp.dev/docs/compile/).
The steps assume the following environment:

- Go installed at `/usr/local/go`
- PHP source cloned to `~/php-src`
- PHP built at: `/usr/local/bin/php`
- FrankenPHP source cloned to `~/frankenphp`

### CLion Setup for CGO glue/PHP Source Development

1. Install CLion (on your host OS)

   - Download from [JetBrains](https://www.jetbrains.com/clion/download/)
   - Launch (if on Windows, in WSL):

     ```bash
     clion &>/dev/null
     ```

2. Open Project in CLion

   - Open CLion → Open → Select the `~/frankenphp` directory
   - Add a build chain: Settings → Build, Execution, Deployment → Custom Build Targets
   - Select any Build Target, under `Build` set up an External Tool (call it e.g. go build)
   - Set up a wrapper script that builds frankenphp for you, called `go_compile_frankenphp.sh`

   ```bash
   CGO_CFLAGS="-O0 -g" ./go.sh
   ```

   - Under Program, select `go_compile_frankenphp.sh`
   - Leave Arguments blank
   - Working Directory: `~/frankenphp/caddy/frankenphp`

3. Configure Run Targets

   - Go to Run → Edit Configurations
   - Create:
     - frankenphp:
       - Type: Native Application
       - Target: select the `go build` target you created
       - Executable: `~/frankenphp/caddy/frankenphp/frankenphp`
       - Arguments: the arguments you want to start frankenphp with, e.g. `php-cli test.php`

4. Debug Go files from CLion

   - Right click on a *.go file in the Project view on the left
   - Override file type → C/C++

   Now you can place breakpoints in C, C++ and Go files.
   To get syntax highlighting for imports from php-src, you may need to tell CLion about the include paths. Create a
   `compile_flags.txt` file in `~/frankenphp` with the following contents:

   ```gcc
   -I/usr/local/include/php
   -I/usr/local/include/php/Zend
   -I/usr/local/include/php/main
   -I/usr/local/include/php/TSRM
   ```

---

### GoLand Setup for FrankenPHP Development

Use GoLand for primary Go development, but the debugger cannot debug C code.

1. Install GoLand (on your host OS)

   - Download from [JetBrains](https://www.jetbrains.com/go/download/)

     ```bash
     goland &>/dev/null
     ```

2. Open in GoLand

   - Launch GoLand → Open → Select the `~/frankenphp` directory

---

### Go Configuration

- Select Go Build
  - Name `frankenphp`
  - Run kind: Directory
- Directory: `~/frankenphp/caddy/frankenphp`
- Output directory: `~/frankenphp/caddy/frankenphp`
- Working directory: `~/frankenphp/caddy/frankenphp`
- Environment (adjust for your $(php-config ...) output):
  `CGO_CFLAGS=-O0 -g -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_LDFLAGS=-lm -lpthread -lsqlite3 -lxml2 -lbrotlienc -lbrotlidec -lbrotlicommon -lwatcher`
- Go tool arguments: `-tags=nobadger,nomysql,nopgx`
- Program arguments: e.g. `php-cli -i`

To debug C files from GoLand

- Right click on a *.c file in the Project view on the left
- Override file type → Go

Now you can place breakpoints in C, C++ and Go files.

---

### GoLand Setup on Windows

1. Follow the [Windows Development section](#windows-development)

2. Install GoLand

   - Download from [JetBrains](https://www.jetbrains.com/go/download/)
   - Launch GoLand

3. Open in GoLand

   - Select **Open** → Choose the directory where you cloned `frankenphp`

4. Configure Go Build

   - Go to **Run** → **Edit Configurations**
   - Click **+** and select **Go Build**
   - Name: `frankenphp`
   - Run kind: **Directory**
   - Directory: `.\caddy\frankenphp`
   - Output directory: `.\caddy\frankenphp`
   - Working directory: `.\caddy\frankenphp`
   - Go tool arguments: `-tags=nobadger,nomysql,nopgx`
   - Environment variables: see the [Windows Development section](#windows-development)
   - Program arguments: e.g. `php-server`

---

### Debugging and Integration Notes

- Use CLion for debugging PHP internals and `cgo` glue code
- Use GoLand for primary Go development and debugging
- FrankenPHP can be added as a run configuration in CLion for unified C/Go debugging if needed, but syntax highlighting won't work in Go files

## Misc Dev Resources

- [PHP embedding in uWSGI](https://github.com/unbit/uwsgi/blob/master/plugins/php/php_plugin.c)
- [PHP embedding in NGINX Unit](https://github.com/nginx/unit/blob/master/src/nxt_php_sapi.c)
- [PHP embedding in Go (go-php)](https://github.com/deuill/go-php)
- [PHP embedding in Go (GoEmPHP)](https://github.com/mikespook/goemphp)
- [PHP embedding in C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7)
- [Extending and Embedding PHP by Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false)
- [What the heck is TSRMLS_CC, anyway?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html)
- [SDL bindings](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main)

## Docker-Related Resources

- [Bake file definition](https://docs.docker.com/build/customize/bake/file-definition/)
- [`docker buildx build`](https://docs.docker.com/engine/reference/commandline/buildx_build/)

## Useful Command

```console
apk add strace util-linux gdb
strace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1
```

## Translating the Documentation

To translate the documentation and the site into a new language,
follow these steps:

1. Create a new directory named with the language's 2-character ISO code in this repository's `docs/` directory
2. Copy all the `.md` files in the root of the `docs/` directory into the new directory (always use the English version as source for translation, as it's always up to date)
3. Copy the `README.md` and `CONTRIBUTING.md` files from the root directory to the new directory
4. Translate the content of the files, but don't change the filenames, also don't translate strings starting with `> [!` (it's special markup for GitHub)
5. Create a Pull Request with the translations
6. In the [site repository](https://github.com/dunglas/frankenphp-website/tree/main), copy and translate the translation files in the `content/`, `data/`, and `i18n/` directories
7. Translate the values in the created YAML file
8. Open a Pull Request on the site repository


================================================
FILE: Dockerfile
================================================
# syntax=docker/dockerfile:1
#checkov:skip=CKV_DOCKER_2
#checkov:skip=CKV_DOCKER_3
#checkov:skip=CKV_DOCKER_7
FROM php-base AS common

WORKDIR /app

RUN apt-get update && \
	apt-get -y --no-install-recommends install \
		mailcap \
		libcap2-bin \
	&& \
	apt-get clean && \
	rm -rf /var/lib/apt/lists/*

RUN set -eux; \
	mkdir -p \
		/app/public \
		/config/caddy \
		/data/caddy \
		/etc/caddy \
		/etc/frankenphp; \
	sed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint; \
	echo '<?php phpinfo();' > /app/public/index.php

COPY --link caddy/frankenphp/Caddyfile /etc/caddy/Caddyfile
RUN ln /etc/caddy/Caddyfile /etc/frankenphp/Caddyfile && \
	curl -sSLf \
		-o /usr/local/bin/install-php-extensions \
		https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \
	chmod +x /usr/local/bin/install-php-extensions

CMD ["--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"]
HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1

# See https://caddyserver.com/docs/conventions#file-locations for details
ENV XDG_CONFIG_HOME=/config
ENV XDG_DATA_HOME=/data

EXPOSE 80
EXPOSE 443
EXPOSE 443/udp
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/php/frankenphp
LABEL org.opencontainers.image.licenses=MIT
LABEL org.opencontainers.image.vendor="Kévin Dunglas"


FROM common AS builder

ARG FRANKENPHP_VERSION='dev'
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

COPY --from=golang-base /usr/local/go /usr/local/go

ENV PATH=/usr/local/go/bin:$PATH
ENV GOTOOLCHAIN=local

# This is required to link the FrankenPHP binary to the PHP binary
RUN apt-get update && \
	apt-get -y --no-install-recommends install \
	cmake \
	git \
	libargon2-dev \
	libbrotli-dev \
	libcurl4-openssl-dev \
	libonig-dev \
	libreadline-dev \
	libsodium-dev \
	libsqlite3-dev \
	libssl-dev \
	libxml2-dev \
	zlib1g-dev \
	&& \
	apt-get clean

# Install e-dant/watcher (necessary for file watching)
WORKDIR /usr/local/src/watcher
RUN --mount=type=secret,id=github-token \
    if [ -f /run/secrets/github-token ] && [ -s /run/secrets/github-token ]; then \
        curl -s -H "Authorization: Bearer $(cat /run/secrets/github-token)" https://api.github.com/repos/e-dant/watcher/releases/latest; \
    else \
        curl -s https://api.github.com/repos/e-dant/watcher/releases/latest; \
    fi | \
    grep tarball_url | \
    awk '{ print $2 }' | \
    sed 's/,$//' | \
    sed 's/"//g' | \
    xargs curl -L | \
    tar xz --strip-components 1 && \
    # -Wno-error=use-after-free: GCC 12 on Bookworm i386 emits a spurious warning in libstdc++ basic_string.h
    cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-Wno-error=use-after-free" && \
    cmake --build build && \
    cmake --install build && \
    ldconfig

WORKDIR /go/src/app

COPY --link go.mod go.sum ./
RUN go mod download

WORKDIR /go/src/app/caddy
COPY --link caddy/go.mod caddy/go.sum ./
RUN go mod download

WORKDIR /go/src/app
COPY --link . ./

# See https://github.com/docker-library/php/blob/master/8.5/trixie/zts/Dockerfile#L57-L59 for PHP values
ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS"
ENV CGO_CPPFLAGS=$PHP_CPPFLAGS
ENV CGO_LDFLAGS="-L/usr/local/lib -lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS"

WORKDIR /go/src/app/caddy/frankenphp
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' -X 'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp' -X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy'" -buildvcs=true && \
	setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
	cp Caddyfile /etc/frankenphp/Caddyfile && \
	frankenphp version && \
 	frankenphp build-info

WORKDIR /go/src/app


FROM common AS runner

ENV GODEBUG=cgocheck=0

# copy watcher shared library
COPY --from=builder /usr/local/lib/libwatcher* /usr/local/lib/
# fix for the file watcher on arm
RUN apt-get install -y --no-install-recommends libstdc++6 && \
	apt-get clean && \
	ldconfig

COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
	frankenphp version && \
	frankenphp build-info


================================================
FILE: LICENSE
================================================
The MIT license

Copyright (c) 2022-present Kévin Dunglas

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
# FrankenPHP: Modern App Server for PHP

<h1 align="center"><a href="https://frankenphp.dev"><img src="frankenphp.png" alt="FrankenPHP" width="600"></a></h1>

FrankenPHP is a modern application server for PHP built on top of the [Caddy](https://caddyserver.com/) web server.

FrankenPHP gives superpowers to your PHP apps thanks to its stunning features: [_Early Hints_](https://frankenphp.dev/docs/early-hints/), [worker mode](https://frankenphp.dev/docs/worker/), [real-time capabilities](https://frankenphp.dev/docs/mercure/), [hot reloading](https://frankenphp.dev/docs/hot-reload/), automatic HTTPS, HTTP/2, and HTTP/3 support...

FrankenPHP works with any PHP app and makes your Laravel and Symfony projects faster than ever thanks to their official integrations with the worker mode.

FrankenPHP can also be used as a standalone Go library to embed PHP in any app using `net/http`.

[**Learn more** on _frankenphp.dev_](https://frankenphp.dev) and in this slide deck:

<a href="https://dunglas.dev/2022/10/frankenphp-the-modern-php-app-server-written-in-go/"><img src="https://dunglas.dev/wp-content/uploads/2022/10/frankenphp.png" alt="Slides" width="600"></a>

## Getting Started

### Install Script

On Linux and macOS, copy this line into your terminal to automatically
install an appropriate version for your platform:

```console
curl https://frankenphp.dev/install.sh | sh
```

On Windows, run this in PowerShell:

```powershell
irm https://frankenphp.dev/install.ps1 | iex
```

### Standalone Binary

We provide FrankenPHP binaries for Linux, macOS and Windows
containing [PHP 8.5](https://www.php.net/releases/8.5/).

Linux binaries are statically linked, so they can be used on any Linux distribution without installing any dependency. macOS binaries are also self-contained.
They contain most popular PHP extensions.
Windows archives contain the official PHP binary for Windows.

[Download FrankenPHP](https://github.com/php/frankenphp/releases)

### rpm Packages

Our maintainers offer rpm packages for all systems using `dnf`. To install, run:

```console
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.5 # 8.2-8.5 available
sudo dnf install frankenphp
```

**Installing extensions:** `sudo dnf install php-zts-<extension>`

For extensions not available by default, use [PIE](https://github.com/php/pie):

```console
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```

### deb Packages

Our maintainers offer deb packages for all systems using `apt`. To install, run:

```console
VERSION=85 # 82-85 available
sudo curl https://pkg.henderkes.com/api/packages/${VERSION}/debian/repository.key -o /etc/apt/keyrings/static-php${VERSION}.asc
echo "deb [signed-by=/etc/apt/keyrings/static-php${VERSION}.asc] https://pkg.henderkes.com/api/packages/${VERSION}/debian php-zts main" | sudo tee -a /etc/apt/sources.list.d/static-php${VERSION}.list
sudo apt update
sudo apt install frankenphp
```

**Installing extensions:** `sudo apt install php-zts-<extension>`

For extensions not available by default, use [PIE](https://github.com/php/pie):

```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```

### apk Packages

Our maintainers offer apk packages for all systems using `apk`. To install, run:

```console
VERSION=85 # 82-85 available
echo "https://pkg.henderkes.com/api/packages/${VERSION}/alpine/main/php-zts" | sudo tee -a /etc/apk/repositories
KEYFILE=$(curl -sJOw '%{filename_effective}' https://pkg.henderkes.com/api/packages/${VERSION}/alpine/key)
sudo mv ${KEYFILE} /etc/apk/keys/ && 
sudo apk update && 
sudo apk add frankenphp
```

**Installing extensions:** `sudo apk add php-zts-<extension>`

For extensions not available by default, use [PIE](https://github.com/php/pie):

```console
sudo apk add pie-zts
sudo pie-zts install asgrim/example-pie-extension
```

### Homebrew

FrankenPHP is also available as a [Homebrew](https://brew.sh) package for macOS and Linux.

```console
brew install dunglas/frankenphp/frankenphp
```

**Installing extensions:** Use [PIE](https://github.com/php/pie).

### Usage

To serve the content of the current directory, run:

```console
frankenphp php-server
```

You can also run command-line scripts with:

```console
frankenphp php-cli /path/to/your/script.php
```

For the deb and rpm packages, you can also start the systemd service:

```console
sudo systemctl start frankenphp
```

### Docker

Alternatively, [Docker images](https://frankenphp.dev/docs/docker/) are available:

```console
docker run -v .:/app/public \
    -p 80:80 -p 443:443 -p 443:443/udp \
    dunglas/frankenphp
```

Go to `https://localhost`, and enjoy!

> [!TIP]
>
> Do not attempt to use `https://127.0.0.1`. Use `https://localhost` and accept the self-signed certificate.
> Use the [`SERVER_NAME` environment variable](docs/config.md#environment-variables) to change the domain to use.

## Docs

- [Classic mode](https://frankenphp.dev/docs/classic/)
- [Worker mode](https://frankenphp.dev/docs/worker/)
- [Early Hints support (103 HTTP status code)](https://frankenphp.dev/docs/early-hints/)
- [Real-time](https://frankenphp.dev/docs/mercure/)
- [Logging](https://frankenphp.dev/docs/logging/)
- [Hot reloading](https://frankenphp.dev/docs/hot-reload/)
- [Efficiently Serving Large Static Files](https://frankenphp.dev/docs/x-sendfile/)
- [Configuration](https://frankenphp.dev/docs/config/)
- [Writing PHP Extensions in Go](https://frankenphp.dev/docs/extensions/)
- [Docker images](https://frankenphp.dev/docs/docker/)
- [Deploy in production](https://frankenphp.dev/docs/production/)
- [Performance optimization](https://frankenphp.dev/docs/performance/)
- [Create **standalone**, self-executable PHP apps](https://frankenphp.dev/docs/embed/)
- [Create static binaries](https://frankenphp.dev/docs/static/)
- [Compile from sources](https://frankenphp.dev/docs/compile/)
- [Monitoring FrankenPHP](https://frankenphp.dev/docs/metrics/)
- [WordPress integration](https://frankenphp.dev/docs/wordpress/)
- [Laravel integration](https://frankenphp.dev/docs/laravel/)
- [Known issues](https://frankenphp.dev/docs/known-issues/)
- [Demo app (Symfony) and benchmarks](https://github.com/dunglas/frankenphp-demo)
- [Go library documentation](https://pkg.go.dev/github.com/dunglas/frankenphp)
- [Contributing and debugging](https://frankenphp.dev/docs/contributing/)

## Examples and Skeletons

- [Symfony](https://github.com/dunglas/symfony-docker)
- [API Platform](https://api-platform.com/docs/symfony)
- [Laravel](https://frankenphp.dev/docs/laravel/)
- [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
- [WordPress](https://github.com/StephenMiracle/frankenwp)
- [Drupal](https://github.com/dunglas/frankenphp-drupal)
- [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
- [TYPO3](https://github.com/ochorocho/franken-typo3)
- [Magento2](https://github.com/ekino/frankenphp-magento2)


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

Only the latest version is supported.
Please ensure that you're always using the latest release.

Binaries and Docker images are rebuilt nightly using the latest versions of dependencies.

## Reporting a Vulnerability

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


================================================
FILE: alpine.Dockerfile
================================================
# syntax=docker/dockerfile:1
#checkov:skip=CKV_DOCKER_2
#checkov:skip=CKV_DOCKER_3
#checkov:skip=CKV_DOCKER_7
FROM php-base AS common

ARG TARGETARCH

WORKDIR /app

RUN apk add --no-cache \
	ca-certificates \
	libcap \
	mailcap

RUN set -eux; \
	mkdir -p \
		/app/public \
		/config/caddy \
		/data/caddy \
		/etc/caddy \
		/etc/frankenphp; \
	sed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint; \
	echo '<?php phpinfo();' > /app/public/index.php

COPY --link caddy/frankenphp/Caddyfile /etc/caddy/Caddyfile

RUN ln /etc/caddy/Caddyfile /etc/frankenphp/Caddyfile && \
	curl -sSLf \
		-o /usr/local/bin/install-php-extensions \
		https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \
	chmod +x /usr/local/bin/install-php-extensions

CMD ["--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"]
HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1

# See https://caddyserver.com/docs/conventions#file-locations for details
ENV XDG_CONFIG_HOME=/config
ENV XDG_DATA_HOME=/data

EXPOSE 80
EXPOSE 443
EXPOSE 443/udp
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/php/frankenphp
LABEL org.opencontainers.image.licenses=MIT
LABEL org.opencontainers.image.vendor="Kévin Dunglas"


FROM common AS builder

ARG FRANKENPHP_VERSION='dev'
ARG NO_COMPRESS=''
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]

COPY --link --from=golang-base /usr/local/go /usr/local/go

ENV PATH=/usr/local/go/bin:$PATH
ENV GOTOOLCHAIN=local

# hadolint ignore=SC2086
RUN apk add --no-cache --virtual .build-deps \
	$PHPIZE_DEPS \
	argon2-dev \
	# Needed for the custom Go build \
	bash \
	brotli-dev \
	coreutils \
	curl-dev \
	# Needed for the custom Go build \
	git \
	gnu-libiconv-dev \
	libsodium-dev \
	# Needed for the file watcher \
	cmake \
	libstdc++ \
	libxml2-dev \
	linux-headers \
	oniguruma-dev \
	openssl-dev \
	readline-dev \
	sqlite-dev \
	upx

# Install e-dant/watcher (necessary for file watching)
WORKDIR /usr/local/src/watcher
RUN --mount=type=secret,id=github-token \
		if [ -f /run/secrets/github-token ] && [ -s /run/secrets/github-token ]; then \
				curl -s -H "Authorization: Bearer $(cat /run/secrets/github-token)" https://api.github.com/repos/e-dant/watcher/releases/latest; \
		else \
				curl -s https://api.github.com/repos/e-dant/watcher/releases/latest; \
		fi | \
		grep tarball_url | \
		awk '{ print $2 }' | \
		sed 's/,$//' | \
		sed 's/"//g' | \
		xargs curl -L | \
		tar xz --strip-components 1 && \
		cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
		cmake --build build && \
		cmake --install build

WORKDIR /go/src/app

COPY --link go.mod go.sum ./
RUN go mod download

WORKDIR /go/src/app/caddy
COPY caddy/go.mod caddy/go.sum ./
RUN go mod download

WORKDIR /go/src/app
COPY --link . ./

# See https://github.com/docker-library/php/blob/master/8.3/alpine3.20/zts/Dockerfile#L53-L55
ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS"
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.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' -X 'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp' -X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy'" -buildvcs=true && \
	setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
	([ -z "${NO_COMPRESS}" ] && upx --best /usr/local/bin/frankenphp || true) && \
	frankenphp version && \
	frankenphp build-info

WORKDIR /go/src/app


FROM common AS runner

ENV GODEBUG=cgocheck=0

# copy watcher shared library (libgcc and libstdc++ are needed for the watcher)
COPY --from=builder /usr/local/lib/libwatcher* /usr/local/lib/
RUN apk add --no-cache libstdc++ && \
	ldconfig /usr/local/lib

COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
	frankenphp version && \
	frankenphp build-info


================================================
FILE: app_checksum.txt
================================================


================================================
FILE: build-static.sh
================================================
#!/bin/bash

set -o errexit
set -x

if ! type "git" >/dev/null 2>&1; then
	echo "The \"git\" command must be installed."
	exit 1
fi

CURRENT_DIR=$(pwd)

arch="$(uname -m)"
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
[ "$os" = "darwin" ] && os="mac"

# Supported variables:
# - PHP_VERSION: PHP version to build (default: "8.4")
# - PHP_EXTENSIONS: PHP extensions to build (default: ${defaultExtensions} set below)
# - PHP_EXTENSION_LIBS: PHP extension libraries to build (default: ${defaultExtensionLibs} set below)
# - FRANKENPHP_VERSION: FrankenPHP version (default: current Git commit)
# - EMBED: Path to the PHP app to embed (default: none)
# - DEBUG_SYMBOLS: Enable debug symbols if set to 1 (default: none)
# - MIMALLOC: Use mimalloc as the allocator if set to 1 (default: none)
# - XCADDY_ARGS: Additional arguments to pass to xcaddy
# - RELEASE: [maintainer only] Create a GitHub release if set to 1 (default: none)

# - SPC_REL_TYPE: Release type to download (accept "source" and "binary", default: "source")
# - SPC_OPT_BUILD_ARGS: Additional arguments to pass to spc build
# - SPC_OPT_DOWNLOAD_ARGS: Additional arguments to pass to spc download
# - SPC_LIBC: Set to glibc to build with GNU toolchain (default: musl)

# init spc command, if we use spc binary, just use it instead of fetching source
if [ -z "${SPC_REL_TYPE}" ]; then
	SPC_REL_TYPE="source"
fi
# init spc libc
if [ -z "${SPC_LIBC}" ]; then
	if [ "${os}" = "linux" ]; then
		SPC_LIBC="musl"
	fi
fi
# init spc build additional args
if [ -z "${SPC_OPT_BUILD_ARGS}" ]; then
	SPC_OPT_BUILD_ARGS=""
fi
if [ "${SPC_LIBC}" = "musl" ] && [[ "${SPC_OPT_BUILD_ARGS}" != *"--disable-opcache-jit"* ]]; then
	SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --disable-opcache-jit"
fi
# init spc download additional args
if [ -z "${SPC_OPT_DOWNLOAD_ARGS}" ]; then
	SPC_OPT_DOWNLOAD_ARGS="--ignore-cache-sources=php-src --retry 5"
	if [ "${SPC_LIBC}" = "musl" ]; then
		SPC_OPT_DOWNLOAD_ARGS="${SPC_OPT_DOWNLOAD_ARGS} --prefer-pre-built"
	fi
fi
# if we need debug symbols, disable strip
if [ -n "${DEBUG_SYMBOLS}" ]; then
	SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --no-strip"
fi
# php version to build
if [ -z "${PHP_VERSION}" ]; then
	get_latest_php_version() {
		input="$1"
		json=$(curl -fsSL "https://www.php.net/releases/index.php?json&version=$input" 2>/dev/null || curl -fsSL "https://phpmirror.static-php.dev/releases/index.php?json&version=$input")
		latest=$(echo "$json" | jq -r '.version')

		if [[ "$latest" == "$input"* ]]; then
			echo "$latest"
		else
			echo "$input"
		fi
	}

	PHP_VERSION="$(get_latest_php_version "8.5")"
	export PHP_VERSION
fi
# default extension set
defaultExtensions="amqp,apcu,ast,bcmath,brotli,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,ftp,gd,gmp,gettext,iconv,igbinary,imagick,intl,ldap,lz4,mbregex,mbstring,memcached,mysqli,mysqlnd,opcache,openssl,password-argon2,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,xsl,xz,zip,zlib,yaml,zstd"
defaultExtensionLibs="libavif,nghttp2,nghttp3,ngtcp2,watcher"

if [ -z "${FRANKENPHP_VERSION}" ]; then
	FRANKENPHP_VERSION="$(git rev-parse --verify HEAD)"
	export FRANKENPHP_VERSION
elif [ -d ".git/" ]; then
	CURRENT_REF="$(git rev-parse --abbrev-ref HEAD)"
	export CURRENT_REF

	if echo "${FRANKENPHP_VERSION}" | grep -F -q "."; then
		# Tag

		# Trim "v" prefix if any
		FRANKENPHP_VERSION=${FRANKENPHP_VERSION#v}
		export FRANKENPHP_VERSION

		git checkout "v${FRANKENPHP_VERSION}"
	else
		git checkout "${FRANKENPHP_VERSION}"
	fi
fi

if [ -n "${CLEAN}" ]; then
	rm -Rf dist/
	go clean -cache
fi

mkdir -p dist/
cd dist/

if type "brew" >/dev/null 2>&1; then
	if ! type "composer" >/dev/null; then
		packages="composer"
	fi
	if ! type "go" >/dev/null 2>&1; then
		packages="${packages} go"
	fi
	if [ -n "${RELEASE}" ] && ! type "gh" >/dev/null 2>&1; then
		packages="${packages} gh"
	fi

	if [ -n "${packages}" ]; then
		# shellcheck disable=SC2086
		brew install --formula --quiet ${packages}
	fi
fi

if [ "${SPC_REL_TYPE}" = "binary" ]; then
	mkdir -p static-php-cli/
	cd static-php-cli/
	if [[ "${arch}" =~ "arm" ]]; then
		dl_arch="aarch64"
	else
		dl_arch="${arch}"
	fi
	curl -o spc -fsSL "https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-${dl_arch}"
	chmod +x spc
	spcCommand="./spc"
elif [ -d "static-php-cli/src" ]; then
	cd static-php-cli/
	git pull
	composer install --no-dev -a --no-interaction
	spcCommand="./bin/spc"
else
	git clone --depth 1 https://github.com/crazywhalecc/static-php-cli --branch main
	cd static-php-cli/
	composer install --no-dev -a --no-interaction
	spcCommand="./bin/spc"
fi

# turn potentially relative EMBED path into absolute path
if [ -n "${EMBED}" ]; then
	if [[ "${EMBED}" != /* ]]; then
		EMBED="${CURRENT_DIR}/${EMBED}"
	fi
fi

# Extensions to build
if [ -z "${PHP_EXTENSIONS}" ]; then
	# enable EMBED mode, first check if project has dumped extensions
	if [ -n "${EMBED}" ] && [ -f "${EMBED}/composer.json" ] && [ -f "${EMBED}/composer.lock" ] && [ -f "${EMBED}/vendor/composer/installed.json" ]; then
		cd "${EMBED}"
		# read the extensions using spc dump-extensions
		PHP_EXTENSIONS=$(${spcCommand} dump-extensions "${EMBED}" --format=text --no-dev --no-ext-output="${defaultExtensions}")
	else
		PHP_EXTENSIONS="${defaultExtensions}"
	fi
fi

# Additional libraries to build
if [ -z "${PHP_EXTENSION_LIBS}" ]; then
	PHP_EXTENSION_LIBS="${defaultExtensionLibs}"
fi

# The Brotli library must always be built as it is required by http://github.com/dunglas/caddy-cbrotli
if ! echo "${PHP_EXTENSION_LIBS}" | grep -q "\bbrotli\b"; then
	PHP_EXTENSION_LIBS="${PHP_EXTENSION_LIBS},brotli"
fi

# The mimalloc library must be built if MIMALLOC is true
if [ -n "${MIMALLOC}" ]; then
	if ! echo "${PHP_EXTENSION_LIBS}" | grep -q "\bmimalloc\b"; then
		PHP_EXTENSION_LIBS="${PHP_EXTENSION_LIBS},mimalloc"
	fi
fi

# Embed PHP app, if any
if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then
	# shellcheck disable=SC2089
	SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --with-frankenphp-app='${EMBED}'"
fi

SPC_OPT_INSTALL_ARGS="go-xcaddy"
if [ -z "${DEBUG_SYMBOLS}" ] && [ -z "${NO_COMPRESS}" ] && [ "${os}" = "linux" ]; then
	SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --with-upx-pack"
	SPC_OPT_INSTALL_ARGS="${SPC_OPT_INSTALL_ARGS} upx"
fi

export SPC_DEFAULT_C_FLAGS="-fPIC -O2"
if [ -n "${DEBUG_SYMBOLS}" ]; then
	SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="${SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS} -fPIE -g"
else
	SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="${SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS} -fPIE -fstack-protector-strong -O2 -w -s"
fi
export SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS
if [ -z "$SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES" ]; then
	export SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="--with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli"
fi

# Build FrankenPHP
${spcCommand} doctor --auto-fix
for pkg in ${SPC_OPT_INSTALL_ARGS}; do
	${spcCommand} install-pkg "${pkg}"
done
# shellcheck disable=SC2086
${spcCommand} download --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}" --for-libs="${PHP_EXTENSION_LIBS}" ${SPC_OPT_DOWNLOAD_ARGS}
export FRANKENPHP_SOURCE_PATH="${CURRENT_DIR}"
# shellcheck disable=SC2086,SC2090
${spcCommand} build --enable-zts --build-embed --build-frankenphp ${SPC_OPT_BUILD_ARGS} "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}"

if [ -n "$CI" ]; then
	rm -rf ./downloads
	rm -rf ./source
fi

cd ../..

bin="dist/frankenphp-${os}-${arch}"
cp "dist/static-php-cli/buildroot/bin/frankenphp" "${bin}"
"${bin}" version
"${bin}" build-info

if [ -n "${RELEASE}" ]; then
	gh release upload "v${FRANKENPHP_VERSION}" "${bin}" --repo dunglas/frankenphp --clobber
fi

if [ -n "${CURRENT_REF}" ]; then
	git checkout "${CURRENT_REF}"
fi


================================================
FILE: caddy/admin.go
================================================
package caddy

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/caddyserver/caddy/v2"
	"github.com/dunglas/frankenphp"
)

type FrankenPHPAdmin struct {
}

// if the id starts with "admin.api" the module will register AdminRoutes via module.Routes()
func (FrankenPHPAdmin) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "admin.api.frankenphp",
		New: func() caddy.Module { return new(FrankenPHPAdmin) },
	}
}

// EXPERIMENTAL: These routes are not yet stable and may change in the future.
func (admin FrankenPHPAdmin) Routes() []caddy.AdminRoute {
	return []caddy.AdminRoute{
		{
			Pattern: "/frankenphp/workers/restart",
			Handler: caddy.AdminHandlerFunc(admin.restartWorkers),
		},
		{
			Pattern: "/frankenphp/threads",
			Handler: caddy.AdminHandlerFunc(admin.threads),
		},
	}
}

func (admin *FrankenPHPAdmin) restartWorkers(w http.ResponseWriter, r *http.Request) error {
	if r.Method != http.MethodPost {
		return admin.error(http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
	}

	frankenphp.RestartWorkers()
	caddy.Log().Info("workers restarted from admin api")
	admin.success(w, "workers restarted successfully\n")

	return nil
}

func (admin *FrankenPHPAdmin) threads(w http.ResponseWriter, _ *http.Request) error {
	debugState := frankenphp.DebugState()
	prettyJson, err := json.MarshalIndent(debugState, "", "    ")
	if err != nil {
		return admin.error(http.StatusInternalServerError, err)
	}

	return admin.success(w, string(prettyJson))
}

func (admin *FrankenPHPAdmin) success(w http.ResponseWriter, message string) error {
	w.WriteHeader(http.StatusOK)
	_, err := w.Write([]byte(message))
	return err
}

func (admin *FrankenPHPAdmin) error(statusCode int, err error) error {
	return caddy.APIError{HTTPStatus: statusCode, Err: err}
}


================================================
FILE: caddy/admin_test.go
================================================
package caddy_test

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync"
	"testing"

	"github.com/dunglas/frankenphp/internal/fastabs"

	"github.com/caddyserver/caddy/v2/caddytest"
	"github.com/dunglas/frankenphp"
	"github.com/stretchr/testify/assert"
)

func TestRestartWorkerViaAdminApi(t *testing.T) {
	tester := caddytest.NewTester(t)
	tester.InitServer(`
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`

			frankenphp {
				worker ../testdata/worker-with-counter.php 1
			}
		}

		localhost:`+testPort+` {
			route {
				root ../testdata
				rewrite worker-with-counter.php
				php
			}
		}
		`, "caddyfile")

	tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:1")
	tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:2")

	assertAdminResponse(t, tester, "POST", "workers/restart", http.StatusOK, "workers restarted successfully\n")

	tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:1")
}

func TestShowTheCorrectThreadDebugStatus(t *testing.T) {
	tester := caddytest.NewTester(t)
	tester.InitServer(`
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`

			frankenphp {
				num_threads 3
				max_threads 6
				worker ../testdata/worker-with-counter.php 1
				worker ../testdata/index.php 1
			}
		}

		localhost:`+testPort+` {
			route {
				root ../testdata
				rewrite worker-with-counter.php
				php
			}
		}
		`, "caddyfile")

	debugState := getDebugState(t, tester)

	// assert that the correct threads are present in the thread info
	assert.Equal(t, debugState.ThreadDebugStates[0].State, "ready")
	assert.Contains(t, debugState.ThreadDebugStates[1].Name, "worker-with-counter.php")
	assert.Contains(t, debugState.ThreadDebugStates[2].Name, "index.php")
	assert.Equal(t, debugState.ReservedThreadCount, 3)
	assert.Len(t, debugState.ThreadDebugStates, 3)
}

func TestAutoScaleWorkerThreads(t *testing.T) {
	wg := sync.WaitGroup{}
	maxTries := 10
	requestsPerTry := 200
	tester := caddytest.NewTester(t)
	tester.InitServer(`
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`

			frankenphp {
				max_threads 10
				num_threads 2
				worker ../testdata/sleep.php {
					num 1
					max_threads 3
				}
			}
		}

		localhost:`+testPort+` {
			route {
				root ../testdata
				rewrite sleep.php
				php
			}
		}
		`, "caddyfile")

	// spam an endpoint that simulates IO
	endpoint := "http://localhost:" + testPort + "/?sleep=2&work=1000"
	amountOfThreads := getNumThreads(t, tester)

	// try to spawn the additional threads by spamming the server
	for range maxTries {
		wg.Add(requestsPerTry)
		for range requestsPerTry {
			go func() {
				tester.AssertGetResponse(endpoint, http.StatusOK, "slept for 2 ms and worked for 1000 iterations")
				wg.Done()
			}()
		}
		wg.Wait()

		amountOfThreads = getNumThreads(t, tester)
		if amountOfThreads > 2 {
			break
		}
	}

	assert.NotEqual(t, amountOfThreads, 2, "at least one thread should have been auto-scaled")
	assert.LessOrEqual(t, amountOfThreads, 4, "at most 3 max_threads + 1 regular thread should be present")
}

// Note this test requires at least 2x40MB available memory for the process
func TestAutoScaleRegularThreadsOnAutomaticThreadLimit(t *testing.T) {
	wg := sync.WaitGroup{}
	maxTries := 10
	requestsPerTry := 200
	tester := caddytest.NewTester(t)
	tester.InitServer(`
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`

			frankenphp {
				max_threads auto
				num_threads 1
				php_ini memory_limit 40M # a reasonable limit for the test
			}
		}

		localhost:`+testPort+` {
			route {
				root ../testdata
				php
			}
		}
		`, "caddyfile")

	// spam an endpoint that simulates IO
	endpoint := "http://localhost:" + testPort + "/sleep.php?sleep=2&work=1000"
	amountOfThreads := getNumThreads(t, tester)

	// try to spawn the additional threads by spamming the server
	for range maxTries {
		wg.Add(requestsPerTry)
		for range requestsPerTry {
			go func() {
				tester.AssertGetResponse(endpoint, http.StatusOK, "slept for 2 ms and worked for 1000 iterations")
				wg.Done()
			}()
		}
		wg.Wait()

		amountOfThreads = getNumThreads(t, tester)
		if amountOfThreads > 1 {
			break
		}
	}

	// assert that there are now more threads present
	assert.NotEqual(t, amountOfThreads, 1)
}

func assertAdminResponse(t *testing.T, tester *caddytest.Tester, method string, path string, expectedStatus int, expectedBody string) {
	adminUrl := "http://localhost:2999/frankenphp/"
	r, err := http.NewRequest(method, adminUrl+path, nil)
	assert.NoError(t, err)
	if expectedBody == "" {
		_ = tester.AssertResponseCode(r, expectedStatus)
		return
	}
	_, _ = tester.AssertResponse(r, expectedStatus, expectedBody)
}

func getAdminResponseBody(t *testing.T, tester *caddytest.Tester, method string, path string) string {
	adminUrl := "http://localhost:2999/frankenphp/"
	r, err := http.NewRequest(method, adminUrl+path, nil)
	assert.NoError(t, err)
	resp := tester.AssertResponseCode(r, http.StatusOK)
	defer resp.Body.Close()
	bytes, err := io.ReadAll(resp.Body)
	assert.NoError(t, err)

	return string(bytes)
}

func getDebugState(t *testing.T, tester *caddytest.Tester) frankenphp.FrankenPHPDebugState {
	t.Helper()
	threadStates := getAdminResponseBody(t, tester, "GET", "threads")

	var debugStates frankenphp.FrankenPHPDebugState
	err := json.Unmarshal([]byte(threadStates), &debugStates)
	assert.NoError(t, err)

	return debugStates
}

func getNumThreads(t *testing.T, tester *caddytest.Tester) int {
	t.Helper()
	return len(getDebugState(t, tester).ThreadDebugStates)
}

func TestAddModuleWorkerViaAdminApi(t *testing.T) {
	// Initialize a server with admin API enabled
	tester := caddytest.NewTester(t)
	tester.InitServer(`
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`
		}

		localhost:`+testPort+` {
			route {
				root ../testdata
				php
			}
		}
		`, "caddyfile")

	// Get initial debug state to check number of workers
	initialDebugState := getDebugState(t, tester)
	initialWorkerCount := 0
	for _, thread := range initialDebugState.ThreadDebugStates {
		if strings.HasPrefix(thread.Name, "Worker PHP Thread") {
			initialWorkerCount++
		}
	}

	// Create a Caddyfile configuration with a module worker
	workerConfig := `
	{
		skip_install_trust
		admin localhost:2999
		http_port ` + testPort + `
	}

	localhost:` + testPort + ` {
		route {
			root ../testdata
			php {
				worker ../testdata/worker-with-counter.php 1
			}
		}
	}
	`

	// Send the configuration to the admin API
	adminUrl := "http://localhost:2999/load"
	r, err := http.NewRequest("POST", adminUrl, bytes.NewBufferString(workerConfig))
	assert.NoError(t, err)
	r.Header.Set("Content-Type", "text/caddyfile")
	resp := tester.AssertResponseCode(r, http.StatusOK)
	defer resp.Body.Close()

	// Get the updated debug state to check if the worker was added
	updatedDebugState := getDebugState(t, tester)
	updatedWorkerCount := 0
	workerFound := false
	filename, _ := fastabs.FastAbs("../testdata/worker-with-counter.php")
	for _, thread := range updatedDebugState.ThreadDebugStates {
		if strings.HasPrefix(thread.Name, "Worker PHP Thread") {
			updatedWorkerCount++
			if thread.Name == "Worker PHP Thread - "+filename {
				workerFound = true
			}
		}
	}

	// Assert that the worker was added
	assert.Greater(t, updatedWorkerCount, initialWorkerCount, "Worker count should have increased")
	assert.True(t, workerFound, fmt.Sprintf("Worker with name %q should be found", "Worker PHP Thread - "+filename))

	// Make a request to the worker to verify it's working
	tester.AssertGetResponse("http://localhost:"+testPort+"/worker-with-counter.php", http.StatusOK, "requests:1")
}


================================================
FILE: caddy/app.go
================================================
package caddy

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddyconfig"
	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
	"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
	"github.com/dunglas/frankenphp"
	"github.com/dunglas/frankenphp/internal/fastabs"
)

var (
	options   []frankenphp.Option
	optionsMU sync.RWMutex
)

// EXPERIMENTAL: RegisterWorkers provides a way for extensions to register frankenphp.Workers
func RegisterWorkers(name, fileName string, num int, wo ...frankenphp.WorkerOption) frankenphp.Workers {
	w, opt := frankenphp.WithExtensionWorkers(name, fileName, num, wo...)

	optionsMU.Lock()
	options = append(options, opt)
	optionsMU.Unlock()

	return w
}

// FrankenPHPApp represents the global "frankenphp" directive in the Caddyfile
// it's responsible for starting up the global PHP instance and all threads
//
//	{
//		frankenphp {
//			num_threads 20
//		}
//	}
type FrankenPHPApp struct {
	// NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.
	NumThreads int `json:"num_threads,omitempty"`
	// MaxThreads limits how many threads can be started at runtime. Default 2x NumThreads
	MaxThreads int `json:"max_threads,omitempty"`
	// Workers configures the worker scripts to start
	Workers []workerConfig `json:"workers,omitempty"`
	// Overwrites the default php ini configuration
	PhpIni map[string]string `json:"php_ini,omitempty"`
	// The maximum amount of time a request may be stalled waiting for a thread
	MaxWaitTime time.Duration `json:"max_wait_time,omitempty"`
	// The maximum amount of time an autoscaled thread may be idle before being deactivated
	MaxIdleTime time.Duration `json:"max_idle_time,omitempty"`

	opts    []frankenphp.Option
	metrics frankenphp.Metrics
	ctx     context.Context
	logger  *slog.Logger
}

var iniError = errors.New(`"php_ini" must be in the format: php_ini "<key>" "<value>"`)

// CaddyModule returns the Caddy module information.
func (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "frankenphp",
		New: func() caddy.Module { return &f },
	}
}

// Provision sets up the module.
func (f *FrankenPHPApp) Provision(ctx caddy.Context) error {
	f.ctx = ctx
	f.logger = ctx.Slogger()

	// We have at least 7 hardcoded options
	f.opts = make([]frankenphp.Option, 0, 7+len(options))

	if httpApp, err := ctx.AppIfConfigured("http"); err == nil {
		if httpApp.(*caddyhttp.App).Metrics != nil {
			f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
		}
	} else {
		// if the http module is not configured (this should never happen) then collect the metrics by default
		if errors.Is(err, caddy.ErrNotConfigured) {
			f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
		} else {
			// the http module failed to provision due to invalid configuration
			return fmt.Errorf("failed to provision caddy http: %w", err)
		}
	}

	return nil
}

func (f *FrankenPHPApp) generateUniqueModuleWorkerName(filepath string) string {
	var i uint
	filepath, _ = fastabs.FastAbs(filepath)
	name := "m#" + filepath

retry:
	for _, wc := range f.Workers {
		if wc.Name == name {
			name = fmt.Sprintf("m#%s_%d", filepath, i)
			i++

			goto retry
		}
	}

	return name
}

func (f *FrankenPHPApp) addModuleWorkers(workers ...workerConfig) ([]workerConfig, error) {
	for i := range workers {
		w := &workers[i]

		if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(w.FileName) {
			w.FileName = filepath.Join(frankenphp.EmbeddedAppPath, w.FileName)
		}

		if w.Name == "" {
			w.Name = f.generateUniqueModuleWorkerName(w.FileName)
		} else if !strings.HasPrefix(w.Name, "m#") {
			w.Name = "m#" + w.Name
		}

		f.Workers = append(f.Workers, *w)
	}

	return workers, nil
}

func (f *FrankenPHPApp) Start() error {
	repl := caddy.NewReplacer()

	optionsMU.RLock()
	f.opts = append(f.opts, options...)
	optionsMU.RUnlock()

	f.opts = append(f.opts,
		frankenphp.WithContext(f.ctx),
		frankenphp.WithLogger(f.logger),
		frankenphp.WithNumThreads(f.NumThreads),
		frankenphp.WithMaxThreads(f.MaxThreads),
		frankenphp.WithMetrics(f.metrics),
		frankenphp.WithPhpIni(f.PhpIni),
		frankenphp.WithMaxWaitTime(f.MaxWaitTime),
		frankenphp.WithMaxIdleTime(f.MaxIdleTime),
	)

	for _, w := range f.Workers {
		w.options = append(w.options,
			frankenphp.WithWorkerEnv(w.Env),
			frankenphp.WithWorkerWatchMode(w.Watch),
			frankenphp.WithWorkerMaxFailures(w.MaxConsecutiveFailures),
			frankenphp.WithWorkerMaxThreads(w.MaxThreads),
			frankenphp.WithWorkerRequestOptions(w.requestOptions...),
		)

		f.opts = append(f.opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, w.options...))
	}

	frankenphp.Shutdown()
	if err := frankenphp.Init(f.opts...); err != nil {
		return err
	}

	return nil
}

func (f *FrankenPHPApp) Stop() error {
	if f.logger.Enabled(f.ctx, slog.LevelInfo) {
		f.logger.LogAttrs(f.ctx, slog.LevelInfo, "FrankenPHP stopped 🐘")
	}

	// attempt a graceful shutdown if caddy is exiting
	// note: Exiting() is currently marked as 'experimental'
	// https://github.com/caddyserver/caddy/blob/e76405d55058b0a3e5ba222b44b5ef00516116aa/caddy.go#L810
	if caddy.Exiting() {
		frankenphp.Shutdown()
	}

	// reset the configuration so it doesn't bleed into later tests
	f.Workers = nil
	f.NumThreads = 0
	f.MaxWaitTime = 0
	f.MaxIdleTime = 0

	optionsMU.Lock()
	options = nil
	optionsMU.Unlock()

	return nil
}

// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
	for d.Next() {
		for d.NextBlock(0) {
			// when adding a new directive, also update the allowedDirectives error message
			switch d.Val() {
			case "num_threads":
				if !d.NextArg() {
					return d.ArgErr()
				}

				v, err := strconv.ParseUint(d.Val(), 10, 32)
				if err != nil {
					return err
				}

				f.NumThreads = int(v)
			case "max_threads":
				if !d.NextArg() {
					return d.ArgErr()
				}

				if d.Val() == "auto" {
					f.MaxThreads = -1
					continue
				}

				v, err := strconv.ParseUint(d.Val(), 10, 32)
				if err != nil {
					return err
				}

				f.MaxThreads = int(v)
			case "max_wait_time":
				if !d.NextArg() {
					return d.ArgErr()
				}

				v, err := time.ParseDuration(d.Val())
				if err != nil {
					return d.Err("max_wait_time must be a valid duration (example: 10s)")
				}

				f.MaxWaitTime = v
			case "max_idle_time":
				if !d.NextArg() {
					return d.ArgErr()
				}

				v, err := time.ParseDuration(d.Val())
				if err != nil {
					return d.Err("max_idle_time must be a valid duration (example: 30s)")
				}

				f.MaxIdleTime = v
			case "php_ini":
				parseIniLine := func(d *caddyfile.Dispenser) error {
					key := d.Val()
					if !d.NextArg() {
						return d.WrapErr(iniError)
					}
					if f.PhpIni == nil {
						f.PhpIni = make(map[string]string)
					}
					f.PhpIni[key] = d.Val()
					if d.NextArg() {
						return d.WrapErr(iniError)
					}

					return nil
				}

				isBlock := false
				for d.NextBlock(1) {
					isBlock = true
					err := parseIniLine(d)
					if err != nil {
						return err
					}
				}

				if !isBlock {
					if !d.NextArg() {
						return d.WrapErr(iniError)
					}
					err := parseIniLine(d)
					if err != nil {
						return err
					}
				}

			case "worker":
				wc, err := unmarshalWorker(d)
				if err != nil {
					return err
				}
				if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
					wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
				}
				if strings.HasPrefix(wc.Name, "m#") {
					return d.Errf(`global worker names must not start with "m#": %q`, wc.Name)
				}
				// check for duplicate workers
				for _, existingWorker := range f.Workers {
					if existingWorker.FileName == wc.FileName {
						return d.Errf("global workers must not have duplicate filenames: %q", wc.FileName)
					}
				}

				f.Workers = append(f.Workers, wc)
			default:
				return wrongSubDirectiveError("frankenphp", "num_threads, max_threads, php_ini, worker, max_wait_time, max_idle_time", d.Val())
			}
		}
	}

	if f.MaxThreads > 0 && f.NumThreads > 0 && f.MaxThreads < f.NumThreads {
		return d.Err(`"max_threads"" must be greater than or equal to "num_threads"`)
	}

	return nil
}

func parseGlobalOption(d *caddyfile.Dispenser, _ any) (any, error) {
	app := &FrankenPHPApp{}
	if err := app.UnmarshalCaddyfile(d); err != nil {
		return nil, err
	}

	// tell Caddyfile adapter that this is the JSON for an app
	return httpcaddyfile.App{
		Name:  "frankenphp",
		Value: caddyconfig.JSON(app, nil),
	}, nil
}

var (
	_ caddy.App         = (*FrankenPHPApp)(nil)
	_ caddy.Provisioner = (*FrankenPHPApp)(nil)
)


================================================
FILE: caddy/br-skip.go
================================================
//go:build nobrotli

package caddy

var brotli = false


================================================
FILE: caddy/br.go
================================================
//go:build !nobrotli

package caddy

var brotli = true


================================================
FILE: caddy/caddy.go
================================================
// Package caddy provides a PHP module for the Caddy web server.
// FrankenPHP embeds the PHP interpreter directly in Caddy, giving it the ability to run your PHP scripts directly.
// No PHP FPM required!
package caddy

import (
	"fmt"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
)

const (
	defaultDocumentRoot = "public"
	defaultWatchPattern = "./**/*.{env,php,twig,yaml,yml}"
)

func init() {
	caddy.RegisterModule(FrankenPHPApp{})
	caddy.RegisterModule(FrankenPHPModule{})
	caddy.RegisterModule(FrankenPHPAdmin{})

	httpcaddyfile.RegisterGlobalOption("frankenphp", parseGlobalOption)

	httpcaddyfile.RegisterHandlerDirective("php", parseCaddyfile)
	httpcaddyfile.RegisterDirectiveOrder("php", "before", "file_server")

	httpcaddyfile.RegisterDirective("php_server", parsePhpServer)
	httpcaddyfile.RegisterDirectiveOrder("php_server", "before", "file_server")
}

// wrongSubDirectiveError returns a nice error message.
func wrongSubDirectiveError(module string, allowedDirectives string, wrongValue string) error {
	return fmt.Errorf("unknown %q subdirective: %s (allowed directives are: %s)", module, wrongValue, allowedDirectives)
}


================================================
FILE: caddy/caddy_test.go
================================================
package caddy_test

import (
	"bytes"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddytest"
	"github.com/dunglas/frankenphp/internal/fastabs"
	"github.com/prometheus/client_golang/prometheus/testutil"
	"github.com/stretchr/testify/require"
)

// initServer initializes a Caddy test server and waits for it to be ready.
// After InitServer, it polls the server to handle a race condition on macOS where
// SO_REUSEPORT can briefly route connections to the old listener being shut down,
// resulting in "connection reset by peer".
func initServer(t *testing.T, tester *caddytest.Tester, config string, format string) {
	t.Helper()
	tester.InitServer(config, format)

	client := &http.Client{Timeout: 1 * time.Second}
	require.Eventually(t, func() bool {
		resp, err := client.Get("http://localhost:" + testPort)
		if err != nil {
			return false
		}

		require.NoError(t, resp.Body.Close())

		return true
	}, 5*time.Second, 100*time.Millisecond, "server failed to become ready")
}

var testPort = "9080"

// skipIfSymlinkNotValid skips the test if the given path is not a valid symlink
func skipIfSymlinkNotValid(t *testing.T, path string) {
	t.Helper()

	info, err := os.Lstat(path)
	if err != nil {
		t.Skipf("symlink test skipped: cannot stat %s: %v", path, err)
	}

	if info.Mode()&os.ModeSymlink == 0 {
		t.Skipf("symlink test skipped: %s is not a symlink (git may not support symlinks on this platform)", path)
	}
}

// escapeMetricLabel escapes backslashes in label values for Prometheus text format
func escapeMetricLabel(s string) string {
	return strings.ReplaceAll(s, "\\", "\\\\")
}

func TestMain(m *testing.M) {
	// setup custom environment vars for TestOsEnv
	if os.Setenv("ENV1", "value1") != nil || os.Setenv("ENV2", "value2") != nil {
		fmt.Println("Failed to set environment variables for tests")
		os.Exit(1)
	}

	os.Exit(m.Run())
}

func TestPHP(t *testing.T) {
	var wg sync.WaitGroup
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`
			https_port 9443
		}

		localhost:`+testPort+` {
			route {
				php {
					root ../testdata
				}
			}
		}
		`, "caddyfile")

	for i := range 100 {
		wg.Add(1)

		go func(i int) {
			tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
			wg.Done()
		}(i)
	}
	wg.Wait()
}

func TestLargeRequest(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`
			https_port 9443
		}

		localhost:`+testPort+` {
			route {
				php {
					root ../testdata
				}
			}
		}
		`, "caddyfile")

	tester.AssertPostResponseBody(
		"http://localhost:"+testPort+"/large-request.php",
		[]string{},
		bytes.NewBufferString(strings.Repeat("f", 1_048_576)),
		http.StatusOK,
		"Request body size: 1048576 (unknown)",
	)
}

func TestWorker(t *testing.T) {
	var wg sync.WaitGroup
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`
			https_port 9443

			frankenphp {
				worker ../testdata/index.php 2
			}
		}

		localhost:`+testPort+` {
			route {
				php {
					root ../testdata
				}
			}
		}
		`, "caddyfile")

	for i := range 100 {
		wg.Add(1)

		go func(i int) {
			tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
			wg.Done()
		}(i)
	}
	wg.Wait()
}

func TestGlobalAndModuleWorker(t *testing.T) {
	var wg sync.WaitGroup
	testPortNum, _ := strconv.Atoi(testPort)
	testPortTwo := strconv.Itoa(testPortNum + 1)
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999

			frankenphp {
				worker {
					file ../testdata/worker-with-env.php
					num 1
					env APP_ENV global
				}
			}
		}

		http://localhost:`+testPort+` {
			route {
				php {
					root ../testdata
					worker {
						file worker-with-env.php
						num 2
						env APP_ENV module
					}
				}
			}
		}

		http://localhost:`+testPortTwo+` {
			route {
				php {
					root ../testdata
				}
			}
		}
		`, "caddyfile")

	for i := range 10 {
		wg.Add(1)

		go func(i int) {
			tester.AssertGetResponse("http://localhost:"+testPort+"/worker-with-env.php", http.StatusOK, "Worker has APP_ENV=module")
			tester.AssertGetResponse("http://localhost:"+testPortTwo+"/worker-with-env.php", http.StatusOK, "Worker has APP_ENV=global")
			wg.Done()
		}(i)
	}
	wg.Wait()
}

func TestModuleWorkerInheritsEnv(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
		}

		http://localhost:`+testPort+` {
			route {
				php {
					root ../testdata
					env APP_ENV inherit_this
					worker worker-with-env.php
				}
			}
		}
		`, "caddyfile")

	tester.AssertGetResponse("http://localhost:"+testPort+"/worker-with-env.php", http.StatusOK, "Worker has APP_ENV=inherit_this")
}

func TestNamedModuleWorkers(t *testing.T) {
	var wg sync.WaitGroup
	testPortNum, _ := strconv.Atoi(testPort)
	testPortTwo := strconv.Itoa(testPortNum + 1)
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
		}

		http://localhost:`+testPort+` {
			route {
				php {
					root ../testdata
					worker {
						file worker-with-env.php
						num 2
						env APP_ENV one
						name module1
					}
				}
			}
		}

		http://localhost:`+testPortTwo+` {
			route {
				php {
					root ../testdata
					worker {
						file worker-with-env.php
						num 1
						env APP_ENV two
						name module2
					}
				}
			}
		}
		`, "caddyfile")

	for i := range 10 {
		wg.Add(1)

		go func(i int) {
			tester.AssertGetResponse("http://localhost:"+testPort+"/worker-with-env.php", http.StatusOK, "Worker has APP_ENV=one")
			tester.AssertGetResponse("http://localhost:"+testPortTwo+"/worker-with-env.php", http.StatusOK, "Worker has APP_ENV=two")
			wg.Done()
		}(i)
	}
	wg.Wait()
}

func TestEnv(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`
			https_port 9443

			frankenphp {
				worker {
					file ../testdata/worker-env.php
					num 1
					env FOO bar
				}
			}
		}

		localhost:`+testPort+` {
			route {
				php {
					root ../testdata
					env FOO baz
				}
			}
		}
		`, "caddyfile")

	tester.AssertGetResponse("http://localhost:"+testPort+"/worker-env.php", http.StatusOK, "bazbar")
}

func TestJsonEnv(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
		"admin": {
			"listen": "localhost:2999"
		},
		"apps": {
			"frankenphp": {
			"workers": [
				{
				"env": {
					"FOO": "bar"
				},
				"file_name": "../testdata/worker-env.php",
				"num": 1
				}
			]
			},
			"http": {
			"http_port": `+testPort+`,
			"https_port": 9443,
			"servers": {
				"srv0": {
				"listen": [
					":`+testPort+`"
				],
				"routes": [
					{
					"handle": [
						{
						"handler": "subroute",
						"routes": [
							{
							"handle": [
								{
								"handler": "subroute",
								"routes": [
									{
									"handle": [
										{
										"env": {
											"FOO": "baz"
										},
										"handler": "php",
										"root": "../testdata"
										}
									]
									}
								]
								}
							]
							}
						]
						}
					],
					"match": [
						{
						"host": [
							"localhost"
						]
						}
					],
					"terminal": true
					}
				]
				}
			}
			},
			"pki": {
			"certificate_authorities": {
				"local": {
				"install_trust": false
				}
			}
			}
		}
		}
		`, "json")

	tester.AssertGetResponse("http://localhost:"+testPort+"/worker-env.php", http.StatusOK, "bazbar")
}

func TestCustomCaddyVariablesInEnv(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`
			https_port 9443

			frankenphp {
				worker {
					file ../testdata/worker-env.php
					num 1
					env FOO world
				}
			}
		}

		localhost:`+testPort+` {
			route {
				map 1 {my_customvar} {
					default "hello "
				}
				php {
					root ../testdata
					env FOO {my_customvar}
				}
			}
		}
		`, "caddyfile")

	tester.AssertGetResponse("http://localhost:"+testPort+"/worker-env.php", http.StatusOK, "hello world")
}

func TestPHPServerDirective(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`
			https_port 9443
		}

		localhost:`+testPort+` {
			root ../testdata
			php_server
		}
		`, "caddyfile")

	tester.AssertGetResponse("http://localhost:"+testPort, http.StatusOK, "I am by birth a Genevese (i not set)")
	tester.AssertGetResponse("http://localhost:"+testPort+"/hello.txt", http.StatusOK, "Hello\n")
	tester.AssertGetResponse("http://localhost:"+testPort+"/not-found.txt", http.StatusOK, "I am by birth a Genevese (i not set)")
}

func TestPHPServerDirectiveDisableFileServer(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`
			https_port 9443
			order php_server before respond
		}

		localhost:`+testPort+` {
			root ../testdata
			php_server {
				file_server off
			}
			respond "Not found" 404
		}
		`, "caddyfile")

	tester.AssertGetResponse("http://localhost:"+testPort, http.StatusOK, "I am by birth a Genevese (i not set)")
	tester.AssertGetResponse("http://localhost:"+testPort+"/not-found.txt", http.StatusOK, "I am by birth a Genevese (i not set)")
}

func TestMetrics(t *testing.T) {
	var wg sync.WaitGroup
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
	{
		skip_install_trust
		admin localhost:2999
		http_port `+testPort+`
		https_port 9443
		metrics
	}

	localhost:`+testPort+` {
		route {
			mercure {
				transport local
				anonymous
				publisher_jwt !ChangeMe!
			}

			php {
				root ../testdata
			}
		}
	}

	example.com:`+testPort+` {
		route {
			mercure {
				transport local
				anonymous
				publisher_jwt !ChangeMe!
			}

			php {
				root ../testdata
			}
		}
	}
	`, "caddyfile")

	// Make some requests
	for i := range 10 {
		wg.Add(1)
		go func(i int) {
			tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
			wg.Done()
		}(i)
	}
	wg.Wait()

	// Fetch metrics
	resp, err := http.Get("http://localhost:2999/metrics")
	require.NoError(t, err, "failed to fetch metrics")
	t.Cleanup(func() {
		require.NoError(t, resp.Body.Close())
	})

	// Read and parse metrics
	metrics := new(bytes.Buffer)
	_, err = metrics.ReadFrom(resp.Body)
	require.NoError(t, err, "failed to read metrics")

	cpus := strconv.Itoa(getNumThreads(t, tester))

	// Check metrics
	expectedMetrics := `
	# HELP frankenphp_total_threads Total number of PHP threads
	# TYPE frankenphp_total_threads counter
	frankenphp_total_threads ` + cpus + `

	# HELP frankenphp_busy_threads Number of busy PHP threads
	# TYPE frankenphp_busy_threads gauge
	frankenphp_busy_threads 0
	`

	ctx := caddy.ActiveContext()

	require.NoError(t, testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expectedMetrics), "frankenphp_total_threads", "frankenphp_busy_threads"))
}

func TestWorkerMetrics(t *testing.T) {
	var wg sync.WaitGroup
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
	{
		skip_install_trust
		admin localhost:2999
		http_port `+testPort+`
		https_port 9443
		metrics

		frankenphp {
			worker ../testdata/index.php 2
		}
	}

	localhost:`+testPort+` {
		route {
			php {
				root ../testdata
			}
		}
	}

	example.com:`+testPort+` {
		route {
			php {
				root ../testdata
			}
		}
	}
	`, "caddyfile")

	workerName, _ := fastabs.FastAbs("../testdata/index.php")
	workerName = escapeMetricLabel(workerName)

	// Make some requests
	for i := range 10 {
		wg.Add(1)
		go func(i int) {
			tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
			wg.Done()
		}(i)
	}
	wg.Wait()

	// Fetch metrics
	resp, err := http.Get("http://localhost:2999/metrics")
	require.NoError(t, err, "failed to fetch metrics")
	t.Cleanup(func() {
		require.NoError(t, resp.Body.Close())
	})

	// Read and parse metrics
	metrics := new(bytes.Buffer)
	_, err = metrics.ReadFrom(resp.Body)
	require.NoError(t, err, "failed to read metrics")

	cpus := strconv.Itoa(getNumThreads(t, tester))

	// Check metrics
	expectedMetrics := `
	# HELP frankenphp_total_threads Total number of PHP threads
	# TYPE frankenphp_total_threads counter
	frankenphp_total_threads ` + cpus + `

	# HELP frankenphp_busy_threads Number of busy PHP threads
	# TYPE frankenphp_busy_threads gauge
	frankenphp_busy_threads 2

	# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
	# TYPE frankenphp_busy_workers gauge
	frankenphp_busy_workers{worker="` + workerName + `"} 0

	# HELP frankenphp_total_workers Total number of PHP workers for this worker
	# TYPE frankenphp_total_workers gauge
	frankenphp_total_workers{worker="` + workerName + `"} 2

	# HELP frankenphp_worker_request_count
	# TYPE frankenphp_worker_request_count counter
	frankenphp_worker_request_count{worker="` + workerName + `"} 10

	# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
	# TYPE frankenphp_ready_workers gauge
	frankenphp_ready_workers{worker="` + workerName + `"} 2
	`

	ctx := caddy.ActiveContext()
	require.NoError(t,
		testutil.GatherAndCompare(
			ctx.GetMetricsRegistry(),
			strings.NewReader(expectedMetrics),
			"frankenphp_total_threads",
			"frankenphp_busy_threads",
			"frankenphp_busy_workers",
			"frankenphp_total_workers",
			"frankenphp_worker_request_count",
			"frankenphp_ready_workers",
		))
}

func TestNamedWorkerMetrics(t *testing.T) {
	var wg sync.WaitGroup
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
	{
		skip_install_trust
		admin localhost:2999
		http_port `+testPort+`
		https_port 9443
		metrics

		frankenphp {
			worker {
				name my_app
				file ../testdata/index.php
				num 2
			}
		}
	}

	localhost:`+testPort+` {
		route {
			php {
				root ../testdata
			}
		}
	}
	`, "caddyfile")

	// Make some requests
	for i := range 10 {
		wg.Add(1)
		go func(i int) {
			tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
			wg.Done()
		}(i)
	}
	wg.Wait()

	// Fetch metrics
	resp, err := http.Get("http://localhost:2999/metrics")
	require.NoError(t, err, "failed to fetch metrics")
	t.Cleanup(func() {
		require.NoError(t, resp.Body.Close())
	})

	// Read and parse metrics
	metrics := new(bytes.Buffer)
	_, err = metrics.ReadFrom(resp.Body)
	require.NoError(t, err, "failed to read metrics")

	cpus := strconv.Itoa(getNumThreads(t, tester))

	// Check metrics
	expectedMetrics := `
	# HELP frankenphp_total_threads Total number of PHP threads
	# TYPE frankenphp_total_threads counter
	frankenphp_total_threads ` + cpus + `

	# HELP frankenphp_busy_threads Number of busy PHP threads
	# TYPE frankenphp_busy_threads gauge
	frankenphp_busy_threads 2

	# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
        # TYPE frankenphp_busy_workers gauge
        frankenphp_busy_workers{worker="my_app"} 0

	# HELP frankenphp_total_workers Total number of PHP workers for this worker
	# TYPE frankenphp_total_workers gauge
	frankenphp_total_workers{worker="my_app"} 2

	# HELP frankenphp_worker_request_count
	# TYPE frankenphp_worker_request_count counter
	frankenphp_worker_request_count{worker="my_app"} 10

	# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
	# TYPE frankenphp_ready_workers gauge
	frankenphp_ready_workers{worker="my_app"} 2
	`

	ctx := caddy.ActiveContext()
	require.NoError(t,
		testutil.GatherAndCompare(
			ctx.GetMetricsRegistry(),
			strings.NewReader(expectedMetrics),
			"frankenphp_total_threads",
			"frankenphp_busy_threads",
			"frankenphp_busy_workers",
			"frankenphp_total_workers",
			"frankenphp_worker_request_count",
			"frankenphp_ready_workers",
		),
	)
}

func TestAutoWorkerConfig(t *testing.T) {
	var wg sync.WaitGroup
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
	{
		skip_install_trust
		admin localhost:2999
		http_port `+testPort+`
		https_port 9443
		metrics

		frankenphp {
			worker ../testdata/index.php
		}
	}

	localhost:`+testPort+` {
		route {
			php {
				root ../testdata
			}
		}
	}
	`, "caddyfile")

	workerName, _ := fastabs.FastAbs("../testdata/index.php")
	workerName = escapeMetricLabel(workerName)

	// Make some requests
	for i := range 10 {
		wg.Add(1)
		go func(i int) {
			tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
			wg.Done()
		}(i)
	}
	wg.Wait()

	// Fetch metrics
	resp, err := http.Get("http://localhost:2999/metrics")
	require.NoError(t, err, "failed to fetch metrics")
	t.Cleanup(func() {
		require.NoError(t, resp.Body.Close())
	})

	// Read and parse metrics
	metrics := new(bytes.Buffer)
	_, err = metrics.ReadFrom(resp.Body)
	require.NoError(t, err, "failed to read metrics")

	numThreads := getNumThreads(t, tester)
	cpus := strconv.Itoa(numThreads)
	workers := strconv.Itoa(numThreads - 1)

	// Check metrics
	expectedMetrics := `
	# HELP frankenphp_total_threads Total number of PHP threads
	# TYPE frankenphp_total_threads counter
	frankenphp_total_threads ` + cpus + `

	# HELP frankenphp_busy_threads Number of busy PHP threads
	# TYPE frankenphp_busy_threads gauge
	frankenphp_busy_threads ` + workers + `

	# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
	# TYPE frankenphp_busy_workers gauge
	frankenphp_busy_workers{worker="` + workerName + `"} 0

	# HELP frankenphp_total_workers Total number of PHP workers for this worker
	# TYPE frankenphp_total_workers gauge
	frankenphp_total_workers{worker="` + workerName + `"} ` + workers + `

	# HELP frankenphp_worker_request_count
	# TYPE frankenphp_worker_request_count counter
	frankenphp_worker_request_count{worker="` + workerName + `"} 10

	# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
	# TYPE frankenphp_ready_workers gauge
	frankenphp_ready_workers{worker="` + workerName + `"} ` + workers + `
	`

	ctx := caddy.ActiveContext()
	require.NoError(t,
		testutil.GatherAndCompare(
			ctx.GetMetricsRegistry(),
			strings.NewReader(expectedMetrics),
			"frankenphp_total_threads",
			"frankenphp_busy_threads",
			"frankenphp_busy_workers",
			"frankenphp_total_workers",
			"frankenphp_worker_request_count",
			"frankenphp_ready_workers",
		))
}

func TestAllDefinedServerVars(t *testing.T) {
	documentRoot, _ := filepath.Abs("../testdata/")
	expectedBodyFile, _ := os.ReadFile("../testdata/server-all-vars-ordered.txt")
	expectedBody := string(expectedBodyFile)
	expectedBody = strings.ReplaceAll(expectedBody, "{documentRoot}", documentRoot)
	expectedBody = strings.ReplaceAll(expectedBody, "\r\n", "\n")
	expectedBody = strings.ReplaceAll(expectedBody, "{testPort}", testPort)
	expectedBody = strings.ReplaceAll(expectedBody, documentRoot+"/", documentRoot+string(filepath.Separator))
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`
		}
		localhost:`+testPort+` {
			route {
			    root ../testdata
			    # rewrite to test that the original path is passed as $REQUEST_URI
			    rewrite /server-all-vars-ordered.php/path
				php
			}
		}
		`, "caddyfile")
	tester.AssertPostResponseBody(
		"http://user@localhost:"+testPort+"/original-path?specialChars=%3E\\x00%00</>",
		[]string{
			"Content-Type: application/x-www-form-urlencoded",
			"Content-Length: 14", // maliciously set to 14
			"Special-Chars: <%00>",
			"Host: Malicious Host",
			"X-Empty-Header:",
		},
		bytes.NewBufferString("foo=bar"),
		http.StatusOK,
		expectedBody,
	)
}

func TestPHPIniConfiguration(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`

			frankenphp {
				num_threads 2
				worker ../testdata/ini.php 1
				php_ini upload_max_filesize 100M
				php_ini memory_limit 10000000
			}
		}

		localhost:`+testPort+` {
			route {
				root ../testdata
				php
			}
		}
		`, "caddyfile")

	testSingleIniConfiguration(tester, "upload_max_filesize", "100M")
	testSingleIniConfiguration(tester, "memory_limit", "10000000")
}

func TestPHPIniBlockConfiguration(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`

			frankenphp {
				num_threads 1
				php_ini {
					upload_max_filesize 100M
					memory_limit 20000000
				}
			}
		}

		localhost:`+testPort+` {
			route {
				root ../testdata
				php
			}
		}
		`, "caddyfile")

	testSingleIniConfiguration(tester, "upload_max_filesize", "100M")
	testSingleIniConfiguration(tester, "memory_limit", "20000000")
}

func testSingleIniConfiguration(tester *caddytest.Tester, key string, value string) {
	// test twice to ensure the ini setting is not lost
	for range 2 {
		tester.AssertGetResponse(
			"http://localhost:"+testPort+"/ini.php?key="+key,
			http.StatusOK,
			key+":"+value,
		)
	}
}

func TestOsEnv(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`

			frankenphp {
				num_threads 2
				php_ini variables_order "EGPCS"
				worker ../testdata/env/env.php 1
			}
		}

		localhost:`+testPort+` {
			route {
				root ../testdata
				php
			}
		}
		`, "caddyfile")

	tester.AssertGetResponse(
		"http://localhost:"+testPort+"/env/env.php?keys[]=ENV1&keys[]=ENV2",
		http.StatusOK,
		"ENV1=value1,ENV2=value2",
	)
}

func TestMaxWaitTime(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`

			frankenphp {
				num_threads 1
				max_wait_time 1ns
			}
		}

		localhost:`+testPort+` {
			route {
				root ../testdata
				php
			}
		}
		`, "caddyfile")

	// send 10 requests simultaneously, at least one request should be stalled longer than 1ns
	// since we only have 1 thread, this will cause a 504 Gateway Timeout
	wg := sync.WaitGroup{}
	success := atomic.Bool{}
	wg.Add(10)
	for range 10 {
		go func() {
			statusCode := getStatusCode("http://localhost:"+testPort+"/sleep.php?sleep=10", t)
			if statusCode == http.StatusServiceUnavailable {
				success.Store(true)
			}
			wg.Done()
		}()
	}
	wg.Wait()

	require.True(t, success.Load(), "At least one request should have failed with a 503 Service Unavailable status")
}

func TestMaxWaitTimeWorker(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
			http_port `+testPort+`
			metrics

			frankenphp {
				num_threads 2
				max_wait_time 1ns
				worker {
					num 1
					name service
					file ../testdata/sleep.php
				}
			}
		}

		localhost:`+testPort+` {
			route {
				root ../testdata
				php
			}
		}
		`, "caddyfile")

	// send 10 requests simultaneously, at least one request should be stalled longer than 1ns
	// since we only have 1 thread, this will cause a 504 Gateway Timeout
	wg := sync.WaitGroup{}
	success := atomic.Bool{}
	wg.Add(10)
	for range 10 {
		go func() {
			statusCode := getStatusCode("http://localhost:"+testPort+"/sleep.php?sleep=10&iteration=1", t)
			if statusCode == http.StatusServiceUnavailable {
				success.Store(true)
			}
			wg.Done()
		}()
	}
	wg.Wait()
	require.True(t, success.Load(), "At least one request should have failed with a 503 Service Unavailable status")

	// Fetch metrics
	resp, err := http.Get("http://localhost:2999/metrics")
	require.NoError(t, err, "failed to fetch metrics")
	t.Cleanup(func() {
		require.NoError(t, resp.Body.Close())
	})

	// Read and parse metrics
	metrics := new(bytes.Buffer)
	_, err = metrics.ReadFrom(resp.Body)
	require.NoError(t, err)

	expectedMetrics := `
	# TYPE frankenphp_worker_queue_depth gauge
	frankenphp_worker_queue_depth{worker="service"} 0
	`

	ctx := caddy.ActiveContext()
	require.NoError(t,
		testutil.GatherAndCompare(
			ctx.GetMetricsRegistry(),
			strings.NewReader(expectedMetrics),
			"frankenphp_worker_queue_depth",
		))
}

func getStatusCode(url string, t *testing.T) int {
	req, err := http.NewRequest("GET", url, nil)
	require.NoError(t, err)

	resp, err := http.DefaultClient.Do(req)
	require.NoError(t, err)
	require.NoError(t, resp.Body.Close())

	return resp.StatusCode
}

func TestMultiWorkersMetrics(t *testing.T) {
	var wg sync.WaitGroup
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
	{
		skip_install_trust
		admin localhost:2999
		http_port `+testPort+`
		https_port 9443
		metrics

		frankenphp {
			worker {
				name service1
				file ../testdata/index.php
				num 2
			}
			worker {
				name service2
				file ../testdata/ini.php
				num 3
			}
		}
	}

	localhost:`+testPort+` {
		route {
			php {
				root ../testdata
			}
		}
	}

	example.com:`+testPort+` {
		route {
			php {
				root ../testdata
			}
		}
	}
	`, "caddyfile")

	// Make some requests
	for i := range 10 {
		wg.Add(1)
		go func(i int) {
			tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
			wg.Done()
		}(i)
	}
	wg.Wait()

	// Fetch metrics
	resp, err := http.Get("http://localhost:2999/metrics")
	require.NoError(t, err, "failed to fetch metrics")
	t.Cleanup(func() {
		require.NoError(t, resp.Body.Close())
	})

	// Read and parse metrics
	metrics := new(bytes.Buffer)
	_, err = metrics.ReadFrom(resp.Body)
	require.NoError(t, err, "failed to read metrics")

	cpus := strconv.Itoa(getNumThreads(t, tester))

	// Check metrics
	expectedMetrics := `
	# HELP frankenphp_total_threads Total number of PHP threads
	# TYPE frankenphp_total_threads counter
	frankenphp_total_threads ` + cpus + `

	# HELP frankenphp_busy_threads Number of busy PHP threads
	# TYPE frankenphp_busy_threads gauge
	frankenphp_busy_threads 5

	# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
	# TYPE frankenphp_busy_workers gauge
	frankenphp_busy_workers{worker="service1"} 0

	# HELP frankenphp_total_workers Total number of PHP workers for this worker
	# TYPE frankenphp_total_workers gauge
	frankenphp_total_workers{worker="service1"} 2
	frankenphp_total_workers{worker="service2"} 3

	# HELP frankenphp_worker_request_count
	# TYPE frankenphp_worker_request_count counter
	frankenphp_worker_request_count{worker="service1"} 10

	# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
	# TYPE frankenphp_ready_workers gauge
	frankenphp_ready_workers{worker="service1"} 2
	frankenphp_ready_workers{worker="service2"} 3
	`

	ctx := caddy.ActiveContext()
	require.NoError(t,
		testutil.GatherAndCompare(
			ctx.GetMetricsRegistry(),
			strings.NewReader(expectedMetrics),
			"frankenphp_total_threads",
			"frankenphp_busy_threads",
			"frankenphp_busy_workers",
			"frankenphp_total_workers",
			"frankenphp_worker_request_count",
			"frankenphp_ready_workers",
		))
}

func TestDisabledMetrics(t *testing.T) {
	var wg sync.WaitGroup
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
	{
		skip_install_trust
		admin localhost:2999
		http_port `+testPort+`
		https_port 9443

		frankenphp {
			worker {
				name service1
				file ../testdata/index.php
				num 2
			}
			worker {
				name service2
				file ../testdata/ini.php
				num 3
			}
		}
	}

	localhost:`+testPort+` {
		route {
			php {
				root ../testdata
			}
		}
	}

	example.com:`+testPort+` {
		route {
			php {
				root ../testdata
			}
		}
	}
	`, "caddyfile")

	// Make some requests
	for i := range 10 {
		wg.Add(1)
		go func(i int) {
			tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i))
			wg.Done()
		}(i)
	}
	wg.Wait()

	// Fetch metrics
	resp, err := http.Get("http://localhost:2999/metrics")
	require.NoError(t, err, "failed to fetch metrics")
	t.Cleanup(func() {
		require.NoError(t, resp.Body.Close())
	})

	// Read and parse metrics
	metrics := new(bytes.Buffer)
	_, err = metrics.ReadFrom(resp.Body)
	require.NoError(t, err, "failed to read metrics")

	ctx := caddy.ActiveContext()
	count, err := testutil.GatherAndCount(
		ctx.GetMetricsRegistry(),
		"frankenphp_busy_threads",
		"frankenphp_busy_workers",
		"frankenphp_queue_depth",
		"frankenphp_ready_workers",
		"frankenphp_total_threads",
		"frankenphp_total_workers",
		"frankenphp_worker_request_count",
		"frankenphp_worker_request_time",
	)

	require.NoError(t, err, "failed to count metrics")
	require.Zero(t, count, "metrics should be missing")
}

func TestWorkerRestart(t *testing.T) {
	var wg sync.WaitGroup
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
	{
		skip_install_trust
		admin localhost:2999
		http_port `+testPort+`
		https_port 9443

		metrics
		frankenphp {
			worker {
				name service
				file ../testdata/worker-restart.php
				num 1
				# restart every 3 requests
				env EVERY 3
			}
		}
	}

	localhost:`+testPort+` {
		route {
			php {
				root ../testdata
			}
		}
	}
	`, "caddyfile")

	ctx := caddy.ActiveContext()

	resp, err := http.Get("http://localhost:2999/metrics")
	require.NoError(t, err, "failed to fetch metrics")
	t.Cleanup(func() {
		require.NoError(t, resp.Body.Close())
	})

	// Read and parse metrics
	metrics := new(bytes.Buffer)
	_, err = metrics.ReadFrom(resp.Body)
	require.NoError(t, err, "failed to read metrics")

	// frankenphp_worker_restarts should be missing
	count, err := testutil.GatherAndCount(
		ctx.GetMetricsRegistry(),
		"frankenphp_worker_restarts",
	)
	require.NoError(t, err, "failed to count metrics")
	require.Zero(t, count, "metrics should be missing")

	// Check metrics
	expectedMetrics := `
	# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
	# TYPE frankenphp_ready_workers gauge
	frankenphp_ready_workers{worker="service"} 1
	# HELP frankenphp_total_workers Total number of PHP workers for this worker
	# TYPE frankenphp_total_workers gauge
	frankenphp_total_workers{worker="service"} 1
	`

	require.NoError(t,
		testutil.GatherAndCompare(
			ctx.GetMetricsRegistry(),
			strings.NewReader(expectedMetrics),
			"frankenphp_total_workers",
			"frankenphp_ready_workers",
		))

	// Make some requests
	for i := range 10 {
		wg.Add(1)
		go func(i int) {
			tester.AssertGetResponse(fmt.Sprintf("http://localhost:"+testPort+"/worker-restart.php?i=%d", i), http.StatusOK, fmt.Sprintf("Counter (%d)", i))
			wg.Done()
		}(i)
	}
	wg.Wait()

	// frankenphp_ready_workers should be back to 1 even after worker restarts
	expectedMetrics = `
	# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
	# TYPE frankenphp_ready_workers gauge
	frankenphp_ready_workers{worker="service"} 1
	# HELP frankenphp_total_workers Total number of PHP workers for this worker
	# TYPE frankenphp_total_workers gauge
	frankenphp_total_workers{worker="service"} 1
	# HELP frankenphp_worker_restarts Number of PHP worker restarts for this worker
	# TYPE frankenphp_worker_restarts counter
	frankenphp_worker_restarts{worker="service"} 3
	`

	require.NoError(t,
		testutil.GatherAndCompare(
			ctx.GetMetricsRegistry(),
			strings.NewReader(expectedMetrics),
			"frankenphp_total_workers",
			"frankenphp_ready_workers",
			"frankenphp_worker_restarts",
		))
}

func TestWorkerMatchDirective(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			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 public directory, match anyway
	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)
	initServer(t, tester, `
		{
			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)
	initServer(t, tester, `
		{
			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")
}

func TestDd(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
		}

		http://localhost:`+testPort+` {
			php {
				worker ../testdata/dd.php 1 {
					match *
				}
			}
		`, "caddyfile")

	// simulate Symfony's dd()
	tester.AssertGetResponse(
		"http://localhost:"+testPort+"/some-path?output=dump123",
		http.StatusInternalServerError,
		"dump123",
	)
}

func TestLog(t *testing.T) {
	tester := caddytest.NewTester(t)
	initServer(t, tester, `
		{
			skip_install_trust
			admin localhost:2999
		}

		http://localhost:`+testPort+` {
			log {
				output stdout
				format json
			}

			root ../testdata
			php_server {
				worker ../testdata/log-frankenphp_log.php
			}
		}
		`, "caddyfile")

	tester.AssertGetResponse(
		"http://localhost:"+testPort+"/log-frankenphp_log.php?i=0",
		http.StatusOK,
		"",
	)
}

// TestSymlinkWorkerPaths tests different ways to reference worker scripts in symlinked directories
func TestSymlinkWorkerPaths(t *testing.T) {
	cwd, _ := os.Getwd()
	publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public")
	skipIfSymlinkNotValid(t, publicDir)

	t.Run("NeighboringWorkerScript", func(t *testing.T) {
		// Scenario: neighboring worker script
		// Given frankenphp located in the test folder
		// When I execute `frankenphp php-server --listen localhost:8080 -w index.php` from `public`
		// Then I expect to see the worker script executed successfully
		tester := caddytest.NewTester(t)
		initServer(t, tester, `
			{
				skip_install_trust
				admin localhost:2999
				http_port `+testPort+`

				frankenphp {
					worker `+publicDir+`/index.php 1
				}
			}

			localhost:`+testPort+` {
				route {
					php {
						root `+publicDir+`
						resolve_root_symlink true
					}
				}
			}
			`, "caddyfile")

		tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, "Request: 0\n")
	})

	t.Run("NestedWorkerScript", func(t *testing.T) {
		// Scenario: nested worker script
		// Given frankenphp located in the test folder
		// When I execute `frankenphp --listen localhost:8080 -w nested/index.php` from `public`
		// Then I expect to see the worker script executed successfully
		tester := caddytest.NewTester(t)
		initServer(t, tester, `
			{
				skip_install_trust
				admin localhost:2999
				http_port `+testPort+`

				frankenphp {
					worker `+publicDir+`/nested/index.php 1
				}
			}

			localhost:`+testPort+` {
				route {
					php {
						root `+publicDir+`
						resolve_root_symlink true
					}
				}
			}
			`, "caddyfile")

		tester.AssertGetResponse("http://localhost:"+testPort+"/nested/index.php", http.StatusOK, "Nested request: 0\n")
	})

	t.Run("OutsideSymlinkedFolder", func(t *testing.T) {
		// Scenario: outside the symlinked folder
		// Given frankenphp located in the root folder
		// When I execute `frankenphp --listen localhost:8080 -w public/index.php` from the root folder
		// Then I expect to see the worker script executed successfully
		tester := caddytest.NewTester(t)
		initServer(t, tester, `
			{
				skip_install_trust
				admin localhost:2999
				http_port `+testPort+`

				frankenphp {
					worker {
						name outside_worker
						file `+publicDir+`/index.php
						num 1
					}
				}
			}

			localhost:`+testPort+` {
				route {
					php {
						root `+publicDir+`
						resolve_root_symlink true
					}
				}
			}
			`, "caddyfile")

		tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, "Request: 0\n")
	})

	t.Run("SpecifiedRootDirectory", func(t *testing.T) {
		// Scenario: specified root directory
		// Given frankenphp located in the root folder
		// When I execute `frankenphp --listen localhost:8080 -w public/index.php -r public` from the root folder
		// Then I expect to see the worker script executed successfully
		tester := caddytest.NewTester(t)
		initServer(t, tester, `
			{
				skip_install_trust
				admin localhost:2999
				http_port `+testPort+`

				frankenphp {
					worker {
						name specified_root_worker
						file `+publicDir+`/index.php
						num 1
					}
				}
			}

			localhost:`+testPort+` {
				route {
					php {
						root `+publicDir+`
						resolve_root_symlink true
					}
				}
			}
			`, "caddyfile")

		tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, "Request: 0\n")
	})
}

// TestSymlinkResolveRoot tests the resolve_root_symlink directive behavior
func TestSymlinkResolveRoot(t *testing.T) {
	cwd, _ := os.Getwd()
	testDir := filepath.Join(cwd, "..", "testdata", "symlinks", "test")
	publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public")
	skipIfSymlinkNotValid(t, publicDir)

	t.Run("ResolveRootSymlink", func(t *testing.T) {
		// Tests that resolve_root_symlink directive works correctly
		tester := caddytest.NewTester(t)
		initServer(t, tester, `
			{
				skip_install_trust
				admin localhost:2999
				http_port `+testPort+`

				frankenphp {
					worker `+publicDir+`/document-root.php 1
				}
			}

			localhost:`+testPort+` {
				route {
					php {
						root `+publicDir+`
						resolve_root_symlink true
					}
				}
			}
			`, "caddyfile")

		// DOCUMENT_ROOT should be the resolved path (testDir)
		tester.AssertGetResponse("http://localhost:"+testPort+"/document-root.php", http.StatusOK, "DOCUMENT_ROOT="+testDir+"\n")
	})

	t.Run("NoResolveRootSymlink", func(t *testing.T) {
		// Tests that symlinks are preserved when resolve_root_symlink is false (non-worker mode)
		tester := caddytest.NewTester(t)
		initServer(t, tester, `
			{
				skip_install_trust
				admin localhost:2999
				http_port `+testPort+`
			}

			localhost:`+testPort+` {
				route {
					php {
						root `+publicDir+`
						resolve_root_symlink false
					}
				}
			}
			`, "caddyfile")

		// DOCUMENT_ROOT should be the symlink path (publicDir) when resolve_root_symlink is false
		tester.AssertGetResponse("http://localhost:"+testPort+"/document-root.php", http.StatusOK, "DOCUMENT_ROOT="+publicDir+"\n")
	})
}

// TestSymlinkWorkerBehavior tests worker behavior with symlinked directories
func TestSymlinkWorkerBehavior(t *testing.T) {
	cwd, _ := os.Getwd()
	publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public")
	skipIfSymlinkNotValid(t, publicDir)

	t.Run("WorkerScriptFailsWithoutWorkerMode", func(t *testing.T) {
		// Tests that accessing a worker-only script without configuring it as a worker actually results in an error
		tester := caddytest.NewTester(t)
		initServer(t, tester, `
			{
				skip_install_trust
				admin localhost:2999
				http_port `+testPort+`
			}

			localhost:`+testPort+` {
				route {
					php {
						root `+publicDir+`
					}
				}
			}
			`, "caddyfile")

		// Accessing the worker script without worker configuration MUST fail
		// The script checks $_SERVER['FRANKENPHP_WORKER'] and dies if not set
		tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, "Error: This script must be run in worker mode (FRANKENPHP_WORKER not set to '1')\n")
	})

	t.Run("MultipleRequests", func(t *testing.T) {
		// Tests that symlinked workers handle multiple requests correctly
		tester := caddytest.NewTester(t)
		initServer(t, tester, `
			{
				skip_install_trust
				admin localhost:2999
				http_port `+testPort+`
			}

			localhost:`+testPort+` {
				route {
					php {
						root `+publicDir+`
						resolve_root_symlink true
						worker index.php 1
					}
				}
			}
			`, "caddyfile")

		// Make multiple requests - each should increment the counter
		for i := 0; i < 5; i++ {
			tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, fmt.Sprintf("Request: %d\n", i))
		}
	})
}


================================================
FILE: caddy/config_test.go
================================================
package caddy

import (
	"testing"

	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
	"github.com/stretchr/testify/require"
)

func TestModuleWorkerDuplicateFilenamesFail(t *testing.T) {
	// Create a test configuration with duplicate worker filenames
	configWithDuplicateFilenames := `
	{
		php {
			worker {
				file worker-with-env.php
				num 1
			}
			worker {
				file worker-with-env.php
				num 2
			}
		}
	}`

	// Parse the configuration
	d := caddyfile.NewTestDispenser(configWithDuplicateFilenames)
	module := &FrankenPHPModule{}

	// Unmarshal the configuration
	err := module.UnmarshalCaddyfile(d)

	// Verify that an error was returned
	require.Error(t, err, "Expected an error when two workers in the same module have the same filename")
	require.Contains(t, err.Error(), "must not have duplicate filenames", "Error message should mention duplicate filenames")
}

func TestModuleWorkersWithDifferentFilenames(t *testing.T) {
	// Create a test configuration with different worker filenames
	configWithDifferentFilenames := `
	{
		php {
			worker ../testdata/worker-with-env.php
			worker ../testdata/worker-with-counter.php
		}
	}`

	// Parse the configuration
	d := caddyfile.NewTestDispenser(configWithDifferentFilenames)
	module := &FrankenPHPModule{}

	// Unmarshal the configuration
	err := module.UnmarshalCaddyfile(d)

	// Verify that no error was returned
	require.NoError(t, err, "Expected no error when two workers in the same module have different filenames")

	// Verify that both workers were added to the module
	require.Len(t, module.Workers, 2, "Expected two workers to be added to the module")
	require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "First worker should have the correct filename")
	require.Equal(t, "../testdata/worker-with-counter.php", module.Workers[1].FileName, "Second worker should have the correct filename")
}

func TestModuleWorkersDifferentNamesSucceed(t *testing.T) {
	// Create a test configuration with a worker name
	configWithWorkerName1 := `
	{
		php_server {
			worker {
				name test-worker-1
				file ../testdata/worker-with-env.php
				num 1
			}
		}
	}`

	// Parse the first configuration
	d1 := caddyfile.NewTestDispenser(configWithWorkerName1)
	app := &FrankenPHPApp{}
	module1 := &FrankenPHPModule{}

	// Unmarshal the first configuration
	err := module1.UnmarshalCaddyfile(d1)
	require.NoError(t, err, "First module should be configured without errors")

	// Create a second test configuration with a different worker name
	configWithWorkerName2 := `
	{
		php_server {
			worker {
				name test-worker-2
				file ../testdata/worker-with-env.php
				num 1
			}
		}
	}`

	// Parse the second configuration
	d2 := caddyfile.NewTestDispenser(configWithWorkerName2)
	module2 := &FrankenPHPModule{}

	// Unmarshal the second configuration
	err = module2.UnmarshalCaddyfile(d2)

	// Verify that no error was returned
	require.NoError(t, err, "Expected no error when two workers have different names")

	_, err = app.addModuleWorkers(module1.Workers...)
	require.NoError(t, err, "Expected no error when adding the first module workers")
	_, err = app.addModuleWorkers(module2.Workers...)
	require.NoError(t, err, "Expected no error when adding the second module workers")

	// Verify that both workers were added
	require.Len(t, app.Workers, 2, "Expected two workers in the app")
	require.Equal(t, "m#test-worker-1", app.Workers[0].Name, "First worker should have the correct name")
	require.Equal(t, "m#test-worker-2", app.Workers[1].Name, "Second worker should have the correct name")
}

func TestModuleWorkerWithEnvironmentVariables(t *testing.T) {
	// Create a test configuration with environment variables
	configWithEnv := `
	{
		php {
			worker {
				file ../testdata/worker-with-env.php
				num 1
				env APP_ENV production
				env DEBUG true
			}
		}
	}`

	// Parse the configuration
	d := caddyfile.NewTestDispenser(configWithEnv)
	module := &FrankenPHPModule{}

	// Unmarshal the configuration
	err := module.UnmarshalCaddyfile(d)

	// Verify that no error was returned
	require.NoError(t, err, "Expected no error when configuring a worker with environment variables")

	// Verify that the worker was added to the module
	require.Len(t, module.Workers, 1, "Expected one worker to be added to the module")
	require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "Worker should have the correct filename")

	// Verify that the environment variables were set correctly
	require.Len(t, module.Workers[0].Env, 2, "Expected two environment variables")
	require.Equal(t, "production", module.Workers[0].Env["APP_ENV"], "APP_ENV should be set to production")
	require.Equal(t, "true", module.Workers[0].Env["DEBUG"], "DEBUG should be set to true")
}

func TestModuleWorkerWithWatchConfiguration(t *testing.T) {
	// Create a test configuration with watch directories
	configWithWatch := `
	{
		php {
			worker {
				file ../testdata/worker-with-env.php
				num 1
				watch
				watch ./src/**/*.php
				watch ./config/**/*.yaml
			}
		}
	}`

	// Parse the configuration
	d := caddyfile.NewTestDispenser(configWithWatch)
	module := &FrankenPHPModule{}

	// Unmarshal the configuration
	err := module.UnmarshalCaddyfile(d)

	// Verify that no error was returned
	require.NoError(t, err, "Expected no error when configuring a worker with watch directories")

	// Verify that the worker was added to the module
	require.Len(t, module.Workers, 1, "Expected one worker to be added to the module")
	require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "Worker should have the correct filename")

	// Verify that the watch directories were set correctly
	require.Len(t, module.Workers[0].Watch, 3, "Expected three watch patterns")
	require.Equal(t, defaultWatchPattern, module.Workers[0].Watch[0], "First watch pattern should be the default")
	require.Equal(t, "./src/**/*.php", module.Workers[0].Watch[1], "Second watch pattern should match the configuration")
	require.Equal(t, "./config/**/*.yaml", module.Workers[0].Watch[2], "Third watch pattern should match the configuration")
}

func TestModuleWorkerWithCustomName(t *testing.T) {
	// Create a test configuration with a custom worker name
	configWithCustomName := `
	{
		php {
			worker {
				file ../testdata/worker-with-env.php
				num 1
				name custom-worker-name
			}
		}
	}`

	// Parse the configuration
	d := caddyfile.NewTestDispenser(configWithCustomName)
	module := &FrankenPHPModule{}
	app := &FrankenPHPApp{}

	// Unmarshal the configuration
	err := module.UnmarshalCaddyfile(d)

	// Verify that no error was returned
	require.NoError(t, err, "Expected no error when configuring a worker with a custom name")

	// Verify that the worker was added to the module
	require.Len(t, module.Workers, 1, "Expected one worker to be added to the module")
	require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "Worker should have the correct filename")

	// Verify that the worker was added to app.Workers with the m# prefix
	module.Workers, err = app.addModuleWorkers(module.Workers...)
	require.NoError(t, err, "Expected no error when adding the worker to the app")
	require.Equal(t, "m#custom-worker-name", module.Workers[0].Name, "Worker should have the custom name, prefixed with m#")
	require.Equal(t, "m#custom-worker-name", app.Workers[0].Name, "Worker should have the custom name, prefixed with m#")
}


================================================
FILE: caddy/extinit.go
================================================
package caddy

import (
	"errors"
	"log"
	"os"
	"path/filepath"
	"strings"

	"github.com/dunglas/frankenphp/internal/extgen"

	caddycmd "github.com/caddyserver/caddy/v2/cmd"
	"github.com/spf13/cobra"
)

func init() {
	caddycmd.RegisterCommand(caddycmd.Command{
		Name:  "extension-init",
		Usage: "go_extension.go [--verbose]",
		Short: "Initializes a PHP extension from a Go file (EXPERIMENTAL)",
		Long: `
Initializes a PHP extension from a Go file. This command generates the necessary C files for the extension, including the header and source files, as well as the arginfo file.`,
		CobraFunc: func(cmd *cobra.Command) {
			cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")

			cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdInitExtension)
		},
	})
}

func cmdInitExtension(_ caddycmd.Flags) (int, error) {
	if len(os.Args) < 3 {
		return 1, errors.New("the path to the Go source is required")
	}

	sourceFile := os.Args[2]
	baseName := extgen.SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go"))

	generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: filepath.Dir(sourceFile)}

	if err := generator.Generate(); err != nil {
		return 1, err
	}

	log.Printf("PHP extension %q initialized successfully in directory %q", baseName, generator.BuildDir)

	return 0, nil
}


================================================
FILE: caddy/frankenphp/Caddyfile
================================================
# The Caddyfile is an easy way to configure FrankenPHP and the Caddy web server.
#
# https://frankenphp.dev/docs/config
# https://caddyserver.com/docs/caddyfile

{
	skip_install_trust

	{$CADDY_GLOBAL_OPTIONS}

	frankenphp {
		{$FRANKENPHP_CONFIG}
	}
}

{$CADDY_EXTRA_CONFIG}

{$SERVER_NAME:localhost} {
	#log {
	#	# Redact the authorization query parameter that can be set by Mercure
	#	format filter {
	#		request>uri query {
	#			replace authorization REDACTED
	#		}
	#	}
	#}

	root {$SERVER_ROOT:public/}
	encode zstd br gzip

	# Uncomment the following lines to enable Mercure and Vulcain modules
	#mercure {
	#	# Publisher JWT key
	#	publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
	#	# Subscriber JWT key
	#	subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
	#	# Allow anonymous subscribers (double-check that it's what you want)
	#	anonymous
	#	# Enable the subscription API (double-check that it's what you want)
	#	subscriptions
	#	# Extra directives
	#	{$MERCURE_EXTRA_DIRECTIVES}
	#}
	#vulcain

	{$CADDY_SERVER_EXTRA_DIRECTIVES}

	php_server {
		#worker /path/to/your/worker.php
	}
}

# As an alternative to editing the above site block, you can add your own site
# block files in the Caddyfile.d directory, and they will be included as long
# as they use the .caddyfile extension.

import Caddyfile.d/*.caddyfile


================================================
FILE: caddy/frankenphp/cbrotli.go
================================================
//go:build !nobrotli

package main

import _ "github.com/dunglas/caddy-cbrotli"


================================================
FILE: caddy/frankenphp/main.go
================================================
package main

import (
	caddycmd "github.com/caddyserver/caddy/v2/cmd"

	// plug in Caddy modules here.
	_ "github.com/caddyserver/caddy/v2/modules/standard"
	_ "github.com/dunglas/frankenphp/caddy"
	_ "github.com/dunglas/mercure/caddy"
	_ "github.com/dunglas/vulcain/caddy"
)

func main() {
	caddycmd.Main()
}


================================================
FILE: caddy/go.mod
================================================
module github.com/dunglas/frankenphp/caddy

go 1.26.0

replace github.com/dunglas/frankenphp => ../

retract v1.0.0-rc.1 // Human error

require (
	github.com/caddyserver/caddy/v2 v2.11.2
	github.com/caddyserver/certmagic v0.25.2
	github.com/dunglas/caddy-cbrotli v1.0.1
	github.com/dunglas/frankenphp v1.12.1
	github.com/dunglas/mercure v0.21.11
	github.com/dunglas/mercure/caddy v0.21.11
	github.com/dunglas/vulcain/caddy v1.4.0
	github.com/prometheus/client_golang v1.23.2
	github.com/spf13/cobra v1.10.2
	github.com/stretchr/testify v1.11.1
)

require github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect

require (
	cel.dev/expr v0.25.1 // indirect
	cloud.google.com/go/auth v0.18.2 // indirect
	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
	cloud.google.com/go/compute/metadata v0.9.0 // indirect
	dario.cat/mergo v1.0.2 // indirect
	filippo.io/bigmod v0.1.0 // indirect
	filippo.io/edwards25519 v1.2.0 // indirect
	github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
	github.com/BurntSushi/toml v1.6.0 // indirect
	github.com/DeRuina/timberjack v1.3.9 // indirect
	github.com/KimMachineGun/automemlimit v0.7.5 // indirect
	github.com/Masterminds/goutils v1.1.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.11.0 // indirect
	github.com/MicahParks/keyfunc/v3 v3.8.0 // indirect
	github.com/RoaringBitmap/roaring/v2 v2.15.0 // indirect
	github.com/alecthomas/chroma/v2 v2.23.1 // 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.24.4 // indirect
	github.com/caddyserver/zerossl v0.1.5 // indirect
	github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
	github.com/cenkalti/backoff/v5 v5.0.3 // indirect
	github.com/cespare/xxhash v1.1.0 // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/chzyer/readline v1.5.1 // indirect
	github.com/cloudflare/circl v1.6.3 // indirect
	github.com/coreos/go-oidc/v3 v3.17.0 // indirect
	github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
	github.com/dgraph-io/badger v1.6.2 // indirect
	github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
	github.com/dgraph-io/ristretto v0.2.0 // indirect
	github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
	github.com/dlclark/regexp2 v1.11.5 // indirect
	github.com/dunglas/httpsfv v1.1.0 // indirect
	github.com/dunglas/skipfilter v1.0.0 // indirect
	github.com/dunglas/vulcain v1.4.0 // indirect
	github.com/dustin/go-humanize v1.0.1 // indirect
	github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be // indirect
	github.com/felixge/httpsnoop v1.0.4 // indirect
	github.com/fsnotify/fsnotify v1.9.0 // indirect
	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
	github.com/getkin/kin-openapi v0.133.0 // indirect
	github.com/go-chi/chi/v5 v5.2.5 // indirect
	github.com/go-jose/go-jose/v3 v3.0.4 // indirect
	github.com/go-jose/go-jose/v4 v4.1.3 // indirect
	github.com/go-logr/logr v1.4.3 // indirect
	github.com/go-logr/stdr v1.2.2 // indirect
	github.com/go-openapi/jsonpointer v0.22.5 // indirect
	github.com/go-openapi/swag/jsonname v0.25.5 // indirect
	github.com/go-sql-driver/mysql v1.9.3 // indirect
	github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
	github.com/gofrs/uuid v4.4.0+incompatible // indirect
	github.com/gofrs/uuid/v5 v5.4.0 // indirect
	github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
	github.com/golang/protobuf v1.5.4 // indirect
	github.com/golang/snappy v1.0.0 // indirect
	github.com/google/brotli/go/cbrotli v1.1.0 // indirect
	github.com/google/cel-go v0.27.0 // indirect
	github.com/google/certificate-transparency-go v1.3.3 // indirect
	github.com/google/go-tpm v0.9.8 // indirect
	github.com/google/go-tspi v0.3.0 // 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.14 // indirect
	github.com/googleapis/gax-go/v2 v2.17.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.28.0 // indirect
	github.com/huandu/xstrings v1.5.0 // indirect
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/jackc/pgpassfile v1.0.0 // indirect
	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
	github.com/jackc/pgx/v5 v5.8.0 // indirect
	github.com/jackc/puddle/v2 v2.2.2 // indirect
	github.com/josharian/intern v1.0.0 // indirect
	github.com/klauspost/compress v1.18.4 // indirect
	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
	github.com/kylelemons/godebug v1.1.0 // indirect
	github.com/libdns/libdns v1.1.1 // indirect
	github.com/mailru/easyjson v0.9.1 // indirect
	github.com/manifoldco/promptui v0.9.0 // indirect
	github.com/mattn/go-colorable v0.1.14 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/maypok86/otter/v2 v2.3.0 // indirect
	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
	github.com/mholt/acmez/v3 v3.1.6 // indirect
	github.com/miekg/dns v1.1.72 // 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
	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
	github.com/mschoch/smat v0.2.0 // indirect
	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
	github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
	github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
	github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
	github.com/perimeterx/marshmallow v1.1.5 // indirect
	github.com/pires/go-proxyproto v0.11.0 // indirect
	github.com/pkg/errors v0.9.1 // indirect
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
	github.com/prometheus/client_model v0.6.2 // indirect
	github.com/prometheus/common v0.67.5 // indirect
	github.com/prometheus/otlptranslator v1.0.0 // indirect
	github.com/prometheus/procfs v0.20.1 // indirect
	github.com/quic-go/qpack v0.6.0 // indirect
	github.com/quic-go/quic-go v0.59.0 // indirect
	github.com/rs/cors v1.11.1 // indirect
	github.com/rs/xid v1.6.0 // indirect
	github.com/russross/blackfriday/v2 v2.1.0 // indirect
	github.com/sagikazarmark/locafero v0.12.0 // indirect
	github.com/shopspring/decimal v1.4.0 // indirect
	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
	github.com/sirupsen/logrus v1.9.4 // indirect
	github.com/slackhq/nebula v1.10.3 // indirect
	github.com/smallstep/certificates v0.30.0-rc3 // indirect
	github.com/smallstep/cli-utils v0.12.2 // indirect
	github.com/smallstep/linkedca v0.25.0 // indirect
	github.com/smallstep/nosql v0.7.0 // indirect
	github.com/smallstep/pkcs7 v0.2.1 // indirect
	github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect
	github.com/smallstep/truststore v0.13.0 // indirect
	github.com/spf13/afero v1.15.0 // indirect
	github.com/spf13/cast v1.10.0 // indirect
	github.com/spf13/pflag v1.0.10 // indirect
	github.com/spf13/viper v1.21.0 // indirect
	github.com/subosito/gotenv v1.6.0 // indirect
	github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
	github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 // indirect
	github.com/tidwall/gjson v1.18.0 // indirect
	github.com/tidwall/match v1.2.0 // indirect
	github.com/tidwall/pretty v1.2.1 // indirect
	github.com/tidwall/sjson v1.2.5 // indirect
	github.com/unrolled/secure v1.17.0 // indirect
	github.com/urfave/cli v1.22.17 // indirect
	github.com/woodsbury/decimal128 v1.4.0 // indirect
	github.com/x448/float16 v0.8.4 // indirect
	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
	github.com/yuin/goldmark v1.7.16 // 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.3 // indirect
	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
	go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect
	go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 // indirect
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
	go.opentelemetry.io/contrib/propagators/autoprop v0.67.0 // indirect
	go.opentelemetry.io/contrib/propagators/aws v1.42.0 // indirect
	go.opentelemetry.io/contrib/propagators/b3 v1.42.0 // indirect
	go.opentelemetry.io/contrib/propagators/jaeger v1.42.0 // indirect
	go.opentelemetry.io/contrib/propagators/ot v1.42.0 // indirect
	go.opentelemetry.io/otel v1.42.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect
	go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect
	go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 // indirect
	go.opentelemetry.io/otel/exporters/stdout/stdoutme
Download .txt
gitextract_hy3ir2xo/

├── .clang-format-ignore
├── .codespellrc
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yaml
│   │   └── feature_request.yaml
│   ├── actions/
│   │   └── watcher/
│   │       └── action.yaml
│   ├── dependabot.yaml
│   ├── scripts/
│   │   ├── docker-compute-fingerprints.sh
│   │   └── docker-verify-fingerprints.sh
│   └── workflows/
│       ├── docker.yaml
│       ├── docs.yaml
│       ├── lint.yaml
│       ├── sanitizers.yaml
│       ├── static.yaml
│       ├── tests.yaml
│       ├── translate.yaml
│       ├── windows.yaml
│       └── wrap-issue-details.yaml
├── .gitignore
├── .gitleaksignore
├── .golangci.yaml
├── .hadolint.yaml
├── .markdown-lint.yaml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── SECURITY.md
├── alpine.Dockerfile
├── app_checksum.txt
├── build-static.sh
├── caddy/
│   ├── admin.go
│   ├── admin_test.go
│   ├── app.go
│   ├── br-skip.go
│   ├── br.go
│   ├── caddy.go
│   ├── caddy_test.go
│   ├── config_test.go
│   ├── extinit.go
│   ├── frankenphp/
│   │   ├── Caddyfile
│   │   ├── cbrotli.go
│   │   └── main.go
│   ├── go.mod
│   ├── go.sum
│   ├── hotreload-skip.go
│   ├── hotreload.go
│   ├── hotreload_test.go
│   ├── mercure-skip.go
│   ├── mercure.go
│   ├── module.go
│   ├── module_test.go
│   ├── php-cli.go
│   ├── php-server.go
│   ├── watcher_test.go
│   └── workerconfig.go
├── cgi.go
├── cgi_test.go
├── cgo.go
├── cli.go
├── cli_test.go
├── context.go
├── debugstate.go
├── dev-alpine.Dockerfile
├── dev.Dockerfile
├── docker-bake.hcl
├── docs/
│   ├── classic.md
│   ├── cn/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── classic.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── extensions.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── mercure.md
│   │   ├── metrics.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   ├── worker.md
│   │   └── x-sendfile.md
│   ├── compile.md
│   ├── config.md
│   ├── docker.md
│   ├── early-hints.md
│   ├── embed.md
│   ├── es/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── classic.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── extensions.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── logging.md
│   │   ├── mercure.md
│   │   ├── metrics.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   ├── wordpress.md
│   │   ├── worker.md
│   │   └── x-sendfile.md
│   ├── extension-workers.md
│   ├── extensions.md
│   ├── fr/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── classic.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── extensions.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── mercure.md
│   │   ├── metrics.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   ├── worker.md
│   │   └── x-sendfile.md
│   ├── github-actions.md
│   ├── hot-reload.md
│   ├── ja/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── classic.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── extensions.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── mercure.md
│   │   ├── metrics.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   ├── worker.md
│   │   └── x-sendfile.md
│   ├── known-issues.md
│   ├── laravel.md
│   ├── logging.md
│   ├── mercure.md
│   ├── metrics.md
│   ├── performance.md
│   ├── production.md
│   ├── pt-br/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── classic.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── extensions.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── mercure.md
│   │   ├── metrics.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   ├── worker.md
│   │   └── x-sendfile.md
│   ├── ru/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── mercure.md
│   │   ├── metrics.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   └── worker.md
│   ├── static.md
│   ├── tr/
│   │   ├── CONTRIBUTING.md
│   │   ├── README.md
│   │   ├── compile.md
│   │   ├── config.md
│   │   ├── docker.md
│   │   ├── early-hints.md
│   │   ├── embed.md
│   │   ├── extension-workers.md
│   │   ├── github-actions.md
│   │   ├── hot-reload.md
│   │   ├── known-issues.md
│   │   ├── laravel.md
│   │   ├── mercure.md
│   │   ├── performance.md
│   │   ├── production.md
│   │   ├── static.md
│   │   └── worker.md
│   ├── translate.php
│   ├── wordpress.md
│   ├── worker.md
│   └── x-sendfile.md
├── embed.go
├── env.go
├── ext.go
├── frankenphp.c
├── frankenphp.go
├── frankenphp.h
├── frankenphp.stub.php
├── frankenphp_arginfo.h
├── frankenphp_test.go
├── go.mod
├── go.sh
├── go.sum
├── hotreload.go
├── install.ps1
├── install.sh
├── internal/
│   ├── cpu/
│   │   ├── cpu_unix.go
│   │   └── cpu_windows.go
│   ├── extgen/
│   │   ├── arginfo.go
│   │   ├── cfile.go
│   │   ├── cfile_namespace_test.go
│   │   ├── cfile_phpmethod_test.go
│   │   ├── cfile_test.go
│   │   ├── classparser.go
│   │   ├── classparser_test.go
│   │   ├── constants_test.go
│   │   ├── constparser.go
│   │   ├── constparser_test.go
│   │   ├── docs.go
│   │   ├── docs_test.go
│   │   ├── errors.go
│   │   ├── funcparser.go
│   │   ├── funcparser_test.go
│   │   ├── generator.go
│   │   ├── gofile.go
│   │   ├── gofile_test.go
│   │   ├── hfile.go
│   │   ├── hfile_test.go
│   │   ├── integration_test.go
│   │   ├── namespace_test.go
│   │   ├── nodes.go
│   │   ├── nsparser.go
│   │   ├── paramparser.go
│   │   ├── paramparser_test.go
│   │   ├── parser.go
│   │   ├── phpfunc.go
│   │   ├── phpfunc_namespace_test.go
│   │   ├── phpfunc_test.go
│   │   ├── srcanalyzer.go
│   │   ├── srcanalyzer_test.go
│   │   ├── stub.go
│   │   ├── stub_test.go
│   │   ├── templates/
│   │   │   ├── README.md.tpl
│   │   │   ├── extension.c.tpl
│   │   │   ├── extension.go.tpl
│   │   │   ├── extension.h.tpl
│   │   │   └── stub.php.tpl
│   │   ├── utils.go
│   │   ├── utils_namespace_test.go
│   │   ├── utils_test.go
│   │   ├── validator.go
│   │   └── validator_test.go
│   ├── fastabs/
│   │   ├── filepath.go
│   │   └── filepath_unix.go
│   ├── memory/
│   │   ├── memory_linux.go
│   │   └── memory_others.go
│   ├── phpheaders/
│   │   ├── phpheaders.go
│   │   └── phpheaders_test.go
│   ├── state/
│   │   ├── state.go
│   │   └── state_test.go
│   ├── testcli/
│   │   └── main.go
│   ├── testext/
│   │   ├── ext_test.go
│   │   ├── extension.h
│   │   ├── extensions.c
│   │   ├── exttest.go
│   │   └── testdata/
│   │       └── index.php
│   ├── testserver/
│   │   └── main.go
│   └── watcher/
│       ├── pattern.go
│       ├── pattern_test.go
│       └── watcher.go
├── log_test.go
├── mercure-skip.go
├── mercure.go
├── mercure_test.go
├── metrics.go
├── metrics_test.go
├── options.go
├── package/
│   ├── Caddyfile
│   ├── alpine/
│   │   ├── frankenphp.openrc
│   │   ├── post-deinstall.sh
│   │   ├── post-install.sh
│   │   └── pre-deinstall.sh
│   ├── content/
│   │   └── index.php
│   ├── debian/
│   │   ├── frankenphp.service
│   │   ├── postinst.sh
│   │   ├── postrm.sh
│   │   └── prerm.sh
│   └── rhel/
│       ├── frankenphp.service
│       ├── postinstall.sh
│       ├── postuninstall.sh
│       ├── preinstall.sh
│       └── preuninstall.sh
├── phpmainthread.go
├── phpmainthread_test.go
├── phpthread.go
├── recorder_test.go
├── release.sh
├── reload_test.sh
├── requestoptions.go
├── requestoptions_test.go
├── scaling.go
├── scaling_test.go
├── static-builder-gnu.Dockerfile
├── static-builder-musl.Dockerfile
├── testdata/
│   ├── Caddyfile
│   ├── _executor.php
│   ├── autoloader-require.php
│   ├── autoloader.php
│   ├── benchmark.Caddyfile
│   ├── command.php
│   ├── connection_status.php
│   ├── cookies.php
│   ├── dd.php
│   ├── die.php
│   ├── dirindex/
│   │   └── index.php
│   ├── early-hints.php
│   ├── echo.php
│   ├── env/
│   │   ├── env.php
│   │   ├── import-env.php
│   │   ├── overwrite-env.php
│   │   ├── putenv.php
│   │   ├── remember-env.php
│   │   └── test-env.php
│   ├── exception.php
│   ├── failing-worker.php
│   ├── fiber-basic.php
│   ├── fiber-no-cgo.php
│   ├── file-stream.php
│   ├── file-stream.txt
│   ├── file-upload.php
│   ├── files/
│   │   ├── .gitignore
│   │   └── static.txt
│   ├── finish-request.php
│   ├── flush.php
│   ├── headers.php
│   ├── hello.php
│   ├── hello.txt
│   ├── index.php
│   ├── ini.php
│   ├── input.php
│   ├── integration/
│   │   ├── basic_function.go
│   │   ├── callable.go
│   │   ├── class_methods.go
│   │   ├── constants.go
│   │   ├── invalid_signature.go
│   │   ├── namespace.go
│   │   └── type_mismatch.go
│   ├── large-request.php
│   ├── large-response.php
│   ├── load-test.js
│   ├── log-error_log.php
│   ├── log-frankenphp_log.php
│   ├── mercure-publish.php
│   ├── message-worker.php
│   ├── non-worker.php
│   ├── only-headers.php
│   ├── performance/
│   │   ├── api.js
│   │   ├── computation.js
│   │   ├── database.js
│   │   ├── flamegraph.sh
│   │   ├── hanging-requests.js
│   │   ├── hello-world.js
│   │   ├── k6.Caddyfile
│   │   ├── perf-test.sh
│   │   ├── performance-testing.md
│   │   ├── start-server.sh
│   │   └── timeouts.js
│   ├── persistent-object-require.php
│   ├── persistent-object.php
│   ├── phpinfo.php
│   ├── preload-check.php
│   ├── preload.php
│   ├── request-headers.php
│   ├── request-superglobal-conditional-include.php
│   ├── request-superglobal-conditional.php
│   ├── request-superglobal.php
│   ├── response-headers.php
│   ├── server-all-vars-ordered.php
│   ├── server-all-vars-ordered.txt
│   ├── server-variable.php
│   ├── session-handler.php
│   ├── session-leak.php
│   ├── session.php
│   ├── sleep.php
│   ├── super-globals.php
│   ├── symlinks/
│   │   └── test/
│   │       ├── document-root.php
│   │       ├── index.php
│   │       └── nested/
│   │           └── index.php
│   ├── timeout.php
│   ├── transition-regular.php
│   ├── transition-worker-1.php
│   ├── transition-worker-2.php
│   ├── worker-env.php
│   ├── worker-getopt.php
│   ├── worker-restart.php
│   ├── worker-with-counter.php
│   ├── worker-with-env.php
│   ├── worker-with-session-handler.php
│   └── worker.php
├── threadinactive.go
├── threadregular.go
├── threadtasks_test.go
├── threadworker.go
├── types.c
├── types.go
├── types.h
├── types_test.go
├── vcpkg.json
├── watcher-skip.go
├── watcher.go
├── watcher_test.go
├── worker.go
├── worker_test.go
├── workerextension.go
├── workerextension_test.go
└── zizmor.yaml
Download .txt
SYMBOL INDEX (1128 symbols across 134 files)

FILE: caddy/admin.go
  type FrankenPHPAdmin (line 12) | type FrankenPHPAdmin struct
    method CaddyModule (line 16) | func (FrankenPHPAdmin) CaddyModule() caddy.ModuleInfo {
    method Routes (line 24) | func (admin FrankenPHPAdmin) Routes() []caddy.AdminRoute {
    method restartWorkers (line 37) | func (admin *FrankenPHPAdmin) restartWorkers(w http.ResponseWriter, r ...
    method threads (line 49) | func (admin *FrankenPHPAdmin) threads(w http.ResponseWriter, _ *http.R...
    method success (line 59) | func (admin *FrankenPHPAdmin) success(w http.ResponseWriter, message s...
    method error (line 65) | func (admin *FrankenPHPAdmin) error(statusCode int, err error) error {

FILE: caddy/admin_test.go
  function TestRestartWorkerViaAdminApi (line 20) | func TestRestartWorkerViaAdminApi(t *testing.T) {
  function TestShowTheCorrectThreadDebugStatus (line 50) | func TestShowTheCorrectThreadDebugStatus(t *testing.T) {
  function TestAutoScaleWorkerThreads (line 85) | func TestAutoScaleWorkerThreads(t *testing.T) {
  function TestAutoScaleRegularThreadsOnAutomaticThreadLimit (line 141) | func TestAutoScaleRegularThreadsOnAutomaticThreadLimit(t *testing.T) {
  function assertAdminResponse (line 192) | func assertAdminResponse(t *testing.T, tester *caddytest.Tester, method ...
  function getAdminResponseBody (line 203) | func getAdminResponseBody(t *testing.T, tester *caddytest.Tester, method...
  function getDebugState (line 215) | func getDebugState(t *testing.T, tester *caddytest.Tester) frankenphp.Fr...
  function getNumThreads (line 226) | func getNumThreads(t *testing.T, tester *caddytest.Tester) int {
  function TestAddModuleWorkerViaAdminApi (line 231) | func TestAddModuleWorkerViaAdminApi(t *testing.T) {

FILE: caddy/app.go
  function RegisterWorkers (line 29) | func RegisterWorkers(name, fileName string, num int, wo ...frankenphp.Wo...
  type FrankenPHPApp (line 47) | type FrankenPHPApp struct
    method CaddyModule (line 70) | func (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
    method Provision (line 78) | func (f *FrankenPHPApp) Provision(ctx caddy.Context) error {
    method generateUniqueModuleWorkerName (line 102) | func (f *FrankenPHPApp) generateUniqueModuleWorkerName(filepath string...
    method addModuleWorkers (line 120) | func (f *FrankenPHPApp) addModuleWorkers(workers ...workerConfig) ([]w...
    method Start (line 140) | func (f *FrankenPHPApp) Start() error {
    method Stop (line 178) | func (f *FrankenPHPApp) Stop() error {
    method UnmarshalCaddyfile (line 204) | func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) err...
  function parseGlobalOption (line 326) | func parseGlobalOption(d *caddyfile.Dispenser, _ any) (any, error) {

FILE: caddy/caddy.go
  constant defaultDocumentRoot (line 14) | defaultDocumentRoot = "public"
  constant defaultWatchPattern (line 15) | defaultWatchPattern = "./**/*.{env,php,twig,yaml,yml}"
  function init (line 18) | func init() {
  function wrongSubDirectiveError (line 33) | func wrongSubDirectiveError(module string, allowedDirectives string, wro...

FILE: caddy/caddy_test.go
  function initServer (line 27) | func initServer(t *testing.T, tester *caddytest.Tester, config string, f...
  function skipIfSymlinkNotValid (line 47) | func skipIfSymlinkNotValid(t *testing.T, path string) {
  function escapeMetricLabel (line 61) | func escapeMetricLabel(s string) string {
  function TestMain (line 65) | func TestMain(m *testing.M) {
  function TestPHP (line 75) | func TestPHP(t *testing.T) {
  function TestLargeRequest (line 106) | func TestLargeRequest(t *testing.T) {
  function TestWorker (line 134) | func TestWorker(t *testing.T) {
  function TestGlobalAndModuleWorker (line 169) | func TestGlobalAndModuleWorker(t *testing.T) {
  function TestModuleWorkerInheritsEnv (line 222) | func TestModuleWorkerInheritsEnv(t *testing.T) {
  function TestNamedModuleWorkers (line 244) | func TestNamedModuleWorkers(t *testing.T) {
  function TestEnv (line 296) | func TestEnv(t *testing.T) {
  function TestJsonEnv (line 327) | func TestJsonEnv(t *testing.T) {
  function TestCustomCaddyVariablesInEnv (line 410) | func TestCustomCaddyVariablesInEnv(t *testing.T) {
  function TestPHPServerDirective (line 444) | func TestPHPServerDirective(t *testing.T) {
  function TestPHPServerDirectiveDisableFileServer (line 465) | func TestPHPServerDirectiveDisableFileServer(t *testing.T) {
  function TestMetrics (line 489) | func TestMetrics(t *testing.T) {
  function TestWorkerMetrics (line 570) | func TestWorkerMetrics(t *testing.T) {
  function TestNamedWorkerMetrics (line 671) | func TestNamedWorkerMetrics(t *testing.T) {
  function TestAutoWorkerConfig (line 766) | func TestAutoWorkerConfig(t *testing.T) {
  function TestAllDefinedServerVars (line 861) | func TestAllDefinedServerVars(t *testing.T) {
  function TestPHPIniConfiguration (line 900) | func TestPHPIniConfiguration(t *testing.T) {
  function TestPHPIniBlockConfiguration (line 928) | func TestPHPIniBlockConfiguration(t *testing.T) {
  function testSingleIniConfiguration (line 957) | func testSingleIniConfiguration(tester *caddytest.Tester, key string, va...
  function TestOsEnv (line 968) | func TestOsEnv(t *testing.T) {
  function TestMaxWaitTime (line 998) | func TestMaxWaitTime(t *testing.T) {
  function TestMaxWaitTimeWorker (line 1039) | func TestMaxWaitTimeWorker(t *testing.T) {
  function getStatusCode (line 1110) | func getStatusCode(url string, t *testing.T) int {
  function TestMultiWorkersMetrics (line 1121) | func TestMultiWorkersMetrics(t *testing.T) {
  function TestDisabledMetrics (line 1230) | func TestDisabledMetrics(t *testing.T) {
  function TestWorkerRestart (line 1310) | func TestWorkerRestart(t *testing.T) {
  function TestWorkerMatchDirective (line 1413) | func TestWorkerMatchDirective(t *testing.T) {
  function TestWorkerMatchDirectiveWithMultipleWorkers (line 1446) | func TestWorkerMatchDirectiveWithMultipleWorkers(t *testing.T) {
  function TestWorkerMatchDirectiveWithoutFileServer (line 1487) | func TestWorkerMatchDirectiveWithoutFileServer(t *testing.T) {
  function TestDd (line 1520) | func TestDd(t *testing.T) {
  function TestLog (line 1544) | func TestLog(t *testing.T) {
  function TestSymlinkWorkerPaths (line 1573) | func TestSymlinkWorkerPaths(t *testing.T) {
  function TestSymlinkResolveRoot (line 1708) | func TestSymlinkResolveRoot(t *testing.T) {
  function TestSymlinkWorkerBehavior (line 1768) | func TestSymlinkWorkerBehavior(t *testing.T) {

FILE: caddy/config_test.go
  function TestModuleWorkerDuplicateFilenamesFail (line 10) | func TestModuleWorkerDuplicateFilenamesFail(t *testing.T) {
  function TestModuleWorkersWithDifferentFilenames (line 38) | func TestModuleWorkersWithDifferentFilenames(t *testing.T) {
  function TestModuleWorkersDifferentNamesSucceed (line 64) | func TestModuleWorkersDifferentNamesSucceed(t *testing.T) {
  function TestModuleWorkerWithEnvironmentVariables (line 119) | func TestModuleWorkerWithEnvironmentVariables(t *testing.T) {
  function TestModuleWorkerWithWatchConfiguration (line 153) | func TestModuleWorkerWithWatchConfiguration(t *testing.T) {
  function TestModuleWorkerWithCustomName (line 189) | func TestModuleWorkerWithCustomName(t *testing.T) {

FILE: caddy/extinit.go
  function init (line 16) | func init() {
  function cmdInitExtension (line 31) | func cmdInitExtension(_ caddycmd.Flags) (int, error) {

FILE: caddy/frankenphp/main.go
  function main (line 13) | func main() {

FILE: caddy/hotreload-skip.go
  type hotReloadContext (line 11) | type hotReloadContext struct
  method configureHotReload (line 14) | func (_ *FrankenPHPModule) configureHotReload(_ *FrankenPHPApp) error {
  method unmarshalHotReload (line 18) | func (_ *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) er...

FILE: caddy/hotreload.go
  constant defaultHotReloadPattern (line 17) | defaultHotReloadPattern = "./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,...
  type hotReloadContext (line 19) | type hotReloadContext struct
  type hotReloadConfig (line 24) | type hotReloadConfig struct
  method configureHotReload (line 29) | func (f *FrankenPHPModule) configureHotReload(app *FrankenPHPApp) error {
  method unmarshalHotReload (line 57) | func (f *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) er...
  function uniqueID (line 91) | func uniqueID(s any) (string, error) {

FILE: caddy/hotreload_test.go
  function TestHotReload (line 19) | func TestHotReload(t *testing.T) {

FILE: caddy/mercure-skip.go
  type mercureContext (line 10) | type mercureContext struct
  method assignMercureHub (line 13) | func (f *FrankenPHPModule) assignMercureHub(_ caddy.Context) {
  function createMercureRoute (line 16) | func createMercureRoute() (caddyhttp.Route, error) {

FILE: caddy/mercure.go
  function init (line 17) | func init() {
  type mercureContext (line 21) | type mercureContext struct
  method assignMercureHub (line 25) | func (f *FrankenPHPModule) assignMercureHub(ctx caddy.Context) {
  function createMercureRoute (line 40) | func createMercureRoute() (caddyhttp.Route, error) {

FILE: caddy/module.go
  type FrankenPHPModule (line 34) | type FrankenPHPModule struct
    method CaddyModule (line 57) | func (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {
    method Provision (line 65) | func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
    method ServeHTTP (line 200) | func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Re...
    method UnmarshalCaddyfile (line 259) | func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) ...
  function needReplacement (line 195) | func needReplacement(s string) bool {
  function parseCaddyfile (line 335) | func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler...
  function parsePhpServer (line 369) | func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue...
  function prependWorkerRoutes (line 631) | func prependWorkerRoutes(routes caddyhttp.RouteList, h httpcaddyfile.Hel...

FILE: caddy/module_test.go
  function TestRootBehavesTheSameOutsideAndInsidePhpServer (line 12) | func TestRootBehavesTheSameOutsideAndInsidePhpServer(t *testing.T) {

FILE: caddy/php-cli.go
  function init (line 14) | func init() {
  function cmdPHPCLI (line 28) | func cmdPHPCLI(fs caddycmd.Flags) (int, error) {

FILE: caddy/php-server.go
  function init (line 26) | func init() {
  function cmdPHPServer (line 62) | func cmdPHPServer(fs caddycmd.Flags) (int, error) {

FILE: caddy/watcher_test.go
  function TestWorkerWithInactiveWatcher (line 12) | func TestWorkerWithInactiveWatcher(t *testing.T) {

FILE: caddy/workerconfig.go
  type workerConfig (line 25) | type workerConfig struct
    method inheritEnv (line 164) | func (wc *workerConfig) inheritEnv(env map[string]string) {
    method matchesPath (line 176) | func (wc *workerConfig) matchesPath(r *http.Request, documentRoot stri...
  function unmarshalWorker (line 51) | func unmarshalWorker(d *caddyfile.Dispenser) (workerConfig, error) {

FILE: cgi.go
  function addKnownVariablesToServer (line 47) | func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C....
  function addHeadersToServer (line 162) | func addHeadersToServer(ctx context.Context, request *http.Request, trac...
  function addPreparedEnvToServer (line 178) | func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zva...
  function go_register_server_variables (line 186) | func go_register_server_variables(threadIndex C.uintptr_t, trackVarsArra...
  function splitCgiPath (line 200) | func splitCgiPath(fc *frankenPHPContext) {
  function splitPos (line 233) | func splitPos(path string, splitPath []string) int {
  function go_update_request_info (line 293) | func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_reques...
  function sanitizedPathJoin (line 340) | func sanitizedPathJoin(root, reqPath string) string {
  constant separator (line 358) | separator = string(filepath.Separator)
  function ensureLeadingSlash (line 360) | func ensureLeadingSlash(path string) string {
  function toUnsafeChar (line 372) | func toUnsafeChar(s string) *C.char {
  function newPersistentZendString (line 377) | func newPersistentZendString(str string) *C.zend_string {
  function tlsProtocol (line 383) | func tlsProtocol(proto uint16) *C.zend_string {

FILE: cgi_test.go
  function TestEnsureLeadingSlash (line 10) | func TestEnsureLeadingSlash(t *testing.T) {
  function TestSplitPos (line 36) | func TestSplitPos(t *testing.T) {
  function TestSplitPosUnicodeSecurityRegression (line 190) | func TestSplitPosUnicodeSecurityRegression(t *testing.T) {

FILE: cli.go
  function ExecuteScriptCLI (line 9) | func ExecuteScriptCLI(script string, args []string) int {
  function ExecutePHPCode (line 22) | func ExecutePHPCode(phpCode string) int {

FILE: cli_test.go
  function TestExecuteScriptCLI (line 14) | func TestExecuteScriptCLI(t *testing.T) {
  function TestExecuteCLICode (line 35) | func TestExecuteCLICode(t *testing.T) {
  function ExampleExecuteScriptCLI (line 48) | func ExampleExecuteScriptCLI() {

FILE: context.go
  type frankenPHPContext (line 16) | type frankenPHPContext struct
    method closeContext (line 123) | func (fc *frankenPHPContext) closeContext() {
    method validate (line 133) | func (fc *frankenPHPContext) validate() error {
    method clientHasClosed (line 154) | func (fc *frankenPHPContext) clientHasClosed() bool {
    method reject (line 168) | func (fc *frankenPHPContext) reject(err error) {
  type contextHolder (line 45) | type contextHolder struct
  function fromContext (line 51) | func fromContext(ctx context.Context) (fctx *frankenPHPContext, ok bool) {
  function newFrankenPHPContext (line 56) | func newFrankenPHPContext() *frankenPHPContext {
  function NewRequestWithContext (line 64) | func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*htt...
  function newDummyContext (line 106) | func newDummyContext(requestPath string, opts ...RequestOption) (*franke...

FILE: debugstate.go
  type ThreadDebugState (line 8) | type ThreadDebugState struct
  type FrankenPHPDebugState (line 18) | type FrankenPHPDebugState struct
  function DebugState (line 24) | func DebugState() FrankenPHPDebugState {
  function threadDebugState (line 41) | func threadDebugState(thread *phpThread) ThreadDebugState {

FILE: docs/translate.php
  function makeGeminiRequest (line 33) | function makeGeminiRequest(string $systemPrompt, string $userPrompt, str...
  function createPrompt (line 67) | function createPrompt(string $language, string $englishFile, string $cur...
  function sanitizeMarkdown (line 91) | function sanitizeMarkdown(string $markdown): string

FILE: embed.go
  function init (line 37) | func init() {
  function untar (line 56) | func untar(dir string) (err error) {
  function nativeRelPath (line 162) | func nativeRelPath(p string) (string, error) {

FILE: env.go
  function go_init_os_env (line 14) | func go_init_os_env(mainThreadEnv *C.zend_array) {
  function go_putenv (line 27) | func go_putenv(name *C.char, nameLen C.int, val *C.char, valLen C.int) C...

FILE: ext.go
  function RegisterExtension (line 16) | func RegisterExtension(me unsafe.Pointer) {
  function registerExtensions (line 20) | func registerExtensions() {

FILE: frankenphp.c
  function frankenphp_version (line 54) | frankenphp_version frankenphp_get_version() {
  function frankenphp_config (line 61) | frankenphp_config frankenphp_get_config() {
  function frankenphp_shutdown_dummy_request (line 280) | bool frankenphp_shutdown_dummy_request(void) {
  function get_full_env (line 290) | void get_full_env(zval *track_vars_array) {
  function frankenphp_worker_request_startup (line 297) | static int frankenphp_worker_request_startup() {
  function PHP_FUNCTION (line 360) | PHP_FUNCTION(frankenphp_finish_request) { /* {{{ */
  function PHP_FUNCTION (line 376) | PHP_FUNCTION(frankenphp_putenv) {
  function PHP_FUNCTION (line 433) | PHP_FUNCTION(frankenphp_getenv) {
  function PHP_FUNCTION (line 461) | PHP_FUNCTION(frankenphp_request_headers) {
  function add_response_header (line 485) | static void add_response_header(sapi_header_struct *h,
  function PHP_FUNCTION (line 518) | PHP_FUNCTION(frankenphp_response_headers) /* {{{ */
  function PHP_FUNCTION (line 529) | PHP_FUNCTION(frankenphp_handle_request) {
  function PHP_FUNCTION (line 611) | PHP_FUNCTION(headers_send) {
  function PHP_FUNCTION (line 632) | PHP_FUNCTION(mercure_publish) {
  function PHP_FUNCTION (line 674) | PHP_FUNCTION(frankenphp_log) {
  function PHP_MINIT_FUNCTION (line 695) | PHP_MINIT_FUNCTION(frankenphp) {
  function frankenphp_startup (line 733) | static int frankenphp_startup(sapi_module_struct *sapi_module) {
  function frankenphp_deactivate (line 739) | static int frankenphp_deactivate(void) { return SUCCESS; }
  function frankenphp_ub_write (line 741) | static size_t frankenphp_ub_write(const char *str, size_t str_length) {
  function frankenphp_send_headers (line 752) | static int frankenphp_send_headers(sapi_headers_struct *sapi_headers) {
  function frankenphp_sapi_flush (line 777) | static void frankenphp_sapi_flush(void *server_context) {
  function frankenphp_read_post (line 784) | static size_t frankenphp_read_post(char *buffer, size_t count_bytes) {
  function frankenphp_register_trusted_var (line 793) | static inline void frankenphp_register_trusted_var(zend_string *z_key,
  function frankenphp_register_server_vars (line 814) | void frankenphp_register_server_vars(zval *track_vars_array,
  function zend_string (line 864) | zend_string *frankenphp_init_persistent_string(const char *string, size_...
  function frankenphp_init_interned_strings (line 876) | static void frankenphp_init_interned_strings(void) {
  function frankenphp_register_variable_from_request_info (line 890) | static inline void
  function frankenphp_register_variables_from_request_info (line 903) | static void
  function frankenphp_register_known_variable (line 923) | void frankenphp_register_known_variable(zend_string *z_key, char *value,
  function frankenphp_register_variable_safe (line 932) | void frankenphp_register_variable_safe(char *key, char *val, size_t val_...
  function register_server_variable_filtered (line 948) | static inline void register_server_variable_filtered(const char *key,
  function frankenphp_register_variables (line 957) | static void frankenphp_register_variables(zval *track_vars_array) {
  function frankenphp_log_message (line 971) | static void frankenphp_log_message(const char *message, int syslog_type_...
  function set_thread_name (line 1024) | static void set_thread_name(char *thread_name) {
  function frankenphp_new_main_thread (line 1175) | int frankenphp_new_main_thread(int num_threads) {
  function frankenphp_new_php_thread (line 1186) | bool frankenphp_new_php_thread(uintptr_t thread_index) {
  function frankenphp_request_startup (line 1195) | static int frankenphp_request_startup() {
  function frankenphp_execute_script (line 1207) | int frankenphp_execute_script(char *file_name) {
  function cli_register_file_handles (line 1255) | static void cli_register_file_handles(void) {
  function sapi_cli_register_variables (line 1304) | static void sapi_cli_register_variables(zval *track_vars_array) /* {{{ */
  function frankenphp_execute_script_cli (line 1369) | int frankenphp_execute_script_cli(char *script, int argc, char **argv,
  function frankenphp_reset_opcache (line 1396) | int frankenphp_reset_opcache(void) {
  function frankenphp_get_current_memory_limit (line 1406) | int frankenphp_get_current_memory_limit() { return PG(memory_limit); }
  function register_internal_extensions (line 1412) | int register_internal_extensions(void) {
  function register_extensions (line 1430) | void register_extensions(zend_module_entry **m, int len) {

FILE: frankenphp.go
  type contextKeyStruct (line 42) | type contextKeyStruct struct
  type ErrRejected (line 72) | type ErrRejected struct
    method Error (line 77) | func (e ErrRejected) Error() string {
  type syslogLevel (line 81) | type syslogLevel
    method String (line 94) | func (l syslogLevel) String() string {
  constant syslogLevelEmerg (line 84) | syslogLevelEmerg  syslogLevel = iota
  constant syslogLevelAlert (line 85) | syslogLevelAlert
  constant syslogLevelCrit (line 86) | syslogLevelCrit
  constant syslogLevelErr (line 87) | syslogLevelErr
  constant syslogLevelWarn (line 88) | syslogLevelWarn
  constant syslogLevelNotice (line 89) | syslogLevelNotice
  constant syslogLevelInfo (line 90) | syslogLevelInfo
  constant syslogLevelDebug (line 91) | syslogLevelDebug
  type PHPVersion (line 115) | type PHPVersion struct
  type PHPConfig (line 124) | type PHPConfig struct
  function Version (line 132) | func Version() PHPVersion {
  function Config (line 145) | func Config() PHPConfig {
  function calculateMaxThreads (line 156) | func calculateMaxThreads(opt *opt) (numWorkers int, _ error) {
  function Init (line 239) | func Init(options ...Option) error {
  function Shutdown (line 360) | func Shutdown() {
  function ServeHTTP (line 390) | func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request...
  function go_ub_write (line 425) | func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.size_t)...
  function go_apache_request_headers (line 469) | func go_apache_request_headers(threadIndex C.uintptr_t) (*C.go_string, C...
  function addHeader (line 507) | func addHeader(ctx context.Context, fc *frankenPHPContext, h *C.sapi_hea...
  function splitRawHeader (line 520) | func splitRawHeader(rawHeader *C.char, length int) (string, string) {
  function go_write_headers (line 551) | func go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C....
  function go_sapi_flush (line 603) | func go_sapi_flush(threadIndex C.uintptr_t) bool {
  function go_read_post (line 633) | func go_read_post(threadIndex C.uintptr_t, cBuf *C.char, countBytes C.si...
  function go_read_cookies (line 652) | func go_read_cookies(threadIndex C.uintptr_t) *C.char {
  function getLogger (line 670) | func getLogger(threadIndex C.uintptr_t) (*slog.Logger, context.Context) {
  function go_log (line 690) | func go_log(threadIndex C.uintptr_t, message *C.char, level C.int) {
  function go_log_attrs (line 718) | func go_log_attrs(threadIndex C.uintptr_t, message *C.zend_string, cLeve...
  function mapToAttr (line 742) | func mapToAttr(input map[string]any) []slog.Attr {
  function go_is_context_done (line 753) | func go_is_context_done(threadIndex C.uintptr_t) C.bool {
  function convertArgs (line 757) | func convertArgs(args []string) (C.int, []*C.char) {
  function freeArgs (line 766) | func freeArgs(argv []*C.char) {
  function timeoutChan (line 772) | func timeoutChan(timeout time.Duration) <-chan time.Time {
  function resetGlobals (line 780) | func resetGlobals() {

FILE: frankenphp.h
  function HRESULT (line 23) | static inline HRESULT LongLongAdd(LONGLONG llAugend, LONGLONG llAddend,
  function HRESULT (line 33) | static inline HRESULT LongLongSub(LONGLONG llMinuend, LONGLONG llSubtrah...
  type go_string (line 55) | typedef struct go_string {
  type frankenphp_server_vars (line 60) | typedef struct frankenphp_server_vars {
  type frankenphp_interned_strings_t (line 143) | typedef struct frankenphp_interned_strings_t {
  type frankenphp_version (line 151) | typedef struct frankenphp_version {
  type frankenphp_config (line 161) | typedef struct frankenphp_config {

FILE: frankenphp.stub.php
  function frankenphp_handle_request (line 17) | function frankenphp_handle_request(callable $callback): bool {}
  function headers_send (line 19) | function headers_send(int $status = 200): int {}
  function frankenphp_finish_request (line 21) | function frankenphp_finish_request(): bool {}
  function fastcgi_finish_request (line 26) | function fastcgi_finish_request(): bool {}
  function frankenphp_request_headers (line 28) | function frankenphp_request_headers(): array {}
  function apache_request_headers (line 33) | function apache_request_headers(): array {}
  function getallheaders (line 38) | function getallheaders(): array {}
  function frankenphp_response_headers (line 40) | function frankenphp_response_headers(): array|bool {}
  function apache_response_headers (line 45) | function apache_response_headers(): array|bool {}
  function mercure_publish (line 50) | function mercure_publish(string|array $topics, string $data = '', bool $...
  function frankenphp_log (line 56) | function frankenphp_log(string $message, int $level = 0, array $context ...

FILE: frankenphp_arginfo.h
  function register_frankenphp_symbols (line 69) | static void register_frankenphp_symbols(int module_number)

FILE: frankenphp_test.go
  type testOptions (line 39) | type testOptions struct
  function runTest (line 52) | func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Req...
  function testRequest (line 110) | func testRequest(req *http.Request, handler func(http.ResponseWriter, *h...
  function testGet (line 122) | func testGet(url string, handler func(http.ResponseWriter, *http.Request...
  function testPost (line 129) | func testPost(url string, body string, handler func(http.ResponseWriter,...
  function TestMain (line 137) | func TestMain(m *testing.M) {
  function TestHelloWorld_module (line 153) | func TestHelloWorld_module(t *testing.T) { testHelloWorld(t, nil) }
  function TestHelloWorld_worker (line 154) | func TestHelloWorld_worker(t *testing.T) {
  function testHelloWorld (line 157) | func testHelloWorld(t *testing.T, opts *testOptions) {
  function TestEnvVarsInPhpIni (line 164) | func TestEnvVarsInPhpIni(t *testing.T) {
  function TestFinishRequest_module (line 175) | func TestFinishRequest_module(t *testing.T) { testFinishRequest(t, nil) }
  function TestFinishRequest_worker (line 176) | func TestFinishRequest_worker(t *testing.T) {
  function testFinishRequest (line 179) | func testFinishRequest(t *testing.T, opts *testOptions) {
  function TestServerVariable_module (line 186) | func TestServerVariable_module(t *testing.T) {
  function TestServerVariable_worker (line 189) | func TestServerVariable_worker(t *testing.T) {
  function testServerVariable (line 192) | func testServerVariable(t *testing.T, opts *testOptions) {
  function TestPathInfo_module (line 227) | func TestPathInfo_module(t *testing.T) { testPathInfo(t, nil) }
  function TestPathInfo_worker (line 228) | func TestPathInfo_worker(t *testing.T) {
  function testPathInfo (line 231) | func testPathInfo(t *testing.T, opts *testOptions) {
  function TestHeaders_module (line 261) | func TestHeaders_module(t *testing.T) { testHeaders(t, nil) }
  function TestHeaders_worker (line 262) | func TestHeaders_worker(t *testing.T) { testHeaders(t, &testOptions{work...
  function testHeaders (line 263) | func testHeaders(t *testing.T, opts *testOptions) {
  function TestResponseHeaders_module (line 277) | func TestResponseHeaders_module(t *testing.T) { testResponseHeaders(t, n...
  function TestResponseHeaders_worker (line 278) | func TestResponseHeaders_worker(t *testing.T) {
  function testResponseHeaders (line 281) | func testResponseHeaders(t *testing.T, opts *testOptions) {
  function TestInput_module (line 299) | func TestInput_module(t *testing.T) { testInput(t, nil) }
  function TestInput_worker (line 300) | func TestInput_worker(t *testing.T) { testInput(t, &testOptions{workerSc...
  function testInput (line 301) | func testInput(t *testing.T, opts *testOptions) {
  function TestPostSuperGlobals_module (line 310) | func TestPostSuperGlobals_module(t *testing.T) { testPostSuperGlobals(t,...
  function TestPostSuperGlobals_worker (line 311) | func TestPostSuperGlobals_worker(t *testing.T) {
  function testPostSuperGlobals (line 314) | func testPostSuperGlobals(t *testing.T, opts *testOptions) {
  function TestRequestSuperGlobal_module (line 328) | func TestRequestSuperGlobal_module(t *testing.T) { testRequestSuperGloba...
  function TestRequestSuperGlobal_worker (line 329) | func TestRequestSuperGlobal_worker(t *testing.T) {
  function testRequestSuperGlobal (line 334) | func testRequestSuperGlobal(t *testing.T, opts *testOptions) {
  function TestRequestSuperGlobalConditional_worker (line 349) | func TestRequestSuperGlobalConditional_worker(t *testing.T) {
  function TestCookies_module (line 378) | func TestCookies_module(t *testing.T) { testCookies(t, nil) }
  function TestCookies_worker (line 379) | func TestCookies_worker(t *testing.T) { testCookies(t, &testOptions{work...
  function testCookies (line 380) | func testCookies(t *testing.T, opts *testOptions) {
  function TestMalformedCookie (line 392) | func TestMalformedCookie(t *testing.T) {
  function TestSession_module (line 413) | func TestSession_module(t *testing.T) { testSession(t, nil) }
  function TestSession_worker (line 414) | func TestSession_worker(t *testing.T) {
  function testSession (line 417) | func testSession(t *testing.T, opts *testOptions) {
  function TestPhpInfo_module (line 443) | func TestPhpInfo_module(t *testing.T) { testPhpInfo(t, nil) }
  function TestPhpInfo_worker (line 444) | func TestPhpInfo_worker(t *testing.T) { testPhpInfo(t, &testOptions{work...
  function testPhpInfo (line 445) | func testPhpInfo(t *testing.T, opts *testOptions) {
  function TestPersistentObject_module (line 459) | func TestPersistentObject_module(t *testing.T) { testPersistentObject(t,...
  function TestPersistentObject_worker (line 460) | func TestPersistentObject_worker(t *testing.T) {
  function testPersistentObject (line 463) | func testPersistentObject(t *testing.T, opts *testOptions) {
  function TestAutoloader_module (line 474) | func TestAutoloader_module(t *testing.T) { testAutoloader(t, nil) }
  function TestAutoloader_worker (line 475) | func TestAutoloader_worker(t *testing.T) {
  function testAutoloader (line 478) | func testAutoloader(t *testing.T, opts *testOptions) {
  function TestLog_error_log_module (line 487) | func TestLog_error_log_module(t *testing.T) { testLog_error_log(t, &test...
  function TestLog_error_log_worker (line 488) | func TestLog_error_log_worker(t *testing.T) {
  function testLog_error_log (line 491) | func testLog_error_log(t *testing.T, opts *testOptions) {
  function TestLog_frankenphp_log_module (line 504) | func TestLog_frankenphp_log_module(t *testing.T) { testLog_frankenphp_lo...
  function TestLog_frankenphp_log_worker (line 505) | func TestLog_frankenphp_log_worker(t *testing.T) {
  function testLog_frankenphp_log (line 508) | func testLog_frankenphp_log(t *testing.T, opts *testOptions) {
  function TestConnectionAbort_module (line 530) | func TestConnectionAbort_module(t *testing.T) { testConnectionAbort(t, &...
  function TestConnectionAbort_worker (line 531) | func TestConnectionAbort_worker(t *testing.T) {
  function testConnectionAbort (line 534) | func testConnectionAbort(t *testing.T, opts *testOptions) {
  function TestException_module (line 559) | func TestException_module(t *testing.T) { testException(t, &testOptions{...
  function TestException_worker (line 560) | func TestException_worker(t *testing.T) {
  function testException (line 563) | func testException(t *testing.T, opts *testOptions) {
  function TestEarlyHints_module (line 572) | func TestEarlyHints_module(t *testing.T) { testEarlyHints(t, &testOption...
  function TestEarlyHints_worker (line 573) | func TestEarlyHints_worker(t *testing.T) {
  function testEarlyHints (line 576) | func testEarlyHints(t *testing.T, opts *testOptions) {
  type streamResponseRecorder (line 604) | type streamResponseRecorder struct
    method Write (line 609) | func (srr *streamResponseRecorder) Write(buf []byte) (int, error) {
  function TestFlush_module (line 615) | func TestFlush_module(t *testing.T) { testFlush(t, &testOptions{}) }
  function TestFlush_worker (line 616) | func TestFlush_worker(t *testing.T) {
  function testFlush (line 619) | func testFlush(t *testing.T, opts *testOptions) {
  function TestLargeRequest_module (line 639) | func TestLargeRequest_module(t *testing.T) {
  function TestLargeRequest_worker (line 642) | func TestLargeRequest_worker(t *testing.T) {
  function testLargeRequest (line 645) | func testLargeRequest(t *testing.T, opts *testOptions) {
  function TestVersion (line 658) | func TestVersion(t *testing.T) {
  function TestFiberNoCgo_module (line 668) | func TestFiberNoCgo_module(t *testing.T) { testFiberNoCgo(t, &testOption...
  function TestFiberNonCgo_worker (line 669) | func TestFiberNonCgo_worker(t *testing.T) {
  function testFiberNoCgo (line 672) | func testFiberNoCgo(t *testing.T, opts *testOptions) {
  function TestFiberBasic_module (line 679) | func TestFiberBasic_module(t *testing.T) { testFiberBasic(t, &testOption...
  function TestFiberBasic_worker (line 680) | func TestFiberBasic_worker(t *testing.T) {
  function testFiberBasic (line 683) | func testFiberBasic(t *testing.T, opts *testOptions) {
  function TestRequestHeaders_module (line 690) | func TestRequestHeaders_module(t *testing.T) { testRequestHeaders(t, &te...
  function TestRequestHeaders_worker (line 691) | func TestRequestHeaders_worker(t *testing.T) {
  function testRequestHeaders (line 694) | func testRequestHeaders(t *testing.T, opts *testOptions) {
  function TestFailingWorker (line 706) | func TestFailingWorker(t *testing.T) {
  function TestEnv_module (line 716) | func TestEnv_module(t *testing.T) {
  function TestEnv_worker (line 719) | func TestEnv_worker(t *testing.T) {
  function testEnv (line 724) | func testEnv(t *testing.T, opts *testOptions) {
  function TestEnvIsResetInNonWorkerMode (line 742) | func TestEnvIsResetInNonWorkerMode(t *testing.T) {
  function TestEnvIsNotResetInWorkerMode (line 756) | func TestEnvIsNotResetInWorkerMode(t *testing.T) {
  function TestModificationsToEnvPersistAcrossRequests (line 770) | func TestModificationsToEnvPersistAcrossRequests(t *testing.T) {
  function TestFileUpload_module (line 782) | func TestFileUpload_module(t *testing.T) { testFileUpload(t, &testOption...
  function TestFileUpload_worker (line 783) | func TestFileUpload_worker(t *testing.T) {
  function testFileUpload (line 786) | func testFileUpload(t *testing.T, opts *testOptions) {
  function ExampleServeHTTP (line 805) | func ExampleServeHTTP() {
  function BenchmarkHelloWorld (line 824) | func BenchmarkHelloWorld(b *testing.B) {
  function BenchmarkEcho (line 847) | func BenchmarkEcho(b *testing.B) {
  function BenchmarkServerSuperGlobal (line 910) | func BenchmarkServerSuperGlobal(b *testing.B) {
  function BenchmarkUncommonHeaders (line 976) | func BenchmarkUncommonHeaders(b *testing.B) {
  function TestRejectInvalidHeaders_module (line 1024) | func TestRejectInvalidHeaders_module(t *testing.T) { testRejectInvalidHe...
  function TestRejectInvalidHeaders_worker (line 1025) | func TestRejectInvalidHeaders_worker(t *testing.T) {
  function testRejectInvalidHeaders (line 1028) | func testRejectInvalidHeaders(t *testing.T, opts *testOptions) {
  function TestFlushEmptyResponse_module (line 1045) | func TestFlushEmptyResponse_module(t *testing.T) { testFlushEmptyRespons...
  function TestFlushEmptyResponse_worker (line 1046) | func TestFlushEmptyResponse_worker(t *testing.T) {
  function testFlushEmptyResponse (line 1050) | func testFlushEmptyResponse(t *testing.T, opts *testOptions) {
  function TestFileStreamInWorkerMode (line 1059) | func TestFileStreamInWorkerMode(t *testing.T) {
  function FuzzRequest (line 1074) | func FuzzRequest(f *testing.F) {
  function TestSessionHandlerReset_worker (line 1108) | func TestSessionHandlerReset_worker(t *testing.T) {
  function TestSessionHandlerPreLoopPreserved_worker (line 1148) | func TestSessionHandlerPreLoopPreserved_worker(t *testing.T) {
  function TestSessionNoLeakBetweenRequests_worker (line 1197) | func TestSessionNoLeakBetweenRequests_worker(t *testing.T) {
  function TestSessionNoLeakAfterExit_worker (line 1249) | func TestSessionNoLeakAfterExit_worker(t *testing.T) {
  function TestOpcachePreload_module (line 1305) | func TestOpcachePreload_module(t *testing.T) {
  function TestOpcachePreload_worker (line 1309) | func TestOpcachePreload_worker(t *testing.T) {
  function testOpcachePreload (line 1313) | func testOpcachePreload(t *testing.T, opts *testOptions) {

FILE: hotreload.go
  function WithHotReload (line 15) | func WithHotReload(topic string, hub *mercure.Hub, patterns []string) Op...

FILE: internal/cpu/cpu_unix.go
  function ProbeCPUs (line 17) | func ProbeCPUs(probeTime time.Duration, maxCPUUsage float64, abort chan ...

FILE: internal/cpu/cpu_windows.go
  function ProbeCPUs (line 8) | func ProbeCPUs(probeTime time.Duration, _ float64, abort chan struct{}) ...

FILE: internal/extgen/arginfo.go
  type arginfoGenerator (line 12) | type arginfoGenerator struct
    method generate (line 16) | func (ag *arginfoGenerator) generate() error {
    method fixArginfoFile (line 38) | func (ag *arginfoGenerator) fixArginfoFile(stubFile string) error {

FILE: internal/extgen/cfile.go
  type cFileGenerator (line 16) | type cFileGenerator struct
    method generate (line 28) | func (cg *cFileGenerator) generate() error {
    method buildContent (line 38) | func (cg *cFileGenerator) buildContent() (string, error) {
    method getTemplateContent (line 58) | func (cg *cFileGenerator) getTemplateContent() (string, error) {
  type cTemplateData (line 20) | type cTemplateData struct
  function escapeCString (line 80) | func escapeCString(s string) string {

FILE: internal/extgen/cfile_namespace_test.go
  function TestNamespacedClassName (line 11) | func TestNamespacedClassName(t *testing.T) {
  function TestCFileGenerationWithNamespace (line 52) | func TestCFileGenerationWithNamespace(t *testing.T) {
  function TestCFileGenerationWithoutNamespace (line 112) | func TestCFileGenerationWithoutNamespace(t *testing.T) {
  function TestCFileGenerationWithNamespacedConstants (line 133) | func TestCFileGenerationWithNamespacedConstants(t *testing.T) {
  function TestCFileGenerationWithoutNamespacedConstants (line 245) | func TestCFileGenerationWithoutNamespacedConstants(t *testing.T) {
  function TestCFileTemplateFunctionMapCString (line 325) | func TestCFileTemplateFunctionMapCString(t *testing.T) {

FILE: internal/extgen/cfile_phpmethod_test.go
  function TestCFile_NamespacedPHPMethods (line 9) | func TestCFile_NamespacedPHPMethods(t *testing.T) {
  function TestCFile_PHP_METHOD_Integration (line 129) | func TestCFile_PHP_METHOD_Integration(t *testing.T) {
  function TestCFile_ClassMethodStringReturn (line 189) | func TestCFile_ClassMethodStringReturn(t *testing.T) {

FILE: internal/extgen/cfile_test.go
  function TestCFileGenerator_Generate (line 12) | func TestCFileGenerator_Generate(t *testing.T) {
  function TestCFileGenerator_BuildContent (line 62) | func TestCFileGenerator_BuildContent(t *testing.T) {
  function TestCFileGenerator_GetTemplateContent (line 147) | func TestCFileGenerator_GetTemplateContent(t *testing.T) {
  function TestCFileIntegrationWithGenerators (line 206) | func TestCFileIntegrationWithGenerators(t *testing.T) {
  function TestCFileErrorHandling (line 278) | func TestCFileErrorHandling(t *testing.T) {
  function TestCFileSpecialCharacters (line 293) | func TestCFileSpecialCharacters(t *testing.T) {
  function testCFileBasicStructure (line 322) | func testCFileBasicStructure(t *testing.T, content, baseName string) {
  function testCFileFunctions (line 338) | func testCFileFunctions(t *testing.T, content string, functions []phpFun...
  function testCFileClasses (line 345) | func testCFileClasses(t *testing.T, content string, classes []phpClass) {
  function TestCFileContentValidation (line 364) | func TestCFileContentValidation(t *testing.T) {
  function TestCFileConstants (line 403) | func TestCFileConstants(t *testing.T) {
  function TestCFileTemplateErrorHandling (line 452) | func TestCFileTemplateErrorHandling(t *testing.T) {
  function TestEscapeCString (line 463) | func TestEscapeCString(t *testing.T) {

FILE: internal/extgen/classparser.go
  type exportDirective (line 19) | type exportDirective struct
  type classParser (line 24) | type classParser struct
    method Parse (line 26) | func (cp *classParser) Parse(filename string) ([]phpClass, error) {
    method parse (line 30) | func (cp *classParser) parse(filename string) (classes []phpClass, err...
    method collectExportDirectives (line 106) | func (cp *classParser) collectExportDirectives(node *ast.File, fset *t...
    method extractPHPClassCommentWithLine (line 124) | func (cp *classParser) extractPHPClassCommentWithLine(commentGroup *as...
    method parseStructFields (line 139) | func (cp *classParser) parseStructFields(fields []*ast.Field) []phpCla...
    method parseStructField (line 152) | func (cp *classParser) parseStructField(fieldName string, field *ast.F...
    method typeToString (line 169) | func (cp *classParser) typeToString(expr ast.Expr) string {
    method goTypeToPHPType (line 193) | func (cp *classParser) goTypeToPHPType(goType string) phpType {
    method parseMethods (line 207) | func (cp *classParser) parseMethods(filename string) (methods []phpCla...
    method parseMethodSignature (line 294) | func (cp *classParser) parseMethodSignature(className, signature strin...
    method parseMethodParameter (line 332) | func (cp *classParser) parseMethodParameter(paramStr string) (phpParam...
    method sanitizeDefaultValue (line 356) | func (cp *classParser) sanitizeDefaultValue(value string) string {
    method extractGoMethodFunction (line 368) | func (cp *classParser) extractGoMethodFunction(scanner *bufio.Scanner,...

FILE: internal/extgen/classparser_test.go
  function TestClassParser (line 12) | func TestClassParser(t *testing.T) {
  function TestClassMethods (line 121) | func TestClassMethods(t *testing.T) {
  function TestMethodParameterParsing (line 187) | func TestMethodParameterParsing(t *testing.T) {
  function TestGoTypeToPHPType (line 268) | func TestGoTypeToPHPType(t *testing.T) {
  function TestTypeToString (line 298) | func TestTypeToString(t *testing.T) {
  function TestClassParserUnsupportedTypes (line 366) | func TestClassParserUnsupportedTypes(t *testing.T) {
  function TestClassParserGoTypeMismatch (line 513) | func TestClassParserGoTypeMismatch(t *testing.T) {

FILE: internal/extgen/constants_test.go
  function TestConstantsIntegration (line 12) | func TestConstantsIntegration(t *testing.T) {
  function TestConstantsIntegrationOctal (line 87) | func TestConstantsIntegrationOctal(t *testing.T) {

FILE: internal/extgen/constparser.go
  type ConstantParser (line 16) | type ConstantParser struct
    method parse (line 18) | func (cp *ConstantParser) parse(filename string) (constants []phpConst...
  function determineConstantType (line 181) | func determineConstantType(value string) phpType {

FILE: internal/extgen/constparser_test.go
  function TestConstantParser (line 12) | func TestConstantParser(t *testing.T) {
  function TestConstantParserErrors (line 173) | func TestConstantParserErrors(t *testing.T) {
  function TestConstantParserIotaSequence (line 218) | func TestConstantParserIotaSequence(t *testing.T) {
  function TestConstantParserConstBlock (line 247) | func TestConstantParserConstBlock(t *testing.T) {
  function TestConstantParserConstBlockWithBlockLevelDirective (line 282) | func TestConstantParserConstBlockWithBlockLevelDirective(t *testing.T) {
  function TestConstantParserMixedConstBlockAndIndividual (line 313) | func TestConstantParserMixedConstBlockAndIndividual(t *testing.T) {
  function TestConstantParserClassConstBlock (line 357) | func TestConstantParserClassConstBlock(t *testing.T) {
  function TestConstantParserClassConstBlockWithIota (line 388) | func TestConstantParserClassConstBlockWithIota(t *testing.T) {
  function TestConstantParserTypeDetection (line 420) | func TestConstantParserTypeDetection(t *testing.T) {
  function TestConstantParserClassConstants (line 450) | func TestConstantParserClassConstants(t *testing.T) {
  function TestConstantParserRegexMatch (line 554) | func TestConstantParserRegexMatch(t *testing.T) {
  function TestConstantParserClassConstRegex (line 577) | func TestConstantParserClassConstRegex(t *testing.T) {
  function TestConstantParserDeclRegex (line 612) | func TestConstantParserDeclRegex(t *testing.T) {
  function TestPHPConstantCValue (line 648) | func TestPHPConstantCValue(t *testing.T) {

FILE: internal/extgen/docs.go
  type DocumentationGenerator (line 13) | type DocumentationGenerator struct
    method generate (line 23) | func (dg *DocumentationGenerator) generate() error {
    method generateMarkdown (line 33) | func (dg *DocumentationGenerator) generateMarkdown() (string, error) {
  type DocTemplateData (line 17) | type DocTemplateData struct

FILE: internal/extgen/docs_test.go
  function TestDocumentationGenerator_Generate (line 12) | func TestDocumentationGenerator_Generate(t *testing.T) {
  function TestDocumentationGenerator_GenerateMarkdown (line 144) | func TestDocumentationGenerator_GenerateMarkdown(t *testing.T) {
  function TestDocumentationGenerator_Generate_InvalidDirectory (line 305) | func TestDocumentationGenerator_Generate_InvalidDirectory(t *testing.T) {
  function TestDocumentationGenerator_TemplateError (line 321) | func TestDocumentationGenerator_TemplateError(t *testing.T) {
  function BenchmarkDocumentationGenerator_GenerateMarkdown (line 343) | func BenchmarkDocumentationGenerator_GenerateMarkdown(b *testing.B) {

FILE: internal/extgen/errors.go
  type GeneratorError (line 5) | type GeneratorError struct
    method Error (line 11) | func (e *GeneratorError) Error() string {

FILE: internal/extgen/funcparser.go
  type FuncParser (line 15) | type FuncParser struct
    method parse (line 17) | func (fp *FuncParser) parse(filename string) (functions []phpFunction,...
    method extractGoFunction (line 90) | func (fp *FuncParser) extractGoFunction(scanner *bufio.Scanner, firstL...
    method parseSignature (line 115) | func (fp *FuncParser) parseSignature(signature string) (*phpFunction, ...
    method parseParameter (line 150) | func (fp *FuncParser) parseParameter(paramStr string) (phpParameter, e...
    method sanitizeDefaultValue (line 174) | func (fp *FuncParser) sanitizeDefaultValue(value string) string {

FILE: internal/extgen/funcparser_test.go
  function TestFunctionParser (line 12) | func TestFunctionParser(t *testing.T) {
  function TestSignatureParsing (line 129) | func TestSignatureParsing(t *testing.T) {
  function TestParameterParsing (line 217) | func TestParameterParsing(t *testing.T) {
  function TestFunctionParserUnsupportedTypes (line 297) | func TestFunctionParserUnsupportedTypes(t *testing.T) {
  function TestFunctionParserGoTypeMismatch (line 398) | func TestFunctionParserGoTypeMismatch(t *testing.T) {

FILE: internal/extgen/generator.go
  type Generator (line 8) | type Generator struct
    method Generate (line 19) | func (g *Generator) Generate() error {
    method setupBuildDirectory (line 52) | func (g *Generator) setupBuildDirectory() error {
    method parseSource (line 56) | func (g *Generator) parseSource() error {
    method generateStubFile (line 86) | func (g *Generator) generateStubFile() error {
    method generateArginfo (line 95) | func (g *Generator) generateArginfo() error {
    method generateHeaderFile (line 104) | func (g *Generator) generateHeaderFile() error {
    method generateCFile (line 113) | func (g *Generator) generateCFile() error {
    method generateGoFile (line 122) | func (g *Generator) generateGoFile() error {
    method generateDocumentation (line 131) | func (g *Generator) generateDocumentation() error {

FILE: internal/extgen/gofile.go
  type GoFileGenerator (line 18) | type GoFileGenerator struct
    method generate (line 33) | func (gg *GoFileGenerator) generate() error {
    method buildContent (line 44) | func (gg *GoFileGenerator) buildContent() (string, error) {
    method getTemplateContent (line 77) | func (gg *GoFileGenerator) getTemplateContent(data goTemplateData) (st...
    method phpTypeToGoType (line 123) | func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string {
  type goTemplateData (line 22) | type goTemplateData struct
  type GoMethodSignature (line 101) | type GoMethodSignature struct
  type GoParameter (line 107) | type GoParameter struct
  function extractGoFunctionName (line 132) | func extractGoFunctionName(goFunction string) string {
  function extractGoFunctionSignatureParams (line 153) | func extractGoFunctionSignatureParams(goFunction string) string {
  function extractGoFunctionSignatureReturn (line 182) | func extractGoFunctionSignatureReturn(goFunction string) string {
  function extractGoFunctionCallParams (line 215) | func extractGoFunctionCallParams(goFunction string) string {

FILE: internal/extgen/gofile_test.go
  function TestGoFileGenerator_Generate (line 15) | func TestGoFileGenerator_Generate(t *testing.T) {
  function TestGoFileGenerator_BuildContent (line 90) | func TestGoFileGenerator_BuildContent(t *testing.T) {
  function TestGoFileGenerator_PackageNameSanitization (line 285) | func TestGoFileGenerator_PackageNameSanitization(t *testing.T) {
  function TestGoFileGenerator_ErrorHandling (line 319) | func TestGoFileGenerator_ErrorHandling(t *testing.T) {
  function TestGoFileGenerator_ComplexScenario (line 361) | func TestGoFileGenerator_ComplexScenario(t *testing.T) {
  function TestGoFileGenerator_MethodWrapperWithNullableParams (line 441) | func TestGoFileGenerator_MethodWrapperWithNullableParams(t *testing.T) {
  function TestGoFileGenerator_MethodWrapperWithArrayParams (line 522) | func TestGoFileGenerator_MethodWrapperWithArrayParams(t *testing.T) {
  function TestGoFileGenerator_Idempotency (line 623) | func TestGoFileGenerator_Idempotency(t *testing.T) {
  function TestGoFileGenerator_HeaderComments (line 683) | func TestGoFileGenerator_HeaderComments(t *testing.T) {
  function TestExtractGoFunctionName (line 718) | func TestExtractGoFunctionName(t *testing.T) {
  function TestExtractGoFunctionSignatureParams (line 764) | func TestExtractGoFunctionSignatureParams(t *testing.T) {
  function TestExtractGoFunctionSignatureReturn (line 810) | func TestExtractGoFunctionSignatureReturn(t *testing.T) {
  function TestExtractGoFunctionCallParams (line 856) | func TestExtractGoFunctionCallParams(t *testing.T) {
  function TestGoFileGenerator_SourceFilePreservation (line 892) | func TestGoFileGenerator_SourceFilePreservation(t *testing.T) {
  function TestGoFileGenerator_WrapperParameterForwarding (line 940) | func TestGoFileGenerator_WrapperParameterForwarding(t *testing.T) {
  function TestGoFileGenerator_MalformedSource (line 995) | func TestGoFileGenerator_MalformedSource(t *testing.T) {
  function TestGoFileGenerator_MethodWrapperWithNullableArrayParams (line 1051) | func TestGoFileGenerator_MethodWrapperWithNullableArrayParams(t *testing...
  function createTempSourceFile (line 1112) | func createTempSourceFile(t *testing.T, content string) string {
  function TestGoFileGenerator_MethodWrapperWithCallableParams (line 1121) | func TestGoFileGenerator_MethodWrapperWithCallableParams(t *testing.T) {
  function TestGoFileGenerator_phpTypeToGoType (line 1208) | func TestGoFileGenerator_phpTypeToGoType(t *testing.T) {
  function testGeneratedFileBasicStructure (line 1240) | func testGeneratedFileBasicStructure(t *testing.T, content, expectedPack...
  function testGeneratedFileWrappers (line 1259) | func testGeneratedFileWrappers(t *testing.T, content string, functions [...
  function computeFileHash (line 1275) | func computeFileHash(t *testing.T, filename string) string {
  function assertContainsHeaderComment (line 1283) | func assertContainsHeaderComment(t *testing.T, filename string) {

FILE: internal/extgen/hfile.go
  type HeaderGenerator (line 15) | type HeaderGenerator struct
    method generate (line 26) | func (hg *HeaderGenerator) generate() error {
    method buildContent (line 36) | func (hg *HeaderGenerator) buildContent() (string, error) {
  type TemplateData (line 19) | type TemplateData struct

FILE: internal/extgen/hfile_test.go
  function TestHeaderGenerator_Generate (line 12) | func TestHeaderGenerator_Generate(t *testing.T) {
  function TestHeaderGenerator_BuildContent (line 33) | func TestHeaderGenerator_BuildContent(t *testing.T) {
  function TestHeaderGenerator_HeaderGuardGeneration (line 93) | func TestHeaderGenerator_HeaderGuardGeneration(t *testing.T) {
  function TestHeaderGenerator_BasicStructure (line 123) | func TestHeaderGenerator_BasicStructure(t *testing.T) {
  function TestHeaderGenerator_CompleteStructure (line 139) | func TestHeaderGenerator_CompleteStructure(t *testing.T) {
  function TestHeaderGenerator_ErrorHandling (line 171) | func TestHeaderGenerator_ErrorHandling(t *testing.T) {
  function TestHeaderGenerator_EmptyBaseName (line 182) | func TestHeaderGenerator_EmptyBaseName(t *testing.T) {
  function TestHeaderGenerator_ContentValidation (line 192) | func TestHeaderGenerator_ContentValidation(t *testing.T) {
  function TestHeaderGenerator_SpecialCharacterHandling (line 204) | func TestHeaderGenerator_SpecialCharacterHandling(t *testing.T) {
  function TestHeaderGenerator_TemplateErrorHandling (line 235) | func TestHeaderGenerator_TemplateErrorHandling(t *testing.T) {
  function TestHeaderGenerator_GuardConsistency (line 243) | func TestHeaderGenerator_GuardConsistency(t *testing.T) {
  function TestHeaderGenerator_MinimalContent (line 257) | func TestHeaderGenerator_MinimalContent(t *testing.T) {
  function testHeaderBasicStructure (line 276) | func testHeaderBasicStructure(t *testing.T, content, baseName string) {
  function testHeaderIncludeGuards (line 299) | func testHeaderIncludeGuards(t *testing.T, content, expectedGuard string) {

FILE: internal/extgen/integration_test.go
  constant testModuleName (line 18) | testModuleName = "github.com/frankenphp/test-extension"
  type IntegrationTestSuite (line 21) | type IntegrationTestSuite struct
    method createGoModule (line 62) | func (s *IntegrationTestSuite) createGoModule(sourceFile string) (stri...
    method runExtensionInit (line 102) | func (s *IntegrationTestSuite) runExtensionInit(sourceFile string) err...
    method cleanupGeneratedFiles (line 122) | func (s *IntegrationTestSuite) cleanupGeneratedFiles(originalSourceFil...
    method compileFrankenPHP (line 146) | func (s *IntegrationTestSuite) compileFrankenPHP(moduleDir string) (st...
    method runPHPCode (line 201) | func (s *IntegrationTestSuite) runPHPCode(phpCode string) (string, err...
    method verifyPHPSymbols (line 223) | func (s *IntegrationTestSuite) verifyPHPSymbols(functions []string, cl...
    method verifyFunctionBehavior (line 256) | func (s *IntegrationTestSuite) verifyFunctionBehavior(phpCode string, ...
  function setupTest (line 30) | func setupTest(t *testing.T) *IntegrationTestSuite {
  function TestBasicFunction (line 271) | func TestBasicFunction(t *testing.T) {
  function TestClassMethodsIntegration (line 348) | func TestClassMethodsIntegration(t *testing.T) {
  function TestConstants (line 460) | func TestConstants(t *testing.T) {
  function TestNamespace (line 561) | func TestNamespace(t *testing.T) {
  function TestInvalidSignature (line 651) | func TestInvalidSignature(t *testing.T) {
  function TestTypeMismatch (line 667) | func TestTypeMismatch(t *testing.T) {
  function TestMissingGenStub (line 687) | func TestMissingGenStub(t *testing.T) {
  function TestCallable (line 716) | func TestCallable(t *testing.T) {

FILE: internal/extgen/namespace_test.go
  function TestNamespaceParser (line 11) | func TestNamespaceParser(t *testing.T) {
  function TestGeneratorWithNamespace (line 84) | func TestGeneratorWithNamespace(t *testing.T) {

FILE: internal/extgen/nodes.go
  type phpType (line 9) | type phpType
  constant phpString (line 12) | phpString   phpType = "string"
  constant phpInt (line 13) | phpInt      phpType = "int"
  constant phpFloat (line 14) | phpFloat    phpType = "float"
  constant phpBool (line 15) | phpBool     phpType = "bool"
  constant phpArray (line 16) | phpArray    phpType = "array"
  constant phpObject (line 17) | phpObject   phpType = "object"
  constant phpMixed (line 18) | phpMixed    phpType = "mixed"
  constant phpVoid (line 19) | phpVoid     phpType = "void"
  constant phpNull (line 20) | phpNull     phpType = "null"
  constant phpTrue (line 21) | phpTrue     phpType = "true"
  constant phpFalse (line 22) | phpFalse    phpType = "false"
  constant phpCallable (line 23) | phpCallable phpType = "callable"
  type phpFunction (line 26) | type phpFunction struct
  type phpParameter (line 36) | type phpParameter struct
  type phpClass (line 44) | type phpClass struct
  type phpClassMethod (line 51) | type phpClassMethod struct
  type phpClassProperty (line 64) | type phpClassProperty struct
  type phpConstant (line 71) | type phpConstant struct
    method CValue (line 81) | func (c phpConstant) CValue() string {

FILE: internal/extgen/nsparser.go
  type NamespaceParser (line 11) | type NamespaceParser struct
    method parse (line 15) | func (np *NamespaceParser) parse(filename string) (string, error) {

FILE: internal/extgen/paramparser.go
  type ParameterParser (line 8) | type ParameterParser struct
    method analyzeParameters (line 15) | func (pp *ParameterParser) analyzeParameters(params []phpParameter) Pa...
    method generateParamDeclarations (line 27) | func (pp *ParameterParser) generateParamDeclarations(params []phpParam...
    method generateSingleParamDeclaration (line 41) | func (pp *ParameterParser) generateSingleParamDeclaration(param phpPar...
    method getDefaultValue (line 82) | func (pp *ParameterParser) getDefaultValue(param phpParameter, fallbac...
    method generateParamParsing (line 89) | func (pp *ParameterParser) generateParamParsing(params []phpParameter,...
    method generateParamParsingMacro (line 111) | func (pp *ParameterParser) generateParamParsingMacro(param phpParamete...
    method generateGoCallParams (line 153) | func (pp *ParameterParser) generateGoCallParams(params []phpParameter)...
    method generateSingleGoCallParam (line 166) | func (pp *ParameterParser) generateSingleGoCallParam(param phpParamete...
  type ParameterInfo (line 10) | type ParameterInfo struct

FILE: internal/extgen/paramparser_test.go
  function TestParameterParser_AnalyzeParameters (line 9) | func TestParameterParser_AnalyzeParameters(t *testing.T) {
  function TestParameterParser_GenerateParamDeclarations (line 58) | func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
  function TestParameterParser_GenerateParamParsing (line 213) | func TestParameterParser_GenerateParamParsing(t *testing.T) {
  function TestParameterParser_GenerateGoCallParams (line 263) | func TestParameterParser_GenerateGoCallParams(t *testing.T) {
  function TestParameterParser_GenerateParamParsingMacro (line 349) | func TestParameterParser_GenerateParamParsingMacro(t *testing.T) {
  function TestParameterParser_GetDefaultValue (line 442) | func TestParameterParser_GetDefaultValue(t *testing.T) {
  function TestParameterParser_GenerateSingleGoCallParam (line 479) | func TestParameterParser_GenerateSingleGoCallParam(t *testing.T) {
  function TestParameterParser_GenerateSingleParamDeclaration (line 562) | func TestParameterParser_GenerateSingleParamDeclaration(t *testing.T) {
  function TestParameterParser_Integration (line 645) | func TestParameterParser_Integration(t *testing.T) {

FILE: internal/extgen/parser.go
  type SourceParser (line 3) | type SourceParser struct
    method ParseFunctions (line 6) | func (p *SourceParser) ParseFunctions(filename string) ([]phpFunction,...
    method ParseClasses (line 12) | func (p *SourceParser) ParseClasses(filename string) ([]phpClass, erro...
    method ParseConstants (line 18) | func (p *SourceParser) ParseConstants(filename string) ([]phpConstant,...
    method ParseNamespace (line 24) | func (p *SourceParser) ParseNamespace(filename string) (string, error) {

FILE: internal/extgen/phpfunc.go
  type PHPFuncGenerator (line 8) | type PHPFuncGenerator struct
    method generate (line 13) | func (pfg *PHPFuncGenerator) generate(fn phpFunction) string {
    method generateGoCall (line 38) | func (pfg *PHPFuncGenerator) generateGoCall(fn phpFunction) string {
    method getCReturnType (line 61) | func (pfg *PHPFuncGenerator) getCReturnType(returnType phpType) string {
    method generateReturnCode (line 74) | func (pfg *PHPFuncGenerator) generateReturnCode(returnType phpType) st...

FILE: internal/extgen/phpfunc_namespace_test.go
  function TestPHPFuncGenerator_NamespacedFunctions (line 9) | func TestPHPFuncGenerator_NamespacedFunctions(t *testing.T) {
  function TestGetNamespacedFunctionName (line 56) | func TestGetNamespacedFunctionName(t *testing.T) {
  function TestCFileWithNamespacedPHPFunctions (line 92) | func TestCFileWithNamespacedPHPFunctions(t *testing.T) {

FILE: internal/extgen/phpfunc_test.go
  function TestPHPFunctionGenerator_Generate (line 10) | func TestPHPFunctionGenerator_Generate(t *testing.T) {
  function TestPHPFunctionGenerator_GenerateParamDeclarations (line 156) | func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) {
  function TestPHPFunctionGenerator_GenerateReturnCode (line 234) | func TestPHPFunctionGenerator_GenerateReturnCode(t *testing.T) {
  function TestPHPFunctionGenerator_GenerateGoCallParams (line 301) | func TestPHPFunctionGenerator_GenerateGoCallParams(t *testing.T) {
  function TestPHPFunctionGenerator_AnalyzeParameters (line 370) | func TestPHPFunctionGenerator_AnalyzeParameters(t *testing.T) {

FILE: internal/extgen/srcanalyzer.go
  type SourceAnalyzer (line 11) | type SourceAnalyzer struct
    method analyze (line 13) | func (sa *SourceAnalyzer) analyze(filename string) (packageName string...
    method extractVariables (line 33) | func (sa *SourceAnalyzer) extractVariables(content string) []string {
    method extractInternalFunctions (line 78) | func (sa *SourceAnalyzer) extractInternalFunctions(content string) []s...

FILE: internal/extgen/srcanalyzer_test.go
  function TestSourceAnalyzer_Analyze (line 13) | func TestSourceAnalyzer_Analyze(t *testing.T) {
  function TestSourceAnalyzer_Analyze_InvalidFile (line 249) | func TestSourceAnalyzer_Analyze_InvalidFile(t *testing.T) {
  function TestSourceAnalyzer_ExtractInternalFunctions (line 273) | func TestSourceAnalyzer_ExtractInternalFunctions(t *testing.T) {
  function TestSourceAnalyzer_InternalFunctionPreservation (line 384) | func TestSourceAnalyzer_InternalFunctionPreservation(t *testing.T) {
  function TestSourceAnalyzer_VariableBlockPreservation (line 446) | func TestSourceAnalyzer_VariableBlockPreservation(t *testing.T) {
  function BenchmarkSourceAnalyzer_Analyze (line 488) | func BenchmarkSourceAnalyzer_Analyze(b *testing.B) {
  function BenchmarkSourceAnalyzer_ExtractInternalFunctions (line 527) | func BenchmarkSourceAnalyzer_ExtractInternalFunctions(b *testing.B) {

FILE: internal/extgen/stub.go
  type StubGenerator (line 13) | type StubGenerator struct
    method generate (line 17) | func (sg *StubGenerator) generate() error {
    method buildContent (line 27) | func (sg *StubGenerator) buildContent() (string, error) {
  function getPhpTypeAnnotation (line 44) | func getPhpTypeAnnotation(t phpType) string {

FILE: internal/extgen/stub_test.go
  function TestStubGenerator_Generate (line 11) | func TestStubGenerator_Generate(t *testing.T) {
  function TestStubGenerator_BuildContent (line 72) | func TestStubGenerator_BuildContent(t *testing.T) {
  function TestStubGenerator_FunctionSignatures (line 181) | func TestStubGenerator_FunctionSignatures(t *testing.T) {
  function TestStubGenerator_ClassGeneration (line 235) | func TestStubGenerator_ClassGeneration(t *testing.T) {
  function TestStubGenerator_MultipleItems (line 282) | func TestStubGenerator_MultipleItems(t *testing.T) {
  function TestStubGenerator_ErrorHandling (line 334) | func TestStubGenerator_ErrorHandling(t *testing.T) {
  function TestStubGenerator_EmptyContent (line 348) | func TestStubGenerator_EmptyContent(t *testing.T) {
  function TestStubGenerator_PHPSyntaxValidation (line 371) | func TestStubGenerator_PHPSyntaxValidation(t *testing.T) {
  function TestStubGenerator_ClassConstants (line 417) | func TestStubGenerator_ClassConstants(t *testing.T) {
  function TestStubGenerator_FileStructure (line 523) | func TestStubGenerator_FileStructure(t *testing.T) {
  function testStubBasicStructure (line 556) | func testStubBasicStructure(t *testing.T, content string) {
  function testStubFunctions (line 572) | func testStubFunctions(t *testing.T, content string, functions []phpFunc...
  function testStubClasses (line 579) | func testStubClasses(t *testing.T, content string, classes []phpClass) {
  function testStubConstants (line 591) | func testStubConstants(t *testing.T, content string, constants []phpCons...

FILE: internal/extgen/utils.go
  function writeFile (line 9) | func writeFile(filename, content string) error {
  function readFile (line 13) | func readFile(filename string) (string, error) {
  function NamespacedName (line 25) | func NamespacedName(namespace, name string) string {
  function SanitizePackageName (line 34) | func SanitizePackageName(name string) string {

FILE: internal/extgen/utils_namespace_test.go
  function TestNamespacedName (line 9) | func TestNamespacedName(t *testing.T) {

FILE: internal/extgen/utils_test.go
  function TestWriteFile (line 12) | func TestWriteFile(t *testing.T) {
  function TestReadFile (line 81) | func TestReadFile(t *testing.T) {
  function TestSanitizePackageName (line 130) | func TestSanitizePackageName(t *testing.T) {
  function BenchmarkSanitizePackageName (line 231) | func BenchmarkSanitizePackageName(b *testing.B) {

FILE: internal/extgen/validator.go
  type Validator (line 25) | type Validator struct
    method validateFunction (line 27) | func (v *Validator) validateFunction(fn phpFunction) error {
    method validateParameter (line 49) | func (v *Validator) validateParameter(param phpParameter) error {
    method validateReturnType (line 65) | func (v *Validator) validateReturnType(returnType phpType) error {
    method validateClass (line 72) | func (v *Validator) validateClass(class phpClass) error {
    method validateClassProperty (line 90) | func (v *Validator) validateClassProperty(prop phpClassProperty) error {
    method validateTypes (line 107) | func (v *Validator) validateTypes(fn phpFunction) error {
    method validateGoFunctionSignatureWithOptions (line 122) | func (v *Validator) validateGoFunctionSignatureWithOptions(phpFunc php...
    method phpTypeToGoType (line 197) | func (v *Validator) phpTypeToGoType(t phpType, isNullable bool) string {
    method isCompatibleGoType (line 226) | func (v *Validator) isCompatibleGoType(expectedType, actualType string...
    method phpReturnTypeToGoType (line 243) | func (v *Validator) phpReturnTypeToGoType(phpReturnType phpType) string {
    method goTypeToString (line 262) | func (v *Validator) goTypeToString(expr ast.Expr) string {
    method goReturnTypeToString (line 275) | func (v *Validator) goReturnTypeToString(results *ast.FieldList) string {

FILE: internal/extgen/validator_test.go
  function TestValidateFunction (line 9) | func TestValidateFunction(t *testing.T) {
  function TestValidateReturnType (line 172) | func TestValidateReturnType(t *testing.T) {
  function TestValidateClassProperty (line 239) | func TestValidateClassProperty(t *testing.T) {
  function TestValidateParameter (line 304) | func TestValidateParameter(t *testing.T) {
  function TestValidateClass (line 411) | func TestValidateClass(t *testing.T) {
  function TestValidateTypes (line 484) | func TestValidateTypes(t *testing.T) {
  function TestValidateGoFunctionSignature (line 628) | func TestValidateGoFunctionSignature(t *testing.T) {
  function TestPhpTypeToGoType (line 856) | func TestPhpTypeToGoType(t *testing.T) {
  function TestPhpReturnTypeToGoType (line 886) | func TestPhpReturnTypeToGoType(t *testing.T) {

FILE: internal/fastabs/filepath.go
  function FastAbs (line 11) | func FastAbs(path string) (string, error) {

FILE: internal/fastabs/filepath_unix.go
  function init (line 15) | func init() {
  function FastAbs (line 31) | func FastAbs(path string) (string, error) {

FILE: internal/memory/memory_linux.go
  function TotalSysMemory (line 5) | func TotalSysMemory() uint64 {

FILE: internal/memory/memory_others.go
  function TotalSysMemory (line 6) | func TotalSysMemory() uint64 {

FILE: internal/phpheaders/phpheaders.go
  function GetUnCommonHeader (line 130) | func GetUnCommonHeader(ctx context.Context, key string) string {

FILE: internal/phpheaders/phpheaders_test.go
  function TestAllCommonHeadersAreCorrect (line 10) | func TestAllCommonHeadersAreCorrect(t *testing.T) {

FILE: internal/state/state.go
  type State (line 11) | type State
    method String (line 35) | func (s State) String() string {
  constant Reserved (line 15) | Reserved State = iota
  constant Booting (line 16) | Booting
  constant BootRequested (line 17) | BootRequested
  constant ShuttingDown (line 18) | ShuttingDown
  constant Done (line 19) | Done
  constant Inactive (line 22) | Inactive
  constant Ready (line 23) | Ready
  constant Restarting (line 26) | Restarting
  constant Yielding (line 27) | Yielding
  constant TransitionRequested (line 30) | TransitionRequested
  constant TransitionInProgress (line 31) | TransitionInProgress
  constant TransitionComplete (line 32) | TransitionComplete
  type ThreadState (line 66) | type ThreadState struct
    method Is (line 87) | func (ts *ThreadState) Is(state State) bool {
    method CompareAndSwap (line 95) | func (ts *ThreadState) CompareAndSwap(compareTo State, swapTo State) b...
    method Name (line 107) | func (ts *ThreadState) Name() string {
    method Get (line 111) | func (ts *ThreadState) Get() State {
    method Set (line 119) | func (ts *ThreadState) Set(nextState State) {
    method notifySubscribers (line 126) | func (ts *ThreadState) notifySubscribers(nextState State) {
    method WaitFor (line 145) | func (ts *ThreadState) WaitFor(states ...State) {
    method RequestSafeStateChange (line 163) | func (ts *ThreadState) RequestSafeStateChange(nextState State) bool {
    method MarkAsWaiting (line 188) | func (ts *ThreadState) MarkAsWaiting(isWaiting bool) {
    method IsInWaitingState (line 197) | func (ts *ThreadState) IsInWaitingState() bool {
    method WaitTime (line 202) | func (ts *ThreadState) WaitTime() int64 {
    method SetWaitTime (line 210) | func (ts *ThreadState) SetWaitTime(t time.Time) {
  type stateSubscriber (line 74) | type stateSubscriber struct
  function NewThreadState (line 79) | func NewThreadState() *ThreadState {

FILE: internal/state/state_test.go
  function Test2GoroutinesYieldToEachOtherViaStates (line 10) | func Test2GoroutinesYieldToEachOtherViaStates(t *testing.T) {
  function TestStateShouldHaveCorrectAmountOfSubscribers (line 24) | func TestStateShouldHaveCorrectAmountOfSubscribers(t *testing.T) {
  function assertNumberOfSubscribers (line 41) | func assertNumberOfSubscribers(t *testing.T, threadState *ThreadState, e...

FILE: internal/testcli/main.go
  function main (line 10) | func main() {

FILE: internal/testext/ext_test.go
  function TestRegisterExtension (line 5) | func TestRegisterExtension(t *testing.T) {

FILE: internal/testext/exttest.go
  function testRegisterExtension (line 25) | func testRegisterExtension(t *testing.T) {

FILE: internal/testserver/main.go
  function main (line 12) | func main() {

FILE: internal/watcher/pattern.go
  constant sep (line 14) | sep = string(filepath.Separator)
  type pattern (line 16) | type pattern struct
    method startSession (line 26) | func (p *pattern) startSession() {
    method parse (line 35) | func (p *pattern) parse() (err error) {
    method allowReload (line 84) | func (p *pattern) allowReload(event *watcher.Event) bool {
    method handle (line 95) | func (p *pattern) handle(event *watcher.Event) {
    method stop (line 108) | func (p *pattern) stop() {
    method isValidPattern (line 124) | func (p *pattern) isValidPattern(fileName string) bool {
    method matchPatterns (line 145) | func (p *pattern) matchPatterns(fileName string) bool {
  function isValidEventType (line 112) | func isValidEventType(effectType watcher.EffectType) bool {
  function isValidPathType (line 116) | func isValidPathType(event *watcher.Event) bool {
  function matchCurlyBracePattern (line 187) | func matchCurlyBracePattern(pattern string, fileName string) bool {
  function expandCurlyBraces (line 198) | func expandCurlyBraces(s string) []string {
  function matchPattern (line 217) | func matchPattern(pattern string, fileName string) bool {

FILE: internal/watcher/pattern_test.go
  function normalizePath (line 15) | func normalizePath(t *testing.T, path string) string {
  function newPattern (line 30) | func newPattern(t *testing.T, value string) pattern {
  function TestDisallowOnEventTypeBiggerThan3 (line 39) | func TestDisallowOnEventTypeBiggerThan3(t *testing.T) {
  function TestDisallowOnPathTypeBiggerThan2 (line 47) | func TestDisallowOnPathTypeBiggerThan2(t *testing.T) {
  function TestWatchesCorrectDir (line 55) | func TestWatchesCorrectDir(t *testing.T) {
  function TestValidRecursiveDirectories (line 84) | func TestValidRecursiveDirectories(t *testing.T) {
  function TestInvalidRecursiveDirectories (line 112) | func TestInvalidRecursiveDirectories(t *testing.T) {
  function TestValidNonRecursiveFilePatterns (line 133) | func TestValidNonRecursiveFilePatterns(t *testing.T) {
  function TestInValidNonRecursiveFilePatterns (line 157) | func TestInValidNonRecursiveFilePatterns(t *testing.T) {
  function TestValidRecursiveFilePatterns (line 180) | func TestValidRecursiveFilePatterns(t *testing.T) {
  function TestInvalidRecursiveFilePatterns (line 205) | func TestInvalidRecursiveFilePatterns(t *testing.T) {
  function TestValidDirectoryPatterns (line 233) | func TestValidDirectoryPatterns(t *testing.T) {
  function TestInvalidDirectoryPatterns (line 260) | func TestInvalidDirectoryPatterns(t *testing.T) {
  function TestValidCurlyBracePatterns (line 290) | func TestValidCurlyBracePatterns(t *testing.T) {
  function TestInvalidCurlyBracePatterns (line 320) | func TestInvalidCurlyBracePatterns(t *testing.T) {
  function TestAnAssociatedEventTriggersTheWatcher (line 346) | func TestAnAssociatedEventTriggersTheWatcher(t *testing.T) {
  function relativeDir (line 358) | func relativeDir(t *testing.T, relativePath string) string {
  function hasDir (line 367) | func hasDir(t *testing.T, p string, dir string) {
  function assertPatternMatch (line 375) | func assertPatternMatch(t *testing.T, p, fileName string) {
  function assertPatternNotMatch (line 383) | func assertPatternNotMatch(t *testing.T, p, fileName string) {

FILE: internal/watcher/watcher.go
  constant debounceDuration (line 18) | debounceDuration = 150 * time.Millisecond
  constant maxFailureCount (line 20) | maxFailureCount      = 5
  constant failureResetDuration (line 21) | failureResetDuration = 5 * time.Second
  type PatternGroup (line 40) | type PatternGroup struct
  type eventHolder (line 45) | type eventHolder struct
  type globalWatcher (line 50) | type globalWatcher struct
    method startWatching (line 138) | func (g *globalWatcher) startWatching() error {
    method parseFilePatterns (line 156) | func (g *globalWatcher) parseFilePatterns() error {
    method stopWatching (line 166) | func (g *globalWatcher) stopWatching() {
    method listenForFileEvents (line 173) | func (g *globalWatcher) listenForFileEvents() {
    method scheduleReload (line 206) | func (g *globalWatcher) scheduleReload(eventsPerGroup map[*PatternGrou...
  function InitWatcher (line 57) | func InitWatcher(ct context.Context, slogger *slog.Logger, groups []*Pat...
  function DrainWatcher (line 89) | func DrainWatcher() {
  method retryWatching (line 106) | func (p *pattern) retryWatching() {

FILE: log_test.go
  function newTestLogger (line 11) | func newTestLogger(t *testing.T) (*slog.Logger, fmt.Stringer) {
  type syncBuffer (line 20) | type syncBuffer struct
    method Write (line 25) | func (s *syncBuffer) Write(p []byte) (n int, err error) {
    method String (line 32) | func (s *syncBuffer) String() string {

FILE: mercure-skip.go
  type mercureContext (line 9) | type mercureContext struct
  function go_mercure_publish (line 13) | func go_mercure_publish(threadIndex C.uintptr_t, topics *C.struct__zval_...
  method configureMercure (line 17) | func (w *worker) configureMercure(_ *workerOpt) {

FILE: mercure.go
  type mercureContext (line 16) | type mercureContext struct
  function go_mercure_publish (line 21) | func go_mercure_publish(threadIndex C.uintptr_t, topics *C.struct__zval_...
  method configureMercure (line 76) | func (w *worker) configureMercure(o *workerOpt) {
  function WithMercureHub (line 85) | func WithMercureHub(hub *mercure.Hub) RequestOption {
  function WithWorkerMercureHub (line 94) | func WithWorkerMercureHub(hub *mercure.Hub) WorkerOption {

FILE: mercure_test.go
  function TestMercurePublish_module (line 17) | func TestMercurePublish_module(t *testing.T) { testMercurePublish(t, &te...
  function TestMercurePublish_worker (line 18) | func TestMercurePublish_worker(t *testing.T) {
  function testMercurePublish (line 21) | func testMercurePublish(t *testing.T, opts *testOptions) {

FILE: metrics.go
  constant StopReasonCrash (line 12) | StopReasonCrash = iota
  constant StopReasonRestart (line 13) | StopReasonRestart
  constant StopReasonBootFailure (line 14) | StopReasonBootFailure
  type StopReason (line 17) | type StopReason
  type Metrics (line 19) | type Metrics interface
  type nullMetrics (line 45) | type nullMetrics struct
    method StartWorker (line 47) | func (n nullMetrics) StartWorker(string) {
    method ReadyWorker (line 50) | func (n nullMetrics) ReadyWorker(string) {
    method StopWorker (line 53) | func (n nullMetrics) StopWorker(string, StopReason) {
    method TotalWorkers (line 56) | func (n nullMetrics) TotalWorkers(string, int) {
    method TotalThreads (line 59) | func (n nullMetrics) TotalThreads(int) {
    method StartRequest (line 62) | func (n nullMetrics) StartRequest() {
    method StopRequest (line 65) | func (n nullMetrics) StopRequest() {
    method StopWorkerRequest (line 68) | func (n nullMetrics) StopWorkerRequest(string, time.Duration) {
    method StartWorkerRequest (line 71) | func (n nullMetrics) StartWorkerRequest(string) {
    method Shutdown (line 74) | func (n nullMetrics) Shutdown() {
    method QueuedWorkerRequest (line 77) | func (n nullMetrics) QueuedWorkerRequest(string) {}
    method DequeuedWorkerRequest (line 79) | func (n nullMetrics) DequeuedWorkerRequest(string) {}
    method QueuedRequest (line 81) | func (n nullMetrics) QueuedRequest()   {}
    method DequeuedRequest (line 82) | func (n nullMetrics) DequeuedRequest() {}
  type PrometheusMetrics (line 84) | type PrometheusMetrics struct
    method StartWorker (line 100) | func (m *PrometheusMetrics) StartWorker(name string) {
    method ReadyWorker (line 111) | func (m *PrometheusMetrics) ReadyWorker(name string) {
    method StopWorker (line 119) | func (m *PrometheusMetrics) StopWorker(name string, reason StopReason) {
    method TotalWorkers (line 142) | func (m *PrometheusMetrics) TotalWorkers(string, int) {
    method TotalThreads (line 248) | func (m *PrometheusMetrics) TotalThreads(num int) {
    method StartRequest (line 252) | func (m *PrometheusMetrics) StartRequest() {
    method StopRequest (line 256) | func (m *PrometheusMetrics) StopRequest() {
    method StopWorkerRequest (line 260) | func (m *PrometheusMetrics) StopWorkerRequest(name string, duration ti...
    method StartWorkerRequest (line 270) | func (m *PrometheusMetrics) StartWorkerRequest(name string) {
    method QueuedWorkerRequest (line 277) | func (m *PrometheusMetrics) QueuedWorkerRequest(name string) {
    method DequeuedWorkerRequest (line 284) | func (m *PrometheusMetrics) DequeuedWorkerRequest(name string) {
    method QueuedRequest (line 291) | func (m *PrometheusMetrics) QueuedRequest() {
    method DequeuedRequest (line 295) | func (m *PrometheusMetrics) DequeuedRequest() {
    method Shutdown (line 299) | func (m *PrometheusMetrics) Shutdown() {
  function NewPrometheusMetrics (line 373) | func NewPrometheusMetrics(registry prometheus.Registerer) *PrometheusMet...

FILE: metrics_test.go
  function createPrometheusMetrics (line 14) | func createPrometheusMetrics() *PrometheusMetrics {
  function TestPrometheusMetrics_TotalWorkers (line 24) | func TestPrometheusMetrics_TotalWorkers(t *testing.T) {
  function TestPrometheusMetrics_StopWorkerRequest (line 46) | func TestPrometheusMetrics_StopWorkerRequest(t *testing.T) {
  function TestPrometheusMetrics_StartWorkerRequest (line 100) | func TestPrometheusMetrics_StartWorkerRequest(t *testing.T) {
  function TestPrometheusMetrics_TestStopReasonCrash (line 132) | func TestPrometheusMetrics_TestStopReasonCrash(t *testing.T) {

FILE: options.go
  constant defaultMaxConsecutiveFailures (line 11) | defaultMaxConsecutiveFailures = 6
  type Option (line 14) | type Option
  type WorkerOption (line 17) | type WorkerOption
  type opt (line 22) | type opt struct
  type workerOpt (line 36) | type workerOpt struct
  function WithContext (line 55) | func WithContext(ctx context.Context) Option {
  function WithNumThreads (line 64) | func WithNumThreads(numThreads int) Option {
  function WithMaxThreads (line 72) | func WithMaxThreads(maxThreads int) Option {
  function WithMetrics (line 80) | func WithMetrics(m Metrics) Option {
  function WithWorkers (line 89) | func WithWorkers(name, fileName string, num int, options ...WorkerOption...
  function WithExtensionWorkers (line 122) | func WithExtensionWorkers(name, fileName string, numThreads int, options...
  function WithLogger (line 135) | func WithLogger(l *slog.Logger) Option {
  function WithPhpIni (line 144) | func WithPhpIni(overrides map[string]string) Option {
  function WithMaxWaitTime (line 152) | func WithMaxWaitTime(maxWaitTime time.Duration) Option {
  function WithMaxIdleTime (line 161) | func WithMaxIdleTime(maxIdleTime time.Duration) Option {
  function WithWorkerEnv (line 170) | func WithWorkerEnv(env map[string]string) WorkerOption {
  function WithWorkerRequestOptions (line 179) | func WithWorkerRequestOptions(options ...RequestOption) WorkerOption {
  function WithWorkerMaxThreads (line 188) | func WithWorkerMaxThreads(num int) WorkerOption {
  function WithWorkerWatchMode (line 197) | func WithWorkerWatchMode(watch []string) WorkerOption {
  function WithWorkerMaxFailures (line 206) | func WithWorkerMaxFailures(maxFailures int) WorkerOption {
  function WithWorkerOnReady (line 217) | func WithWorkerOnReady(f func(int)) WorkerOption {
  function WithWorkerOnShutdown (line 225) | func WithWorkerOnShutdown(f func(int)) WorkerOption {
  function WithWorkerOnServerStartup (line 234) | func WithWorkerOnServerStartup(f func()) WorkerOption {
  function WithWorkerOnServerShutdown (line 243) | func WithWorkerOnServerShutdown(f func()) WorkerOption {
  function withExtensionWorkers (line 251) | func withExtensionWorkers(w *extensionWorkers) WorkerOption {

FILE: phpmainthread.go
  type phpMainThread (line 20) | type phpMainThread struct
    method start (line 103) | func (mainThread *phpMainThread) start() error {
    method setAutomaticMaxThreads (line 152) | func (mainThread *phpMainThread) setAutomaticMaxThreads() {
  function initPHPThreads (line 37) | func initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string...
  function drainPHPThreads (line 76) | func drainPHPThreads() {
  function getInactivePHPThread (line 121) | func getInactivePHPThread() *phpThread {
  function go_frankenphp_main_thread_is_ready (line 139) | func go_frankenphp_main_thread_is_ready() {
  function go_frankenphp_shutdown_main_thread (line 171) | func go_frankenphp_shutdown_main_thread() {
  function go_get_custom_php_ini (line 176) | func go_get_custom_php_ini(disableTimeouts C.bool) *C.char {

FILE: phpmainthread_test.go
  function setupGlobals (line 20) | func setupGlobals(t *testing.T) {
  function TestStartAndStopTheMainThreadWithOneInactiveThread (line 28) | func TestStartAndStopTheMainThreadWithOneInactiveThread(t *testing.T) {
  function TestTransitionRegularThreadToWorkerThread (line 41) | func TestTransitionRegularThreadToWorkerThread(t *testing.T) {
  function TestTransitionAThreadBetween2DifferentWorkers (line 66) | func TestTransitionAThreadBetween2DifferentWorkers(t *testing.T) {
  function TestTransitionThreadsWhileDoingRequests (line 94) | func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
  function TestFinishBootingAWorkerScript (line 163) | func TestFinishBootingAWorkerScript(t *testing.T) {
  function TestReturnAnErrorIf2WorkersHaveTheSameFileName (line 186) | func TestReturnAnErrorIf2WorkersHaveTheSameFileName(t *testing.T) {
  function TestReturnAnErrorIf2ModuleWorkersHaveTheSameName (line 199) | func TestReturnAnErrorIf2ModuleWorkersHaveTheSameName(t *testing.T) {
  function getDummyWorker (line 212) | func getDummyWorker(t *testing.T, fileName string) *worker {
  function assertRequestBody (line 228) | func assertRequestBody(t *testing.T, url string, expected string) {
  function allPossibleTransitions (line 242) | func allPossibleTransitions(worker1Path string, worker2Path string) []fu...
  function TestCorrectThreadCalculation (line 258) | func TestCorrectThreadCalculation(t *testing.T) {
  function testThreadCalculation (line 304) | func testThreadCalculation(t *testing.T, expectedNumThreads int, expecte...
  function testThreadCalculationError (line 313) | func testThreadCalculationError(t *testing.T, o *opt) {

FILE: phpthread.go
  type phpThread (line 17) | type phpThread struct
    method boot (line 45) | func (thread *phpThread) boot() {
    method shutdown (line 66) | func (thread *phpThread) shutdown() {
    method setHandler (line 86) | func (thread *phpThread) setHandler(handler threadHandler) {
    method transitionToNewHandler (line 105) | func (thread *phpThread) transitionToNewHandler() string {
    method frankenPHPContext (line 113) | func (thread *phpThread) frankenPHPContext() *frankenPHPContext {
    method context (line 117) | func (thread *phpThread) context() context.Context {
    method name (line 126) | func (thread *phpThread) name() string {
    method pinString (line 136) | func (thread *phpThread) pinString(s string) *C.char {
    method pinCString (line 148) | func (thread *phpThread) pinCString(s string) *C.char {
    method updateContext (line 152) | func (*phpThread) updateContext(isWorker bool) {
  type threadHandler (line 28) | type threadHandler interface
  function newPHPThread (line 36) | func newPHPThread(threadIndex int) *phpThread {
  function go_frankenphp_before_script_execution (line 157) | func go_frankenphp_before_script_execution(threadIndex C.uintptr_t) *C.c...
  function go_frankenphp_after_script_execution (line 171) | func go_frankenphp_after_script_execution(threadIndex C.uintptr_t, exitS...
  function go_frankenphp_on_thread_shutdown (line 183) | func go_frankenphp_on_thread_shutdown(threadIndex C.uintptr_t) {

FILE: recorder_test.go
  type ResponseRecorder (line 24) | type ResponseRecorder struct
    method Header (line 73) | func (rw *ResponseRecorder) Header() http.Header {
    method writeHeader (line 89) | func (rw *ResponseRecorder) writeHeader(b []byte, str string) {
    method Write (line 113) | func (rw *ResponseRecorder) Write(buf []byte) (int, error) {
    method WriteString (line 123) | func (rw *ResponseRecorder) WriteString(str string) (int, error) {
    method WriteHeader (line 149) | func (rw *ResponseRecorder) WriteHeader(code int) {
    method Flush (line 179) | func (rw *ResponseRecorder) Flush() {
    method Result (line 201) | func (rw *ResponseRecorder) Result() *http.Response {
  function NewRecorder (line 57) | func NewRecorder() *ResponseRecorder {
  constant DefaultRemoteAddr (line 67) | DefaultRemoteAddr = "1.2.3.4"
  function checkWriteHeaderCode (line 131) | func checkWriteHeaderCode(code int) {
  function parseContentLength (line 265) | func parseContentLength(cl string) int64 {

FILE: requestoptions.go
  type RequestOption (line 17) | type RequestOption
  function WithRequestDocumentRoot (line 33) | func WithRequestDocumentRoot(documentRoot string, resolveSymlink bool) R...
  function WithRequestResolvedDocumentRoot (line 64) | func WithRequestResolvedDocumentRoot(documentRoot string) RequestOption {
  function WithRequestSplitPath (line 85) | func WithRequestSplitPath(splitPath []string) (RequestOption, error) {
  function PrepareEnv (line 117) | func PrepareEnv(env map[string]string) PreparedEnv {
  function WithRequestEnv (line 128) | func WithRequestEnv(env map[string]string) RequestOption {
  function WithRequestPreparedEnv (line 132) | func WithRequestPreparedEnv(env PreparedEnv) RequestOption {
  function WithOriginalRequest (line 140) | func WithOriginalRequest(r *http.Request) RequestOption {
  function WithRequestLogger (line 149) | func WithRequestLogger(logger *slog.Logger) RequestOption {
  function WithWorkerName (line 158) | func WithWorkerName(name string) RequestOption {

FILE: requestoptions_test.go
  function TestWithRequestSplitPath (line 10) | func TestWithRequestSplitPath(t *testing.T) {

FILE: scaling.go
  constant minStallTime (line 15) | minStallTime = 5 * time.Millisecond
  constant cpuProbeTime (line 17) | cpuProbeTime = 120 * time.Millisecond
  constant maxCpuUsageForScaling (line 19) | maxCpuUsageForScaling = 0.8
  constant downScaleCheckTime (line 21) | downScaleCheckTime = 5 * time.Second
  constant maxTerminationCount (line 23) | maxTerminationCount = 10
  constant defaultMaxIdleTime (line 25) | defaultMaxIdleTime = 5 * time.Second
  function initAutoScaling (line 37) | func initAutoScaling(mainThread *phpMainThread) {
  function drainAutoScaling (line 53) | func drainAutoScaling() {
  function addRegularThread (line 63) | func addRegularThread() (*phpThread, error) {
  function addWorkerThread (line 73) | func addWorkerThread(worker *worker) (*phpThread, error) {
  function scaleWorkerThread (line 84) | func scaleWorkerThread(worker *worker) {
  function scaleRegularThread (line 114) | func scaleRegularThread() {
  function startUpscalingThreads (line 143) | func startUpscalingThreads(maxScaledThreads int, scale chan *frankenPHPC...
  function startDownScalingThreads (line 194) | func startDownScalingThreads(done chan struct{}) {
  function deactivateThreads (line 206) | func deactivateThreads() {

FILE: scaling_test.go
  function TestScaleARegularThreadUpAndDown (line 12) | func TestScaleARegularThreadUpAndDown(t *testing.T) {
  function TestScaleAWorkerThreadUpAndDown (line 33) | func TestScaleAWorkerThreadUpAndDown(t *testing.T) {
  function TestMaxIdleTimePreventsEarlyDeactivation (line 60) | func TestMaxIdleTimePreventsEarlyDeactivation(t *testing.T) {
  function setLongWaitTime (line 86) | func setLongWaitTime(t *testing.T, thread *phpThread) {

FILE: testdata/autoloader-require.php
  function my_autoloader (line 3) | function my_autoloader(string $class) {

FILE: testdata/dd.php
  class Dumper (line 5) | class Dumper
    method dump (line 9) | public function dump(string $message): void
    method __destruct (line 15) | public function __destruct()

FILE: testdata/integration/basic_function.go
  function test_uppercase (line 13) | func test_uppercase(s *C.zend_string) unsafe.Pointer {
  function test_add_numbers (line 20) | func test_add_numbers(a int64, b int64) int64 {
  function test_multiply (line 25) | func test_multiply(a float64, b float64) float64 {
  function test_is_enabled (line 30) | func test_is_enabled(flag bool) bool {

FILE: testdata/integration/callable.go
  function my_array_map (line 12) | func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
  function my_filter (line 28) | func my_filter(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
  type Processor (line 50) | type Processor struct
    method Transform (line 53) | func (p *Processor) Transform(input *C.zend_string, callback *C.zval) ...

FILE: testdata/integration/class_methods.go
  type CounterStruct (line 12) | type CounterStruct struct
    method Increment (line 17) | func (c *CounterStruct) Increment() {
    method Decrement (line 22) | func (c *CounterStruct) Decrement() {
    method GetValue (line 27) | func (c *CounterStruct) GetValue() int64 {
    method SetValue (line 32) | func (c *CounterStruct) SetValue(value int64) {
    method Reset (line 37) | func (c *CounterStruct) Reset() {
    method AddValue (line 42) | func (c *CounterStruct) AddValue(amount int64) int64 {
    method UpdateWithNullable (line 48) | func (c *CounterStruct) UpdateWithNullable(newValue *int64) {
  type StringHolderStruct (line 55) | type StringHolderStruct struct
    method SetData (line 60) | func (sh *StringHolderStruct) SetData(data *C.zend_string) {
    method GetData (line 65) | func (sh *StringHolderStruct) GetData() unsafe.Pointer {
    method GetLength (line 70) | func (sh *StringHolderStruct) GetLength() int64 {

FILE: testdata/integration/constants.go
  constant TEST_MAX_RETRIES (line 12) | TEST_MAX_RETRIES = 100
  constant TEST_API_VERSION (line 15) | TEST_API_VERSION = "2.0.0"
  constant TEST_ENABLED (line 18) | TEST_ENABLED = true
  constant TEST_PI (line 21) | TEST_PI = 3.14159
  constant STATUS_PENDING (line 25) | STATUS_PENDING = iota
  constant STATUS_PROCESSING (line 26) | STATUS_PROCESSING
  constant STATUS_COMPLETED (line 27) | STATUS_COMPLETED
  constant ONE (line 32) | ONE = 1
  constant TWO (line 34) | TWO = 2
  type ConfigStruct (line 38) | type ConfigStruct struct
    method SetMode (line 52) | func (c *ConfigStruct) SetMode(mode int64) {
    method GetMode (line 57) | func (c *ConfigStruct) GetMode() int64 {
  constant MODE_DEBUG (line 43) | MODE_DEBUG = 1
  constant MODE_PRODUCTION (line 46) | MODE_PRODUCTION = 2
  constant DEFAULT_TIMEOUT (line 49) | DEFAULT_TIMEOUT = 30
  function test_with_constants (line 62) | func test_with_constants(status int64) unsafe.Pointer {

FILE: testdata/integration/invalid_signature.go
  function invalid_return_type (line 7) | func invalid_return_type(s *C.zend_string) int {

FILE: testdata/integration/namespace.go
  constant NAMESPACE_VERSION (line 14) | NAMESPACE_VERSION = "1.0.0"
  function greet (line 17) | func greet(name *C.zend_string) unsafe.Pointer {
  type PersonStruct (line 24) | type PersonStruct struct
    method SetName (line 30) | func (p *PersonStruct) SetName(name *C.zend_string) {
    method GetName (line 35) | func (p *PersonStruct) GetName() unsafe.Pointer {
    method SetAge (line 40) | func (p *PersonStruct) SetAge(age int64) {
    method GetAge (line 45) | func (p *PersonStruct) GetAge() int64 {
  constant DEFAULT_AGE (line 50) | DEFAULT_AGE = 18

FILE: testdata/integration/type_mismatch.go
  function mismatched_param_type (line 7) | func mismatched_param_type(value string) int64 {
  type BadClassStruct (line 12) | type BadClassStruct struct
    method WrongReturnType (line 17) | func (bc *BadClassStruct) WrongReturnType() int {

FILE: testdata/persistent-object-require.php
  class MyObject (line 3) | class MyObject
    method __construct (line 5) | public function __construct(public string $id) {}

FILE: testdata/preload.php
  function preloaded_function (line 5) | function preloaded_function(): string

FILE: testdata/session-handler.php
  class TestSessionHandler (line 6) | class TestSessionHandler implements SessionHandlerInterface
    method open (line 10) | public function open(string $path, string $name): bool
    method close (line 15) | public function close(): bool
    method read (line 20) | public function read(string $id): string|false
    method write (line 25) | public function write(string $id, string $data): bool
    method destroy (line 31) | public function destroy(string $id): bool
    method gc (line 37) | public function gc(int $max_lifetime): int|false

FILE: testdata/worker-with-session-handler.php
  class PreLoopSessionHandler (line 4) | class PreLoopSessionHandler implements SessionHandlerInterface
    method open (line 8) | public function open(string $path, string $name): bool
    method close (line 13) | public function close(): bool
    method read (line 18) | public function read(string $id): string|false
    method write (line 23) | public function write(string $id, string $data): bool
    method destroy (line 29) | public function destroy(string $id): bool
    method gc (line 35) | public function gc(int $max_lifetime): int|false

FILE: threadinactive.go
  type inactiveThread (line 13) | type inactiveThread struct
    method beforeScriptExecution (line 21) | func (handler *inactiveThread) beforeScriptExecution() string {
    method afterScriptExecution (line 46) | func (handler *inactiveThread) afterScriptExecution(int) {
    method frankenPHPContext (line 50) | func (handler *inactiveThread) frankenPHPContext() *frankenPHPContext {
    method context (line 54) | func (handler *inactiveThread) context() context.Context {
    method name (line 58) | func (handler *inactiveThread) name() string {
  function convertToInactiveThread (line 17) | func convertToInactiveThread(thread *phpThread) {

FILE: threadregular.go
  type regularThread (line 15) | type regularThread struct
    method beforeScriptExecution (line 38) | func (handler *regularThread) beforeScriptExecution() string {
    method afterScriptExecution (line 62) | func (handler *regularThread) afterScriptExecution(_ int) {
    method frankenPHPContext (line 66) | func (handler *regularThread) frankenPHPContext() *frankenPHPContext {
    method context (line 70) | func (handler *regularThread) context() context.Context {
    method name (line 74) | func (handler *regularThread) name() string {
    method waitForRequest (line 78) | func (handler *regularThread) waitForRequest() string {
    method afterRequest (line 99) | func (handler *regularThread) afterRequest() {
  function convertToRegularThread (line 29) | func convertToRegularThread(thread *phpThread) {
  function handleRequestWithRegularPHPThreads (line 105) | func handleRequestWithRegularPHPThreads(ch contextHolder) error {
  function attachRegularThread (line 156) | func attachRegularThread(thread *phpThread) {
  function detachRegularThread (line 162) | func detachRegularThread(thread *phpThread) {

FILE: threadtasks_test.go
  type taskThread (line 12) | type taskThread struct
    method beforeScriptExecution (line 44) | func (handler *taskThread) beforeScriptExecution() string {
    method afterScriptExecution (line 66) | func (handler *taskThread) afterScriptExecution(_ int) {
    method frankenPHPContext (line 70) | func (handler *taskThread) frankenPHPContext() *frankenPHPContext {
    method context (line 74) | func (handler *taskThread) context() context.Context {
    method name (line 78) | func (handler *taskThread) name() string {
    method waitForTasks (line 82) | func (handler *taskThread) waitForTasks() {
    method execute (line 95) | func (handler *taskThread) execute(t *task) {
  type task (line 19) | type task struct
    method waitForCompletion (line 31) | func (t *task) waitForCompletion() {
  function newTask (line 24) | func newTask(cb func()) *task {
  function convertToTaskThread (line 35) | func convertToTaskThread(thread *phpThread) *taskThread {

FILE: threadworker.go
  type workerThread (line 19) | type workerThread struct
    method beforeScriptExecution (line 41) | func (handler *workerThread) beforeScriptExecution() string {
    method afterScriptExecution (line 78) | func (handler *workerThread) afterScriptExecution(exitStatus int) {
    method frankenPHPContext (line 82) | func (handler *workerThread) frankenPHPContext() *frankenPHPContext {
    method context (line 89) | func (handler *workerThread) context() context.Context {
    method name (line 97) | func (handler *workerThread) name() string {
    method waitForWorkerRequest (line 194) | func (handler *workerThread) waitForWorkerRequest() (bool, any) {
  function convertToWorkerThread (line 31) | func convertToWorkerThread(thread *phpThread, worker *worker) {
  function setupWorkerScript (line 101) | func setupWorkerScript(handler *workerThread, worker *worker) {
  function tearDownWorkerScript (line 125) | func tearDownWorkerScript(handler *workerThread, exitStatus int) {
  function go_frankenphp_worker_handle_request_start (line 256) | func go_frankenphp_worker_handle_request_start(threadIndex C.uintptr_t) ...
  function go_frankenphp_finish_worker_request (line 281) | func go_frankenphp_finish_worker_request(threadIndex C.uintptr_t, retval...
  function go_frankenphp_finish_php_request (line 311) | func go_frankenphp_finish_php_request(threadIndex C.uintptr_t) {

FILE: types.c
  function zval (line 3) | zval *get_ht_packed_data(HashTable *ht, uint32_t index) {
  function Bucket (line 10) | Bucket *get_ht_bucket_data(HashTable *ht, uint32_t index) {
  function __efree__ (line 19) | void __efree__(void *ptr) { free(ptr); }
  function __zend_hash_init__ (line 21) | void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDest...
  function __hash_update_string__ (line 26) | void __hash_update_string__(zend_array *ht, zend_string *k, zend_string ...
  function __zval_null__ (line 32) | void __zval_null__(zval *zv) { ZVAL_NULL(zv); }
  function __zval_bool__ (line 34) | void __zval_bool__(zval *zv, bool val) { ZVAL_BOOL(zv, val); }
  function __zval_long__ (line 36) | void __zval_long__(zval *zv, zend_long val) { ZVAL_LONG(zv, val); }
  function __zval_double__ (line 38) | void __zval_double__(zval *zv, double val) { ZVAL_DOUBLE(zv, val); }
  function __zval_string__ (line 40) | void __zval_string__(zval *zv, zend_string *str) { ZVAL_STR(zv, str); }
  function __zval_empty_string__ (line 42) | void __zval_empty_string__(zval *zv) { ZVAL_EMPTY_STRING(zv); }
  function __zval_arr__ (line 44) | void __zval_arr__(zval *zv, zend_array *arr) { ZVAL_ARR(zv, arr); }
  function zend_array (line 46) | zend_array *__zend_new_array__(uint32_t size) { return zend_new_array(si...
  function __zend_is_callable__ (line 48) | int __zend_is_callable__(zval *cb) { return zend_is_callable(cb, 0, NULL...
  function __call_user_function__ (line 50) | int __call_user_function__(zval *function_name, zval *retval,

FILE: types.go
  type toZval (line 31) | type toZval interface
  function GoString (line 36) | func GoString(s unsafe.Pointer) string {
  function PHPString (line 49) | func PHPString(s string, persistent bool) unsafe.Pointer {
  type AssociativeArray (line 64) | type AssociativeArray struct
  method toZval (line 69) | func (a AssociativeArray[T]) toZval(zval *C.zval) {
  function GoAssociativeArray (line 74) | func GoAssociativeArray[T any](arr unsafe.Pointer) (AssociativeArray[T],...
  function GoMap (line 81) | func GoMap[T any](arr unsafe.Pointer) (map[string]T, error) {
  function goArray (line 87) | func goArray[T any](arr unsafe.Pointer, ordered bool) (map[string]T, []s...
  function GoPackedArray (line 168) | func GoPackedArray[T any](arr unsafe.Pointer) ([]T, error) {
  function PHPMap (line 215) | func PHPMap[T any](arr map[string]T) unsafe.Pointer {
  function PHPAssociativeArray (line 220) | func PHPAssociativeArray[T any](arr AssociativeArray[T]) unsafe.Pointer {
  function phpArray (line 224) | func phpArray[T any](entries map[string]T, order []string) unsafe.Pointer {
  function PHPPackedArray (line 248) | func PHPPackedArray[T any](slice []T) unsafe.Pointer {
  function GoValue (line 265) | func GoValue[T any](zval unsafe.Pointer) (T, error) {
  function goValue (line 269) | func goValue[T any](zval *C.zval) (res T, err error) {
  function PHPValue (line 371) | func PHPValue(value any) unsafe.Pointer {
  function phpValue (line 375) | func phpValue(value any) *C.zval {
  function createNewArray (line 416) | func createNewArray(size uint32) *C.zend_array {
  function IsPacked (line 423) | func IsPacked(arr unsafe.Pointer) bool {
  function htIsPacked (line 432) | func htIsPacked(ht *C.zend_array) bool {
  function extractZvalValue (line 439) | func extractZvalValue(zval *C.zval, expectedType C.uint8_t) (unsafe.Poin...
  function zendStringRelease (line 466) | func zendStringRelease(p unsafe.Pointer) {
  function zendHashDestroy (line 471) | func zendHashDestroy(p unsafe.Pointer) {
  function CallPHPCallable (line 478) | func CallPHPCallable(cb unsafe.Pointer, params []interface{}) interface{} {

FILE: types_test.go
  function testOnDummyPHPThread (line 13) | func testOnDummyPHPThread(t *testing.T, test func()) {
  function TestGoString (line 28) | func TestGoString(t *testing.T) {
  function TestPHPMap (line 39) | func TestPHPMap(t *testing.T) {
  function TestOrderedPHPAssociativeArray (line 55) | func TestOrderedPHPAssociativeArray(t *testing.T) {
  function TestPHPPackedArray (line 74) | func TestPHPPackedArray(t *testing.T) {
  function TestPHPPackedArrayToGoMap (line 87) | func TestPHPPackedArrayToGoMap(t *testing.T) {
  function TestPHPAssociativeArrayToPacked (line 104) | func TestPHPAssociativeArrayToPacked(t *testing.T) {
  function TestNestedMixedArray (line 124) | func TestNestedMixedArray(t *testing.T) {

FILE: watcher-skip.go
  type hotReloadOpt (line 7) | type hotReloadOpt struct
  function initWatchers (line 12) | func initWatchers(o *opt) error {
  function drainWatchers (line 22) | func drainWatchers() {

FILE: watcher.go
  type hotReloadOpt (line 12) | type hotReloadOpt struct
  function initWatchers (line 18) | func initWatchers(o *opt) error {
  function drainWatchers (line 45) | func drainWatchers() {

FILE: watcher_test.go
  constant pollingTime (line 18) | pollingTime = 250
  constant minTimesToPollForChanges (line 21) | minTimesToPollForChanges = 3
  constant maxTimesToPollForChanges (line 24) | maxTimesToPollForChanges = 60
  function TestWorkersShouldReloadOnMatchingPattern (line 26) | func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) {
  function TestWorkersShouldNotReloadOnExcludingPattern (line 35) | func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) {
  function pollForWorkerReset (line 44) | func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, ...
  function updateTestFile (line 64) | func updateTestFile(t *testing.T, fileName, content string) {

FILE: worker.go
  type worker (line 20) | type worker struct
    method attachThread (line 224) | func (worker *worker) attachThread(thread *phpThread) {
    method detachThread (line 230) | func (worker *worker) detachThread(thread *phpThread) {
    method countThreads (line 241) | func (worker *worker) countThreads() int {
    method isAtThreadLimit (line 250) | func (worker *worker) isAtThreadLimit() bool {
    method handleRequest (line 262) | func (worker *worker) handleRequest(ch contextHolder) error {
  function initWorkers (line 46) | func initWorkers(opt []workerOpt) error {
  function newWorker (line 101) | func newWorker(o workerOpt) (*worker, error) {
  function DrainWorkers (line 169) | func DrainWorkers() {
  function drainWorkerThreads (line 173) | func drainWorkerThreads() []*phpThread {
  function RestartWorkers (line 211) | func RestartWorkers() {

FILE: worker_test.go
  function TestWorker (line 19) | func TestWorker(t *testing.T) {
  function TestWorkerDie (line 46) | func TestWorkerDie(t *testing.T) {
  function TestNonWorkerModeAlwaysWorks (line 54) | func TestNonWorkerModeAlwaysWorks(t *testing.T) {
  function TestCannotCallHandleRequestInNonWorkerMode (line 67) | func TestCannotCallHandleRequestInNonWorkerMode(t *testing.T) {
  function TestWorkerEnv (line 80) | func TestWorkerEnv(t *testing.T) {
  function TestWorkerGetOpt (line 93) | func TestWorkerGetOpt(t *testing.T) {
  function ExampleServeHTTP_workers (line 113) | func ExampleServeHTTP_workers() {
  function TestWorkerHasOSEnvironmentVariableInSERVER (line 143) | func TestWorkerHasOSEnvironmentVariableInSERVER(t *testing.T) {
  function TestKeepRunningOnConnectionAbort (line 157) | func TestKeepRunningOnConnectionAbort(t *testing.T) {

FILE: workerextension.go
  type Workers (line 9) | type Workers interface
  type extensionWorkers (line 19) | type extensionWorkers struct
    method SendRequest (line 28) | func (w *extensionWorkers) SendRequest(rw http.ResponseWriter, r *http...
    method NumThreads (line 42) | func (w *extensionWorkers) NumThreads() int {
    method SendMessage (line 47) | func (w *extensionWorkers) SendMessage(ctx context.Context, message an...

FILE: workerextension_test.go
  function TestWorkersExtension (line 12) | func TestWorkersExtension(t *testing.T) {
  function TestWorkerExtensionSendMessage (line 67) | func TestWorkerExtensionSendMessage(t *testing.T) {
  function TestErrorIf2WorkersHaveSameName (line 80) | func TestErrorIf2WorkersHaveSameName(t *testing.T) {
Condensed preview — 457 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,297K chars).
[
  {
    "path": ".clang-format-ignore",
    "chars": 21,
    "preview": "frankenphp_arginfo.h\n"
  },
  {
    "path": ".codespellrc",
    "chars": 107,
    "preview": "[codespell]\ncheck-hidden =\nskip = .git,docs/*/*,docs,*/go.mod,*/go.sum,./internal/phpheaders/phpheaders.go\n"
  },
  {
    "path": ".dockerignore",
    "chars": 176,
    "preview": "/caddy/frankenphp/frankenphp\n/internal/testserver/testserver\n/internal/testcli/testcli\n/dist\n.DS_Store\n.idea/\n.vscode/\n_"
  },
  {
    "path": ".editorconfig",
    "chars": 167,
    "preview": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\n\n[*.{sh,Dockerfile}]\nindent_style = tab\ntab_width = 4\n\n[*."
  },
  {
    "path": ".gitattributes",
    "chars": 19,
    "preview": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "chars": 2745,
    "preview": "---\nname: Bug Report\ndescription: File a bug report\nlabels: [bug]\nbody:\n  - type: markdown\n    attributes:\n      value: "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
    "chars": 672,
    "preview": "---\nname: Feature Request\ndescription: Suggest an idea for this project\nlabels: [enhancement]\nbody:\n  - type: textarea\n "
  },
  {
    "path": ".github/actions/watcher/action.yaml",
    "chars": 1264,
    "preview": "name: watcher\ndescription: Install e-dant/watcher\nruns:\n  using: composite\n  steps:\n    - name: Determine e-dant/watcher"
  },
  {
    "path": ".github/dependabot.yaml",
    "chars": 715,
    "preview": "---\nversion: 2\nupdates:\n  - package-ecosystem: gomod\n    directory: /\n    schedule:\n      interval: weekly\n    commit-me"
  },
  {
    "path": ".github/scripts/docker-compute-fingerprints.sh",
    "chars": 2493,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\nwrite_output() {\n\tif [[ -n \"${GITHUB_OUTPUT:-}\" ]]; then\n\t\techo \"$1\" >>\"${GITHUB_"
  },
  {
    "path": ".github/scripts/docker-verify-fingerprints.sh",
    "chars": 2803,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\nPHP_VERSION=\"${PHP_VERSION:-}\"\nGO_VERSION=\"${GO_VERSION:-}\"\nUSE_LATEST_PHP=\"${USE"
  },
  {
    "path": ".github/workflows/docker.yaml",
    "chars": 11724,
    "preview": "---\nname: Build Docker Images\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }}\n"
  },
  {
    "path": ".github/workflows/docs.yaml",
    "chars": 551,
    "preview": "---\nname: Deploy Docs\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"docs/**\"\n      - \"README.md\"\n      - \"C"
  },
  {
    "path": ".github/workflows/lint.yaml",
    "chars": 1572,
    "preview": "---\nname: Lint Code Base\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }}\non:\n "
  },
  {
    "path": ".github/workflows/sanitizers.yaml",
    "chars": 4295,
    "preview": "---\nname: Sanitizers\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }}\non:\n  pul"
  },
  {
    "path": ".github/workflows/static.yaml",
    "chars": 23161,
    "preview": "---\nname: Build binary releases\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }"
  },
  {
    "path": ".github/workflows/tests.yaml",
    "chars": 5649,
    "preview": "---\nname: Tests\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }}\non:\n  pull_req"
  },
  {
    "path": ".github/workflows/translate.yaml",
    "chars": 2792,
    "preview": "name: Translate Docs\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref }}\non:\n  pus"
  },
  {
    "path": ".github/workflows/windows.yaml",
    "chars": 8875,
    "preview": "---\nname: Build Windows release\n\nconcurrency:\n  cancel-in-progress: true\n  group: ${{ github.workflow }}-${{ github.ref "
  },
  {
    "path": ".github/workflows/wrap-issue-details.yaml",
    "chars": 1318,
    "preview": "name: Wrap Issue Content\non:\n  issues:\n    types: [opened, edited]\n\npermissions:\n  contents: read\n\njobs:\n  wrap_content:"
  },
  {
    "path": ".gitignore",
    "chars": 301,
    "preview": "/caddy/frankenphp/Build\n/caddy/frankenphp/frankenphp\n/caddy/frankenphp/frankenphp.exe\n/dist\n/github_conf\n/internal/tests"
  },
  {
    "path": ".gitleaksignore",
    "chars": 82,
    "preview": "/github/workspace/docs/mercure.md:jwt:88\n/github/workspace/docs/mercure.md:jwt:90\n"
  },
  {
    "path": ".golangci.yaml",
    "chars": 77,
    "preview": "---\nversion: \"2\"\nrun:\n  build-tags:\n    - nobadger\n    - nomysql\n    - nopgx\n"
  },
  {
    "path": ".hadolint.yaml",
    "chars": 57,
    "preview": "---\nignored:\n  - DL3006\n  - DL3008\n  - DL3018\n  - DL3022\n"
  },
  {
    "path": ".markdown-lint.yaml",
    "chars": 56,
    "preview": "---\nMD010: false\nMD013: false\nMD033: false\nMD060: false\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 13318,
    "preview": "# Contributing\n\n## Compiling PHP\n\n### With Docker (Linux)\n\nBuild the dev Docker image:\n\n```console\ndocker build -t frank"
  },
  {
    "path": "Dockerfile",
    "chars": 4503,
    "preview": "# syntax=docker/dockerfile:1\n#checkov:skip=CKV_DOCKER_2\n#checkov:skip=CKV_DOCKER_3\n#checkov:skip=CKV_DOCKER_7\nFROM php-b"
  },
  {
    "path": "LICENSE",
    "chars": 1082,
    "preview": "The MIT license\n\nCopyright (c) 2022-present Kévin Dunglas\n\nPermission is hereby granted, free of charge, to any person o"
  },
  {
    "path": "README.md",
    "chars": 6967,
    "preview": "# FrankenPHP: Modern App Server for PHP\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev\"><img src=\"frankenphp.png\" a"
  },
  {
    "path": "SECURITY.md",
    "chars": 911,
    "preview": "# Security Policy\n\n## Supported Versions\n\nOnly the latest version is supported.\nPlease ensure that you're always using t"
  },
  {
    "path": "alpine.Dockerfile",
    "chars": 4351,
    "preview": "# syntax=docker/dockerfile:1\n#checkov:skip=CKV_DOCKER_2\n#checkov:skip=CKV_DOCKER_3\n#checkov:skip=CKV_DOCKER_7\nFROM php-b"
  },
  {
    "path": "app_checksum.txt",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "build-static.sh",
    "chars": 7888,
    "preview": "#!/bin/bash\n\nset -o errexit\nset -x\n\nif ! type \"git\" >/dev/null 2>&1; then\n\techo \"The \\\"git\\\" command must be installed.\""
  },
  {
    "path": "caddy/admin.go",
    "chars": 1794,
    "preview": "package caddy\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.com/dunglas/fra"
  },
  {
    "path": "caddy/admin_test.go",
    "chars": 7769,
    "preview": "package caddy_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github."
  },
  {
    "path": "caddy/app.go",
    "chars": 8864,
    "preview": "package caddy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n"
  },
  {
    "path": "caddy/br-skip.go",
    "chars": 55,
    "preview": "//go:build nobrotli\n\npackage caddy\n\nvar brotli = false\n"
  },
  {
    "path": "caddy/br.go",
    "chars": 55,
    "preview": "//go:build !nobrotli\n\npackage caddy\n\nvar brotli = true\n"
  },
  {
    "path": "caddy/caddy.go",
    "chars": 1191,
    "preview": "// Package caddy provides a PHP module for the Caddy web server.\n// FrankenPHP embeds the PHP interpreter directly in Ca"
  },
  {
    "path": "caddy/caddy_test.go",
    "chars": 43308,
    "preview": "package caddy_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/ato"
  },
  {
    "path": "caddy/config_test.go",
    "chars": 7440,
    "preview": "package caddy\n\nimport (\n\t\"testing\"\n\n\t\"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile\"\n\t\"github.com/stretchr/testi"
  },
  {
    "path": "caddy/extinit.go",
    "chars": 1341,
    "preview": "package caddy\n\nimport (\n\t\"errors\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/dunglas/frankenphp/internal/ext"
  },
  {
    "path": "caddy/frankenphp/Caddyfile",
    "chars": 1387,
    "preview": "# The Caddyfile is an easy way to configure FrankenPHP and the Caddy web server.\n#\n# https://frankenphp.dev/docs/config\n"
  },
  {
    "path": "caddy/frankenphp/cbrotli.go",
    "chars": 80,
    "preview": "//go:build !nobrotli\n\npackage main\n\nimport _ \"github.com/dunglas/caddy-cbrotli\"\n"
  },
  {
    "path": "caddy/frankenphp/main.go",
    "chars": 311,
    "preview": "package main\n\nimport (\n\tcaddycmd \"github.com/caddyserver/caddy/v2/cmd\"\n\n\t// plug in Caddy modules here.\n\t_ \"github.com/c"
  },
  {
    "path": "caddy/go.mod",
    "chars": 11544,
    "preview": "module github.com/dunglas/frankenphp/caddy\n\ngo 1.26.0\n\nreplace github.com/dunglas/frankenphp => ../\n\nretract v1.0.0-rc.1"
  },
  {
    "path": "caddy/go.sum",
    "chars": 62350,
    "preview": "cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ"
  },
  {
    "path": "caddy/hotreload-skip.go",
    "chars": 381,
    "preview": "//go:build nowatcher || nomercure\n\npackage caddy\n\nimport (\n\t\"errors\"\n\n\t\"github.com/caddyserver/caddy/v2/caddyconfig/cadd"
  },
  {
    "path": "caddy/hotreload.go",
    "chars": 2351,
    "preview": "//go:build !nowatcher && !nomercure\n\npackage caddy\n\nimport (\n\t\"bytes\"\n\t\"encoding/gob\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash/fnv\"\n\t\"net"
  },
  {
    "path": "caddy/hotreload_test.go",
    "chars": 1677,
    "preview": "//go:build !nowatcher && !nomercure\n\npackage caddy_test\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepat"
  },
  {
    "path": "caddy/mercure-skip.go",
    "chars": 321,
    "preview": "//go:build nomercure\n\npackage caddy\n\nimport (\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.com/caddyserver/caddy/v2/modul"
  },
  {
    "path": "caddy/mercure.go",
    "chars": 1869,
    "preview": "//go:build !nomercure\n\npackage caddy\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.co"
  },
  {
    "path": "caddy/module.go",
    "chars": 19743,
    "preview": "package caddy\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n"
  },
  {
    "path": "caddy/module_test.go",
    "chars": 1716,
    "preview": "package caddy_test\n\nimport (\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/caddyserver/caddy/v2/caddytest\"\n)\n\nfu"
  },
  {
    "path": "caddy/php-cli.go",
    "chars": 1042,
    "preview": "package caddy\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\n\tcaddycmd \"github.com/caddyserver/caddy/v2/cmd\"\n\t\"github.com/d"
  },
  {
    "path": "caddy/php-server.go",
    "chars": 9510,
    "preview": "package caddy\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.co"
  },
  {
    "path": "caddy/watcher_test.go",
    "chars": 715,
    "preview": "//go:build !nowatcher\n\npackage caddy_test\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/caddyserver/caddy/v2/caddytest\""
  },
  {
    "path": "caddy/workerconfig.go",
    "chars": 5405,
    "preview": "package caddy\n\nimport (\n\t\"net/http\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"github.com/caddyserver/caddy/v2\"\n\t\"github.com"
  },
  {
    "path": "cgi.go",
    "chars": 12748,
    "preview": "package frankenphp\n\n// #cgo nocallback frankenphp_register_server_vars\n// #cgo nocallback frankenphp_register_variable_s"
  },
  {
    "path": "cgi_test.go",
    "chars": 5963,
    "preview": "package frankenphp\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestEnsureLeadingSlash"
  },
  {
    "path": "cgo.go",
    "chars": 487,
    "preview": "package frankenphp\n\n// #cgo darwin pkg-config: libxml-2.0\n// #cgo unix CFLAGS: -Wall -Werror\n// #cgo linux CFLAGS: -D_GN"
  },
  {
    "path": "cli.go",
    "chars": 820,
    "preview": "package frankenphp\n\n// #include \"frankenphp.h\"\nimport \"C\"\nimport \"unsafe\"\n\n// ExecuteScriptCLI executes the PHP script p"
  },
  {
    "path": "cli_test.go",
    "chars": 1444,
    "preview": "package frankenphp_test\n\nimport (\n\t\"errors\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"testing\"\n\n\t\"github.com/dunglas/frankenphp\"\n\t\"githu"
  },
  {
    "path": "context.go",
    "chars": 4085,
    "preview": "package frankenphp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n"
  },
  {
    "path": "debugstate.go",
    "chars": 1608,
    "preview": "package frankenphp\n\nimport (\n\t\"github.com/dunglas/frankenphp/internal/state\"\n)\n\n// EXPERIMENTAL: ThreadDebugState prints"
  },
  {
    "path": "dev-alpine.Dockerfile",
    "chars": 1909,
    "preview": "# syntax=docker/dockerfile:1\n#checkov:skip=CKV_DOCKER_2\n#checkov:skip=CKV_DOCKER_3\nFROM golang:1.26-alpine\n\nENV GOTOOLCH"
  },
  {
    "path": "dev.Dockerfile",
    "chars": 2111,
    "preview": "# syntax=docker/dockerfile:1\n#checkov:skip=CKV_DOCKER_2\n#checkov:skip=CKV_DOCKER_3\nFROM golang:1.26\n\nENV GOTOOLCHAIN=loc"
  },
  {
    "path": "docker-bake.hcl",
    "chars": 6280,
    "preview": "variable \"IMAGE_NAME\" {\n    default = \"dunglas/frankenphp\"\n}\n\nvariable \"VERSION\" {\n    default = \"dev\"\n}\n\nvariable \"PHP_"
  },
  {
    "path": "docs/classic.md",
    "chars": 1328,
    "preview": "# Using Classic Mode\n\nWithout any additional configuration, FrankenPHP operates in classic mode. In this mode, FrankenPH"
  },
  {
    "path": "docs/cn/CONTRIBUTING.md",
    "chars": 4945,
    "preview": "# 贡献\n\n## 编译 PHP\n\n### 使用 Docker (Linux)\n\n构建开发环境 Docker 镜像:\n\n```console\ndocker build -t frankenphp-dev -f dev.Dockerfile ."
  },
  {
    "path": "docs/cn/README.md",
    "chars": 4057,
    "preview": "# FrankenPHP: 适用于 PHP 的现代应用服务器\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev\"><img src=\"../../frankenphp.png\" alt="
  },
  {
    "path": "docs/cn/classic.md",
    "chars": 635,
    "preview": "# 使用经典模式\n\n在没有任何额外配置的情况下,FrankenPHP 以经典模式运行。在此模式下,FrankenPHP 的功能类似于传统的 PHP 服务器,直接提供 PHP 文件服务。这使其成为 PHP-FPM 或 Apache with "
  },
  {
    "path": "docs/cn/compile.md",
    "chars": 3219,
    "preview": "# 从源代码编译\n\n本文档解释了如何创建一个 FrankenPHP 构建,它将 PHP 加载为一个动态库。\n这是推荐的方法。\n\n或者,你也可以 [编译静态版本](static.md)。\n\n## 安装 PHP\n\nFrankenPHP 支持 P"
  },
  {
    "path": "docs/cn/config.md",
    "chars": 9304,
    "preview": "# 配置\n\nFrankenPHP、Caddy 以及 [Mercure](mercure.md) 和 [Vulcain](https://vulcain.rocks) 模块可以使用 [Caddy 支持的格式](https://caddyser"
  },
  {
    "path": "docs/cn/docker.md",
    "chars": 6977,
    "preview": "# 构建自定义 Docker 镜像\n\n[FrankenPHP Docker 镜像](https://hub.docker.com/r/dunglas/frankenphp) 基于 [官方 PHP 镜像](https://hub.docker"
  },
  {
    "path": "docs/cn/early-hints.md",
    "chars": 376,
    "preview": "# 早期提示\n\nFrankenPHP 原生支持 [103 Early Hints 状态码](https://developer.chrome.com/blog/early-hints/)。\n使用早期提示可以将网页的加载时间缩短 30%。\n\n"
  },
  {
    "path": "docs/cn/embed.md",
    "chars": 2966,
    "preview": "# PHP 应用程序作为独立二进制文件\n\nFrankenPHP 能够将 PHP 应用程序的源代码和资源文件嵌入到静态的、独立的二进制文件中。\n\n由于这个特性,PHP 应用程序可以作为独立的二进制文件分发,包括应用程序本身、PHP 解释器和生"
  },
  {
    "path": "docs/cn/extension-workers.md",
    "chars": 4262,
    "preview": "# 扩展 Worker\n\n扩展 Worker 使您的 [FrankenPHP 扩展](https://frankenphp.dev/docs/extensions/) 能够管理专用的 PHP 线程池,用于执行后台任务、处理异步事件或实现自定"
  },
  {
    "path": "docs/cn/extensions.md",
    "chars": 18386,
    "preview": "# 使用 Go 编写 PHP 扩展\n\n使用 FrankenPHP,你可以**使用 Go 编写 PHP 扩展**,这允许你创建**高性能的原生函数**,可以直接从 PHP 调用。你的应用程序可以利用任何现有或新的 Go 库,以及直接从你的 P"
  },
  {
    "path": "docs/cn/github-actions.md",
    "chars": 792,
    "preview": "# 使用 GitHub Actions\n\n此存储库构建 Docker 镜像并将其部署到 [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) 上\n每个批准的拉取请求或设置后在你自"
  },
  {
    "path": "docs/cn/hot-reload.md",
    "chars": 3412,
    "preview": "# 热重载\n\nFrankenPHP 包含一个内置的**热重载**功能,旨在极大改善开发者的体验。\n\n![Hot Reload](hot-reload.png)\n\n此功能提供了类似于现代 JavaScript 工具(如 Vite 或 webp"
  },
  {
    "path": "docs/cn/known-issues.md",
    "chars": 5591,
    "preview": "# 已知问题\n\n## 不支持的 PHP 扩展\n\n已知以下扩展与 FrankenPHP 不兼容:\n\n| 名称                                                                   "
  },
  {
    "path": "docs/cn/laravel.md",
    "chars": 4006,
    "preview": "# Laravel\n\n## Docker\n\n使用 FrankenPHP 为 [Laravel](https://laravel.com) Web 应用程序提供服务就像将项目挂载到官方 Docker 镜像的 `/app` 目录中一样简单。\n\n"
  },
  {
    "path": "docs/cn/mercure.md",
    "chars": 513,
    "preview": "# 实时\n\nFrankenPHP 配备了内置的 [Mercure](https://mercure.rocks) 中心!\nMercure 允许将事件实时推送到所有连接的设备:它们将立即收到 JavaScript 事件。\n\n无需 JS 库或 "
  },
  {
    "path": "docs/cn/metrics.md",
    "chars": 918,
    "preview": "# 指标\n\n当启用 [Caddy 指标](https://caddyserver.com/docs/metrics) 时,FrankenPHP 公开以下指标:\n\n- `frankenphp_total_threads`:PHP 线程的总数。"
  },
  {
    "path": "docs/cn/performance.md",
    "chars": 4809,
    "preview": "# 性能\n\n默认情况下,FrankenPHP 尝试在性能和易用性之间提供良好的折衷。\n但是,通过使用适当的配置,可以大幅提高性能。\n\n## 线程和 Worker 数量\n\n默认情况下,FrankenPHP 启动的线程和 worker(在 wo"
  },
  {
    "path": "docs/cn/production.md",
    "chars": 3299,
    "preview": "# 在生产环境中部署\n\n在本教程中,我们将学习如何使用 Docker Compose 在单个服务器上部署 PHP 应用程序。\n\n如果你使用的是 Symfony,请阅读 Symfony Docker 项目(使用 FrankenPHP)的 [在"
  },
  {
    "path": "docs/cn/static.md",
    "chars": 5252,
    "preview": "# 创建静态构建\n\n与其使用本地安装的PHP库,\n由于伟大的 [static-php-cli 项目](https://github.com/crazywhalecc/static-php-cli),创建一个静态或基本静态的 FrankenP"
  },
  {
    "path": "docs/cn/worker.md",
    "chars": 4549,
    "preview": "# 使用 FrankenPHP Workers\n\n启动一次应用程序并将其保存在内存中。\nFrankenPHP 将在几毫秒内处理传入请求。\n\n## 启动 Worker 脚本\n\n### Docker\n\n将 `FRANKENPHP_CONFIG`"
  },
  {
    "path": "docs/cn/x-sendfile.md",
    "chars": 1530,
    "preview": "# 高效服务大型静态文件 (`X-Sendfile`/`X-Accel-Redirect`)\n\n通常,静态文件可以直接由 Web 服务器提供服务,\n但有时在发送它们之前需要执行一些 PHP 代码:\n访问控制、统计、自定义 HTTP 头..."
  },
  {
    "path": "docs/compile.md",
    "chars": 4981,
    "preview": "# Compile From Sources\n\nThis document explains how to create a FrankenPHP binary that will load PHP as a dynamic library"
  },
  {
    "path": "docs/config.md",
    "chars": 15488,
    "preview": "# Configuration\n\nFrankenPHP, Caddy as well as the [Mercure](mercure.md) and [Vulcain](https://vulcain.rocks) modules can"
  },
  {
    "path": "docs/docker.md",
    "chars": 9533,
    "preview": "# Building Custom Docker Image\n\n[FrankenPHP Docker images](https://hub.docker.com/r/dunglas/frankenphp) are based on [of"
  },
  {
    "path": "docs/early-hints.md",
    "chars": 523,
    "preview": "# Early Hints\n\nFrankenPHP natively supports the [103 Early Hints status code](https://developer.chrome.com/blog/early-hi"
  },
  {
    "path": "docs/embed.md",
    "chars": 4711,
    "preview": "# PHP Apps As Standalone Binaries\n\nFrankenPHP has the ability to embed the source code and assets of PHP applications in"
  },
  {
    "path": "docs/es/CONTRIBUTING.md",
    "chars": 7202,
    "preview": "# Contribuir\n\n## Compilar PHP\n\n### Con Docker (Linux)\n\nConstruya la imagen Docker de desarrollo:\n\n```console\ndocker buil"
  },
  {
    "path": "docs/es/README.md",
    "chars": 5899,
    "preview": "# FrankenPHP: el servidor de aplicaciones PHP moderno, escrito en Go\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev"
  },
  {
    "path": "docs/es/classic.md",
    "chars": 1452,
    "preview": "# Usando el Modo Clásico\n\nSin ninguna configuración adicional, FrankenPHP opera en modo clásico. En este modo, FrankenPH"
  },
  {
    "path": "docs/es/compile.md",
    "chars": 5370,
    "preview": "# Compilar desde fuentes\n\nEste documento explica cómo crear un binario de FrankenPHP que cargará PHP como una biblioteca"
  },
  {
    "path": "docs/es/config.md",
    "chars": 14520,
    "preview": "# Configuración\n\nFrankenPHP, Caddy así como los módulos [Mercure](mercure.md) y [Vulcain](https://vulcain.rocks) pueden "
  },
  {
    "path": "docs/es/docker.md",
    "chars": 10278,
    "preview": "# Construir una imagen Docker personalizada\n\nLas [imágenes Docker de FrankenPHP](https://hub.docker.com/r/dunglas/franke"
  },
  {
    "path": "docs/es/early-hints.md",
    "chars": 579,
    "preview": "# Early Hints (Pistas Tempranas)\n\nFrankenPHP soporta nativamente el [código de estado 103 Early Hints](https://developer"
  },
  {
    "path": "docs/es/embed.md",
    "chars": 5171,
    "preview": "# Aplicaciones PHP como Binarios Autónomos\n\nFrankenPHP tiene la capacidad de incrustar el código fuente y los activos de"
  },
  {
    "path": "docs/es/extension-workers.md",
    "chars": 6170,
    "preview": "# Extension Workers\n\nLos Extension Workers permiten que tu [extensión FrankenPHP](https://frankenphp.dev/docs/extensions"
  },
  {
    "path": "docs/es/extensions.md",
    "chars": 36809,
    "preview": "# Escribir Extensiones PHP en Go\n\nCon FrankenPHP, puedes **escribir extensiones PHP en Go**, lo que te permite crear **f"
  },
  {
    "path": "docs/es/github-actions.md",
    "chars": 1539,
    "preview": "# Usando GitHub Actions\n\nEste repositorio construye y despliega la imagen Docker en [Docker Hub](https://hub.docker.com/"
  },
  {
    "path": "docs/es/hot-reload.md",
    "chars": 5580,
    "preview": "# Hot reload\n\nFrankenPHP incluye una función de **hot reload** integrada diseñada para mejorar significativamente la exp"
  },
  {
    "path": "docs/es/known-issues.md",
    "chars": 7962,
    "preview": "# Problemas Conocidos\n\n## Extensiones PHP no Soportadas\n\nLas siguientes extensiones se sabe que no son compatibles con F"
  },
  {
    "path": "docs/es/laravel.md",
    "chars": 7871,
    "preview": "# Laravel\n\n## Docker\n\nServir una aplicación web [Laravel](https://laravel.com) con FrankenPHP es tan fácil como montar e"
  },
  {
    "path": "docs/es/logging.md",
    "chars": 2947,
    "preview": "# Registro de actividad\n\nFrankenPHP se integra perfectamente con [el sistema de registro de Caddy](https://caddyserver.c"
  },
  {
    "path": "docs/es/mercure.md",
    "chars": 5669,
    "preview": "# Tiempo Real\n\n¡FrankenPHP incluye un hub [Mercure](https://mercure.rocks) integrado!\nMercure te permite enviar eventos "
  },
  {
    "path": "docs/es/metrics.md",
    "chars": 1557,
    "preview": "# Métricas\n\nCuando las [métricas de Caddy](https://caddyserver.com/docs/metrics) están habilitadas, FrankenPHP expone la"
  },
  {
    "path": "docs/es/performance.md",
    "chars": 9265,
    "preview": "# Rendimiento\n\nPor defecto, FrankenPHP intenta ofrecer un buen compromiso entre rendimiento y facilidad de uso.\nSin emba"
  },
  {
    "path": "docs/es/production.md",
    "chars": 6040,
    "preview": "# Despliegue en Producción\n\nEn este tutorial, aprenderemos cómo desplegar una aplicación PHP en un único servidor usando"
  },
  {
    "path": "docs/es/static.md",
    "chars": 8297,
    "preview": "# Crear una Compilación Estática\n\nEn lugar de usar una instalación local de la biblioteca PHP,\nes posible crear una comp"
  },
  {
    "path": "docs/es/wordpress.md",
    "chars": 1593,
    "preview": "# WordPress\n\nEjecute [WordPress](https://wordpress.org/) con FrankenPHP para disfrutar de una pila moderna y de alto ren"
  },
  {
    "path": "docs/es/worker.md",
    "chars": 7295,
    "preview": "# Usando los Workers de FrankenPHP\n\nInicia tu aplicación una vez y manténla en memoria.\nFrankenPHP gestionará las petici"
  },
  {
    "path": "docs/es/x-sendfile.md",
    "chars": 2535,
    "preview": "# Sirviendo archivos estáticos grandes de manera eficiente (`X-Sendfile`/`X-Accel-Redirect`)\n\nNormalmente, los archivos "
  },
  {
    "path": "docs/extension-workers.md",
    "chars": 5894,
    "preview": "# Extension Workers\n\nExtension Workers enable your [FrankenPHP extension](https://frankenphp.dev/docs/extensions/) to ma"
  },
  {
    "path": "docs/extensions.md",
    "chars": 35359,
    "preview": "# Writing PHP Extensions in Go\n\nWith FrankenPHP, you can **write PHP extensions in Go**, which allows you to create **hi"
  },
  {
    "path": "docs/fr/CONTRIBUTING.md",
    "chars": 7250,
    "preview": "# Contribuer\n\n## Compiler PHP\n\n### Avec Docker (Linux)\n\nConstruisez l'image Docker de développement :\n\n```console\ndocker"
  },
  {
    "path": "docs/fr/README.md",
    "chars": 5904,
    "preview": "# FrankenPHP : le serveur d'applications PHP moderne, écrit en Go\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev\"><"
  },
  {
    "path": "docs/fr/classic.md",
    "chars": 1495,
    "preview": "# Utilisation du mode classique\n\nSans aucune configuration additionnelle, FrankenPHP fonctionne en mode classique. Dans "
  },
  {
    "path": "docs/fr/compile.md",
    "chars": 5082,
    "preview": "# Compiler depuis les sources\n\nCe document explique comment créer un build FrankenPHP qui chargera PHP en tant que bibli"
  },
  {
    "path": "docs/fr/config.md",
    "chars": 17491,
    "preview": "# Configuration\n\nFrankenPHP, Caddy ainsi que les modules [Mercure](mercure.md) et [Vulcain](https://vulcain.rocks) peuve"
  },
  {
    "path": "docs/fr/docker.md",
    "chars": 10666,
    "preview": "# Création d'une image Docker personnalisée\n\nLes images Docker de [FrankenPHP](https://hub.docker.com/r/dunglas/frankenp"
  },
  {
    "path": "docs/fr/early-hints.md",
    "chars": 574,
    "preview": "# Early Hints\n\nFrankenPHP prend nativement en charge le code de statut [103 Early Hints](https://developer.chrome.com/bl"
  },
  {
    "path": "docs/fr/embed.md",
    "chars": 5412,
    "preview": "# Applications PHP en tant que binaires autonomes\n\nFrankenPHP a la capacité d'incorporer le code source et les assets de"
  },
  {
    "path": "docs/fr/extension-workers.md",
    "chars": 6409,
    "preview": "# Workers d'extension\n\nLes Workers d'extension permettent à votre [extension FrankenPHP](https://frankenphp.dev/docs/ext"
  },
  {
    "path": "docs/fr/extensions.md",
    "chars": 38310,
    "preview": "# Écrire des extensions PHP en Go\n\nAvec FrankenPHP, vous pouvez **écrire des extensions PHP en Go**, ce qui vous permet "
  },
  {
    "path": "docs/fr/github-actions.md",
    "chars": 1572,
    "preview": "# Utilisation de GitHub Actions\n\nCe dépôt construit et déploie l'image Docker sur [le Hub Docker](https://hub.docker.com"
  },
  {
    "path": "docs/fr/hot-reload.md",
    "chars": 6128,
    "preview": "# Hot Reload\n\nFrankenPHP inclut une fonctionnalité de **hot reload** intégrée, conçue pour améliorer considérablement l'"
  },
  {
    "path": "docs/fr/known-issues.md",
    "chars": 8355,
    "preview": "# Problèmes Connus\n\n## Extensions PHP non prises en charge\n\nLes extensions suivantes sont connues pour ne pas être compa"
  },
  {
    "path": "docs/fr/laravel.md",
    "chars": 6923,
    "preview": "# Laravel\n\n## Docker\n\nDéployer une application web [Laravel](https://laravel.com) avec FrankenPHP est très facile. Il su"
  },
  {
    "path": "docs/fr/mercure.md",
    "chars": 667,
    "preview": "# Temps Réel\n\nFrankenPHP est livré avec un hub [Mercure](https://mercure.rocks) intégré.\nMercure permet de pousser des é"
  },
  {
    "path": "docs/fr/metrics.md",
    "chars": 1562,
    "preview": "# Métriques\n\nLorsque les [métriques Caddy](https://caddyserver.com/docs/metrics) sont activées, FrankenPHP expose les mé"
  },
  {
    "path": "docs/fr/performance.md",
    "chars": 10370,
    "preview": "# Performance\n\nPar défaut, FrankenPHP essaie d'offrir un bon compromis entre performance et facilité d'utilisation.\nCepe"
  },
  {
    "path": "docs/fr/production.md",
    "chars": 6428,
    "preview": "# Déploiement en Production\n\nDans ce tutoriel, nous apprendrons comment déployer une application PHP sur un serveur uniq"
  },
  {
    "path": "docs/fr/static.md",
    "chars": 8523,
    "preview": "# Créer un binaire statique\n\nAu lieu d'utiliser une installation locale de la bibliothèque PHP, il est possible de créer"
  },
  {
    "path": "docs/fr/worker.md",
    "chars": 7383,
    "preview": "# Utilisation des workers FrankenPHP\n\nDémarrez votre application une fois et gardez-la en mémoire.\nFrankenPHP traitera l"
  },
  {
    "path": "docs/fr/x-sendfile.md",
    "chars": 2562,
    "preview": "# Servir efficacement les gros fichiers statiques (`X-Sendfile`/`X-Accel-Redirect`)\n\nHabituellement, les fichiers statiq"
  },
  {
    "path": "docs/github-actions.md",
    "chars": 1401,
    "preview": "# Using GitHub Actions\n\nThis repository builds and deploys the Docker image to [Docker Hub](https://hub.docker.com/r/dun"
  },
  {
    "path": "docs/hot-reload.md",
    "chars": 5466,
    "preview": "# Hot Reload\n\nFrankenPHP includes a built-in **hot reload** feature designed to vastly improve the developer experience."
  },
  {
    "path": "docs/ja/CONTRIBUTING.md",
    "chars": 5404,
    "preview": "# コントリビューション\n\n## PHPのコンパイル\n\n### Dockerを使用する場合(Linux)\n\n開発用Dockerイメージをビルドします:\n\n```console\ndocker build -t frankenphp-dev -"
  },
  {
    "path": "docs/ja/README.md",
    "chars": 4896,
    "preview": "# FrankenPHP: PHPのためのモダンなアプリケーションサーバー\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev\"><img src=\"frankenphp.png\" alt"
  },
  {
    "path": "docs/ja/classic.md",
    "chars": 774,
    "preview": "# クラシックモードの使用\n\n追加の設定を行わなくても、FrankenPHPはクラシックモードで動作します。このモードでは、FrankenPHPは従来のPHPサーバーのように機能し、PHPファイルを直接提供します。これにより、PHP-FPM"
  },
  {
    "path": "docs/ja/compile.md",
    "chars": 3624,
    "preview": "# ソースからのコンパイル\n\nこのドキュメントでは、PHPを動的ライブラリとしてロードするFrankenPHPバイナリの作成方法を説明します。\nこれが推奨される方法です。\n\nまたは、[完全静的およびほぼ静的なビルド](static.md)も"
  },
  {
    "path": "docs/ja/config.md",
    "chars": 10809,
    "preview": "# 設定\n\nFrankenPHP、Caddy、そして[Mercure](mercure.md)や[Vulcain](https://vulcain.rocks)モジュールは、[Caddyでサポートされる形式](https://caddyse"
  },
  {
    "path": "docs/ja/docker.md",
    "chars": 7641,
    "preview": "# カスタムDockerイメージのビルド\n\n[FrankenPHPのDockerイメージ](https://hub.docker.com/r/dunglas/frankenphp)は、[公式PHPイメージ](https://hub.dock"
  },
  {
    "path": "docs/ja/early-hints.md",
    "chars": 441,
    "preview": "# Early Hints\n\nFrankenPHPは[103 Early Hints ステータスコード](https://developer.chrome.com/blog/early-hints/)をネイティブサポートしています。\nEar"
  },
  {
    "path": "docs/ja/embed.md",
    "chars": 3351,
    "preview": "# PHPアプリのスタンドアロンバイナリ化\n\nFrankenPHPには、PHPアプリケーションのソースコードやアセットを静的な自己完結型バイナリに埋め込む機能があります。\n\nこの機能により、PHPアプリケーション自体に加えて、PHPインター"
  },
  {
    "path": "docs/ja/extension-workers.md",
    "chars": 4610,
    "preview": "# 拡張ワーカー\n\n拡張ワーカーは、[FrankenPHP拡張機能](https://frankenphp.dev/docs/extensions/)がバックグラウンドタスクの実行、非同期イベントの処理、またはカスタムプロトコルの実装のため"
  },
  {
    "path": "docs/ja/extensions.md",
    "chars": 18215,
    "preview": "# GoでPHP拡張モジュールを作成する\n\nFrankenPHPでは、**GoでPHP拡張モジュールを作成する**ことができます。これにより、PHPから直接呼び出せる**高パフォーマンスなネイティブ関数**を作成できます。アプリケーションは"
  },
  {
    "path": "docs/ja/github-actions.md",
    "chars": 844,
    "preview": "# GitHub Actionsの使用\n\nこのリポジトリでは、承認されたプルリクエストごと、またはセットアップ後のあなた自身のフォークで、\nDockerイメージをビルドして[Docker Hub](https://hub.docker.co"
  },
  {
    "path": "docs/ja/hot-reload.md",
    "chars": 3986,
    "preview": "# ホットリロード\n\nFrankenPHPには、開発者のエクスペリエンスを大幅に向上させるために設計された組み込みの**ホットリロード**機能が含まれています。\n\n![Hot Reload](hot-reload.png)\n\nこの機能は、V"
  },
  {
    "path": "docs/ja/known-issues.md",
    "chars": 6101,
    "preview": "# 既知の問題\n\n## 未対応のPHP拡張モジュール\n\n以下の拡張モジュールはFrankenPHPと互換性がないことが確認されています:\n\n| 名前                                              "
  },
  {
    "path": "docs/ja/laravel.md",
    "chars": 4575,
    "preview": "# Laravel\n\n## Docker\n\nFrankenPHPを使用して[Laravel](https://laravel.com)のWebアプリケーションを配信するのは簡単で、公式Dockerイメージの`/app`ディレクトリにプロジェ"
  },
  {
    "path": "docs/ja/mercure.md",
    "chars": 594,
    "preview": "# リアルタイム\n\nFrankenPHPには組み込みの[Mercure](https://mercure.rocks)ハブが付属しています!\nMercureを使用すると、接続されているすべてのデバイスにリアルタイムイベントをプッシュでき、各"
  },
  {
    "path": "docs/ja/metrics.md",
    "chars": 993,
    "preview": "# メトリクス\n\n[Caddyのメトリクス](https://caddyserver.com/docs/metrics)が有効になっていると、FrankenPHPは以下のメトリクスを公開します:\n\n- `frankenphp_total_t"
  },
  {
    "path": "docs/ja/performance.md",
    "chars": 5934,
    "preview": "# パフォーマンス\n\nデフォルトでは、FrankenPHPはパフォーマンスと使いやすさのバランスが取れた構成を提供するよう設計されています。\nただし、適切な設定により、パフォーマンスを大幅に向上させることが可能です。\n\n## スレッド数とワ"
  },
  {
    "path": "docs/ja/production.md",
    "chars": 3826,
    "preview": "# 本番環境でのデプロイ\n\nこのチュートリアルでは、Docker Composeを使用して単一サーバーにPHPアプリケーションをデプロイする方法を学びます。\n\nSymfonyを使用している場合は、Symfony Dockerプロジェクトの「"
  },
  {
    "path": "docs/ja/static.md",
    "chars": 5896,
    "preview": "# 静的ビルドの作成\n\nPHPライブラリのローカルインストールを使用する代わりに、\n[static-php-cli プロジェクト](https://github.com/crazywhalecc/static-php-cli)を利用して、F"
  },
  {
    "path": "docs/ja/worker.md",
    "chars": 5033,
    "preview": "# FrankenPHPワーカーの使用\n\nアプリケーションを一度起動してメモリに保持します。\nFrankenPHPは数ミリ秒で受信リクエストを処理します。\n\n## ワーカースクリプトの開始\n\n### Docker\n\n`FRANKENPHP_"
  },
  {
    "path": "docs/ja/x-sendfile.md",
    "chars": 1711,
    "preview": "# 大きな静的ファイルを効率的に配信する (`X-Sendfile`/`X-Accel-Redirect`)\n\n通常、静的ファイルはウェブサーバーによって直接配信されますが、\n時にはファイルを送信する前にPHPコードを実行する必要があります"
  },
  {
    "path": "docs/known-issues.md",
    "chars": 7519,
    "preview": "# Known Issues\n\n## Unsupported PHP Extensions\n\nThe following extensions are known not to be compatible with FrankenPHP:\n"
  },
  {
    "path": "docs/laravel.md",
    "chars": 7222,
    "preview": "# Laravel\n\n## Docker\n\nServing a [Laravel](https://laravel.com) web application with FrankenPHP is as easy as mounting th"
  },
  {
    "path": "docs/logging.md",
    "chars": 2717,
    "preview": "# Logging\n\nFrankenPHP integrates seamlessly with [Caddy's logging system](https://caddyserver.com/docs/logging).\nYou can"
  },
  {
    "path": "docs/mercure.md",
    "chars": 5299,
    "preview": "# Real-time\n\nFrankenPHP comes with a built-in [Mercure](https://mercure.rocks) hub!\nMercure allows you to push real-time"
  },
  {
    "path": "docs/metrics.md",
    "chars": 1407,
    "preview": "# Metrics\n\nWhen [Caddy metrics](https://caddyserver.com/docs/metrics) are enabled, FrankenPHP exposes the following metr"
  },
  {
    "path": "docs/performance.md",
    "chars": 9013,
    "preview": "# Performance\n\nBy default, FrankenPHP tries to offer a good compromise between performance and ease of use.\nHowever, it "
  },
  {
    "path": "docs/production.md",
    "chars": 5657,
    "preview": "# Deploying in Production\n\nIn this tutorial, we will learn how to deploy a PHP application on a single server using Dock"
  },
  {
    "path": "docs/pt-br/CONTRIBUTING.md",
    "chars": 6885,
    "preview": "# Contribuindo\n\n## Compilando o PHP\n\n### Com Docker (Linux)\n\nCrie a imagem Docker de desenvolvimento:\n\n```console\ndocker"
  },
  {
    "path": "docs/pt-br/README.md",
    "chars": 5809,
    "preview": "# FrankenPHP: um moderno servidor de aplicações para PHP\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev/pt-br\"><img"
  },
  {
    "path": "docs/pt-br/classic.md",
    "chars": 1447,
    "preview": "# Usando o modo clássico\n\nSem nenhuma configuração adicional, o FrankenPHP opera no modo clássico.\nNeste modo, o Franken"
  },
  {
    "path": "docs/pt-br/compile.md",
    "chars": 4805,
    "preview": "# Compilar a partir do código-fonte\n\nEste documento explica como criar um binário FrankenPHP que carregará o PHP como\num"
  },
  {
    "path": "docs/pt-br/config.md",
    "chars": 16500,
    "preview": "# Configuração\n\nFrankenPHP, Caddy, bem como os módulos [Mercure](mercure.md) e [Vulcain](https://vulcain.rocks), podem s"
  },
  {
    "path": "docs/pt-br/docker.md",
    "chars": 10170,
    "preview": "# Construindo Imagens Docker Personalizadas\n\n[As imagens Docker do FrankenPHP](https://hub.docker.com/r/dunglas/frankenp"
  },
  {
    "path": "docs/pt-br/early-hints.md",
    "chars": 555,
    "preview": "# Early Hints\n\nO FrankenPHP suporta nativamente o\n[código de status 103 Early Hints](https://developer.chrome.com/blog/e"
  },
  {
    "path": "docs/pt-br/embed.md",
    "chars": 5249,
    "preview": "# Aplicações PHP como binários independentes\n\nO FrankenPHP tem a capacidade de incorporar o código-fonte e os assets de\n"
  },
  {
    "path": "docs/pt-br/extension-workers.md",
    "chars": 6203,
    "preview": "# Workers de Extensão\n\nOs Workers de Extensão permitem que sua [extensão FrankenPHP](https://frankenphp.dev/docs/extensi"
  },
  {
    "path": "docs/pt-br/extensions.md",
    "chars": 34069,
    "preview": "# Escrevendo extensões PHP em Go\n\nCom o FrankenPHP, você pode **escrever extensões PHP em Go**, o que permite\ncriar **fu"
  },
  {
    "path": "docs/pt-br/github-actions.md",
    "chars": 1526,
    "preview": "# Usando GitHub Actions\n\nEste repositório constrói e implanta a imagem Docker no\n[Docker Hub](https://hub.docker.com/r/d"
  },
  {
    "path": "docs/pt-br/hot-reload.md",
    "chars": 6030,
    "preview": "# Recarregamento Instantâneo\n\nFrankenPHP inclui um recurso de **recarregamento instantâneo** embutido, projetado para me"
  },
  {
    "path": "docs/pt-br/known-issues.md",
    "chars": 8051,
    "preview": "# Problemas conhecidos\n\n## Extensões PHP não suportadas\n\nAs seguintes extensões são conhecidas por não serem compatíveis"
  },
  {
    "path": "docs/pt-br/laravel.md",
    "chars": 6696,
    "preview": "# Laravel\n\n## Docker\n\nServir uma aplicação web [Laravel](https://laravel.com) com FrankenPHP é tão\nfácil quanto montar o"
  },
  {
    "path": "docs/pt-br/mercure.md",
    "chars": 878,
    "preview": "# Tempo real\n\nO FrankenPHP vem com um hub [Mercure](https://mercure.rocks) integrado!\nO Mercure permite que você envie e"
  },
  {
    "path": "docs/pt-br/metrics.md",
    "chars": 1537,
    "preview": "# Métricas\n\nQuando as [métricas do Caddy](https://caddyserver.com/docs/metrics) estão\nhabilitadas, o FrankenPHP expõe as"
  },
  {
    "path": "docs/pt-br/performance.md",
    "chars": 9638,
    "preview": "# Desempenho\n\nPor padrão, o FrankenPHP tenta oferecer um bom equilíbrio entre desempenho e\nfacilidade de uso.\nNo entanto"
  },
  {
    "path": "docs/pt-br/production.md",
    "chars": 6017,
    "preview": "# Implantando em produção\n\nNeste tutorial, aprenderemos como implantar uma aplicação PHP em um único\nservidor usando o D"
  },
  {
    "path": "docs/pt-br/static.md",
    "chars": 8330,
    "preview": "# Criar uma compilação estática\n\nEm vez de usar uma instalação local da biblioteca PHP, é possível criar uma\ncompilação "
  },
  {
    "path": "docs/pt-br/worker.md",
    "chars": 7119,
    "preview": "# Usando Workers do FrankenPHP\n\nInicialize sua aplicação uma vez e mantenha-a na memória.\nO FrankenPHP processará as req"
  },
  {
    "path": "docs/pt-br/x-sendfile.md",
    "chars": 2516,
    "preview": "# Servindo arquivos estáticos grandes com eficiência (`X-Sendfile`/`X-Accel-Redirect`)\n\nNormalmente, arquivos estáticos "
  },
  {
    "path": "docs/ru/CONTRIBUTING.md",
    "chars": 6556,
    "preview": "# Участие в проекте\n\n## Компиляция PHP\n\n### С помощью Docker (Linux)\n\nСоздайте образ Docker для разработки:\n\n```console\n"
  },
  {
    "path": "docs/ru/README.md",
    "chars": 5668,
    "preview": "# FrankenPHP: Современный сервер приложений для PHP\n\n<h1 align=\"center\"><a href=\"https://frankenphp.dev\"><img src=\"../.."
  },
  {
    "path": "docs/ru/compile.md",
    "chars": 3839,
    "preview": "# Компиляция из исходников\n\nЭтот документ объясняет, как создать бинарный файл FrankenPHP, который будет загружать PHP к"
  },
  {
    "path": "docs/ru/config.md",
    "chars": 16590,
    "preview": "# Конфигурация\n\nFrankenPHP, Caddy, а также модули [Mercure](mercure.md) и [Vulcain](https://vulcain.rocks) могут быть на"
  },
  {
    "path": "docs/ru/docker.md",
    "chars": 9902,
    "preview": "# Создание кастомных Docker-образов\n\n[Docker-образы FrankenPHP](https://hub.docker.com/r/dunglas/frankenphp) основаны на"
  },
  {
    "path": "docs/ru/early-hints.md",
    "chars": 545,
    "preview": "# Early Hints\n\nFrankenPHP изначально поддерживает [Early Hints (103 HTTP статус код)](https://developer.chrome.com/blog/"
  },
  {
    "path": "docs/ru/embed.md",
    "chars": 4762,
    "preview": "# PHP-приложения как автономные бинарные файлы\n\nFrankenPHP позволяет встраивать исходный код и ресурсы PHP-приложений в "
  },
  {
    "path": "docs/ru/extension-workers.md",
    "chars": 6198,
    "preview": "# Расширение Workers\n\nРасширение Workers позволяет вашему [расширению FrankenPHP](https://frankenphp.dev/docs/extensions"
  }
]

// ... and 257 more files (download for full content)

About this extraction

This page contains the full source code of the php/frankenphp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 457 files (2.0 MB), approximately 555.3k tokens, and a symbol index with 1128 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!