The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .dockerignore
├── .github
    ├── ISSUE_TEMPLATE
    │   ├── bug_report.yaml
    │   └── feature_request.yaml
    ├── actions
    │   └── watcher
    │   │   └── action.yaml
    ├── dependabot.yaml
    └── workflows
    │   ├── docker.yaml
    │   ├── lint.yaml
    │   ├── sanitizers.yaml
    │   ├── static.yaml
    │   └── tests.yaml
├── .gitignore
├── .golangci.yaml
├── .hadolint.yaml
├── .markdown-lint.yaml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── SECURITY.md
├── alpine.Dockerfile
├── app.tar
├── app_checksum.txt
├── backoff.go
├── backoff_test.go
├── build-packages.sh
├── 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
    │   └── main.go
    ├── go.mod
    ├── go.sum
    ├── module.go
    ├── module_test.go
    ├── php-cli.go
    ├── php-server.go
    ├── watcher_test.go
    └── workerconfig.go
├── cgi.go
├── context.go
├── debugstate.go
├── dev-alpine.Dockerfile
├── dev.Dockerfile
├── docker-bake.hcl
├── docs
    ├── classic.md
    ├── cn
    │   ├── CONTRIBUTING.md
    │   ├── README.md
    │   ├── compile.md
    │   ├── config.md
    │   ├── docker.md
    │   ├── early-hints.md
    │   ├── embed.md
    │   ├── github-actions.md
    │   ├── known-issues.md
    │   ├── laravel.md
    │   ├── mercure.md
    │   ├── production.md
    │   ├── static.md
    │   └── worker.md
    ├── compile.md
    ├── config.md
    ├── digitalocean-dns.png
    ├── digitalocean-droplet.png
    ├── docker.md
    ├── early-hints.md
    ├── embed.md
    ├── extensions.md
    ├── fr
    │   ├── CONTRIBUTING.md
    │   ├── README.md
    │   ├── classic.md
    │   ├── compile.md
    │   ├── config.md
    │   ├── docker.md
    │   ├── early-hints.md
    │   ├── embed.md
    │   ├── extensions.md
    │   ├── github-actions.md
    │   ├── known-issues.md
    │   ├── laravel.md
    │   ├── mercure.md
    │   ├── metrics.md
    │   ├── performance.md
    │   ├── production.md
    │   ├── static.md
    │   ├── worker.md
    │   └── x-sendfile.md
    ├── github-actions.md
    ├── known-issues.md
    ├── laravel.md
    ├── mercure-hub.png
    ├── mercure.md
    ├── metrics.md
    ├── performance.md
    ├── production.md
    ├── ru
    │   ├── CONTRIBUTING.md
    │   ├── README.md
    │   ├── compile.md
    │   ├── config.md
    │   ├── docker.md
    │   ├── early-hints.md
    │   ├── embed.md
    │   ├── github-actions.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
    │   ├── github-actions.md
    │   ├── known-issues.md
    │   ├── laravel.md
    │   ├── mercure.md
    │   ├── production.md
    │   ├── static.md
    │   └── worker.md
    ├── worker.md
    └── x-sendfile.md
├── embed.go
├── env.go
├── ext.go
├── frankenphp.c
├── frankenphp.go
├── frankenphp.h
├── frankenphp.png
├── frankenphp.stub.php
├── frankenphp_arginfo.h
├── frankenphp_test.go
├── go.mod
├── go.sum
├── 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
    │   ├── 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
    ├── testcli
    │   └── main.go
    ├── testext
    │   ├── ext_test.go
    │   ├── extension.h
    │   ├── extensions.c
    │   ├── exttest.go
    │   └── testdata
    │   │   └── index.php
    ├── testserver
    │   └── main.go
    └── watcher
    │   ├── watch_pattern.go
    │   ├── watch_pattern_test.go
    │   ├── watcher-skip.go
    │   ├── watcher.c
    │   ├── watcher.go
    │   └── watcher.h
├── metrics.go
├── metrics_test.go
├── options.go
├── package
    ├── Caddyfile
    ├── 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
├── request_options.go
├── scaling.go
├── scaling_test.go
├── state.go
├── state_test.go
├── static-builder-gnu.Dockerfile
├── static-builder-musl.Dockerfile
├── testdata
    ├── Caddyfile
    ├── _executor.php
    ├── autoloader-require.php
    ├── autoloader.php
    ├── benchmark.Caddyfile
    ├── command.php
    ├── connectionStatusLog.php
    ├── cookies.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
    ├── large-request.php
    ├── large-response.php
    ├── load-test.js
    ├── log.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
    ├── request-headers.php
    ├── response-headers.php
    ├── server-all-vars-ordered.php
    ├── server-all-vars-ordered.txt
    ├── server-variable.php
    ├── session.php
    ├── sleep.php
    ├── super-globals.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.php
├── threadinactive.go
├── threadregular.go
├── threadworker.go
├── types.c
├── types.go
├── types.h
├── types_test.go
├── typestest.go
├── watcher_test.go
├── worker.go
└── worker_test.go


/.dockerignore:
--------------------------------------------------------------------------------
 1 | /caddy/frankenphp/frankenphp
 2 | /internal/testserver/testserver
 3 | /internal/testcli/testcli
 4 | /dist
 5 | .DS_Store
 6 | .idea/
 7 | .vscode/
 8 | __debug_bin
 9 | frankenphp.test
10 | caddy/frankenphp/Build
11 | *.log
12 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yaml:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Bug Report
 3 | description: File a bug report
 4 | labels: [bug]
 5 | body:
 6 |   - type: markdown
 7 |     attributes:
 8 |       value: |
 9 |         Thanks for taking the time to fill out this bug report!
10 |         Before submitting a bug, please double-check that your problem [is not
11 |         a known issue](https://frankenphp.dev/docs/known-issues/)
12 |         (especially if you use XDebug or Tideways), and that is has not
13 |         [already been reported](https://github.com/php/frankenphp/issues).
14 |   - type: textarea
15 |     id: what-happened
16 |     attributes:
17 |       label: What happened?
18 |       description: |
19 |         Tell us what you do, what you get and what you expected.
20 |         Provide us with some step-by-step instructions to reproduce the issue.
21 |     validations:
22 |       required: true
23 |   - type: dropdown
24 |     id: build
25 |     attributes:
26 |       label: Build Type
27 |       description: What build of FrankenPHP do you use?
28 |       options:
29 |         - Docker (Debian Bookworm)
30 |         - Docker (Alpine)
31 |         - Official static build
32 |         - Standalone binary
33 |         - Custom (tell us more in the description)
34 |       default: 0
35 |     validations:
36 |       required: true
37 |   - type: dropdown
38 |     id: worker
39 |     attributes:
40 |       label: Worker Mode
41 |       description: Does the problem happen only when using the worker mode?
42 |       options:
43 |         - "Yes"
44 |         - "No"
45 |       default: 0
46 |     validations:
47 |       required: true
48 |   - type: dropdown
49 |     id: os
50 |     attributes:
51 |       label: Operating System
52 |       description: What operating system are you executing FrankenPHP with?
53 |       options:
54 |         - GNU/Linux
55 |         - macOS
56 |         - Other (tell us more in the description)
57 |       default: 0
58 |     validations:
59 |       required: true
60 |   - type: dropdown
61 |     id: arch
62 |     attributes:
63 |       label: CPU Architecture
64 |       description: What CPU architecture are you using?
65 |       options:
66 |         - x86_64
67 |         - Apple Silicon
68 |         - x86
69 |         - aarch64
70 |         - Other (tell us more in the description)
71 |       default: 0
72 |   - type: textarea
73 |     id: php
74 |     attributes:
75 |       label: PHP configuration
76 |       description: |
77 |         Please copy and paste the output of the `phpinfo()` function -- remember to remove **sensitive information** like passwords, API keys, etc.
78 |       render: shell
79 |     validations:
80 |       required: true
81 |   - type: textarea
82 |     id: logs
83 |     attributes:
84 |       label: Relevant log output
85 |       description: |
86 |         Please copy and paste any relevant log output.
87 |         This will be automatically formatted into code,
88 |         so no need for backticks.
89 |       render: shell
90 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yaml:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Feature Request
 3 | description: Suggest an idea for this project
 4 | labels: [enhancement]
 5 | body:
 6 |   - type: textarea
 7 |     id: description
 8 |     attributes:
 9 |       label: Describe you feature request
10 |       value: |
11 |         **Is your feature request related to a problem? Please describe.**
12 |         A clear and concise description of what the problem is.
13 |         Ex. I'm always frustrated when [...]
14 | 
15 |         **Describe the solution you'd like**
16 |         A clear and concise description of what you want to happen.
17 | 
18 |         **Describe alternatives you've considered**
19 |         A clear and concise description of any alternative solutions
20 |         or features you've considered.
21 | 


--------------------------------------------------------------------------------
/.github/actions/watcher/action.yaml:
--------------------------------------------------------------------------------
 1 | name: watcher
 2 | description: Install e-dant/watcher
 3 | runs:
 4 |   using: composite
 5 |   steps:
 6 |     - name: Determine e-dant/watcher version
 7 |       id: determine-watcher-version
 8 |       run: echo version="$(gh release view --repo e-dant/watcher --json tagName --template '{{ .tagName }}')" >> "${GITHUB_OUTPUT}"
 9 |       shell: bash
10 |       env:
11 |         GH_TOKEN: ${{ github.token }}
12 |     - name: Cache e-dant/watcher
13 |       id: cache-watcher
14 |       uses: actions/cache@v4
15 |       with:
16 |         path: watcher/target
17 |         key: watcher-${{ runner.os }}-${{ runner.arch }}-${{ steps.determine-watcher-version.outputs.version }}-${{ env.CC && env.CC || 'gcc' }}
18 |     - if: steps.cache-watcher.outputs.cache-hit != 'true'
19 |       name: Compile e-dant/watcher
20 |       run: |
21 |         mkdir watcher
22 |         gh release download --repo e-dant/watcher -A tar.gz -O - | tar -xz -C watcher --strip-components 1
23 |         cd watcher
24 |         cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
25 |         cmake --build build
26 |         sudo cmake --install build --prefix target
27 |       shell: bash
28 |       env:
29 |         GH_TOKEN: ${{ github.token }}
30 |     - name: Update LD_LIBRARY_PATH
31 |       run: |
32 |         sudo sh -c "echo ${PWD}/watcher/target/lib > /etc/ld.so.conf.d/watcher.conf"
33 |         sudo ldconfig
34 |       shell: bash
35 | 


--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
 1 | ---
 2 | version: 2
 3 | updates:
 4 |   - package-ecosystem: gomod
 5 |     directory: /
 6 |     schedule:
 7 |       interval: weekly
 8 |     commit-message:
 9 |       prefix: chore
10 |     groups:
11 |       go-modules:
12 |         patterns:
13 |           - "*"
14 |   - package-ecosystem: gomod
15 |     directory: /caddy
16 |     schedule:
17 |       interval: weekly
18 |     commit-message:
19 |       prefix: chore(caddy)
20 |     groups:
21 |       go-modules:
22 |         patterns:
23 |           - "*"
24 |   - package-ecosystem: github-actions
25 |     directory: /
26 |     schedule:
27 |       interval: weekly
28 |     commit-message:
29 |       prefix: ci
30 |     groups:
31 |       github-actions:
32 |         patterns:
33 |           - "*"
34 | 


--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Lint Code Base
 3 | concurrency:
 4 |   cancel-in-progress: true
 5 |   group: ${{ github.workflow }}-${{ github.ref }}
 6 | on:
 7 |   pull_request:
 8 |     branches:
 9 |       - main
10 |   push:
11 |     branches:
12 |       - main
13 | permissions:
14 |   contents: read
15 |   packages: read
16 |   statuses: write
17 | jobs:
18 |   build:
19 |     name: Lint Code Base
20 |     runs-on: ubuntu-latest
21 |     steps:
22 |       - name: Checkout Code
23 |         uses: actions/checkout@v4
24 |         with:
25 |           fetch-depth: 0
26 |       - name: Lint Code Base
27 |         uses: super-linter/super-linter/slim@v7.4.0
28 |         env:
29 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 |           LINTER_RULES_PATH: /
31 |           MARKDOWN_CONFIG_FILE: .markdown-lint.yaml
32 |           VALIDATE_CPP: false
33 |           VALIDATE_JSCPD: false
34 |           VALIDATE_GO: false
35 |           VALIDATE_GO_MODULES: false
36 |           VALIDATE_PHP_PHPCS: false
37 |           VALIDATE_PHP_PHPSTAN: false
38 |           VALIDATE_PHP_PSALM: false
39 |           VALIDATE_TERRAGRUNT: false
40 |           VALIDATE_DOCKERFILE_HADOLINT: false
41 |           # Prettier and StandardJS are incompatible
42 |           VALIDATE_JAVASCRIPT_PRETTIER: false
43 |           VALIDATE_TYPESCRIPT_PRETTIER: false
44 |           # Conflicts with MARKDOWN
45 |           VALIDATE_MARKDOWN_PRETTIER: false
46 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | /caddy/frankenphp/frankenphp
 2 | /internal/testserver/testserver
 3 | /internal/testcli/testcli
 4 | /dist
 5 | .DS_Store
 6 | .idea/
 7 | .vscode/
 8 | __debug_bin
 9 | frankenphp.test
10 | caddy/frankenphp/Build
11 | package/etc/php.ini
12 | *.log
13 | 


--------------------------------------------------------------------------------
/.golangci.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: "2"
3 | run:
4 |   build-tags:
5 |     - nobadger
6 |     - nomysql
7 |     - nopgx
8 | 


--------------------------------------------------------------------------------
/.hadolint.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | ignored:
3 |   - DL3006
4 |   - DL3008
5 |   - DL3018
6 |   - DL3022
7 | 


--------------------------------------------------------------------------------
/.markdown-lint.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | no-hard-tabs: false
3 | MD013: false
4 | MD033: false
5 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | The MIT license
 2 | 
 3 | Copyright (c) 2022-present Kévin Dunglas
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is furnished
10 | to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
 1 | # Security Policy
 2 | 
 3 | ## Supported Versions
 4 | 
 5 | Only the latest version is supported.
 6 | Please ensure that you're always using the latest release.
 7 | 
 8 | Binaries and Docker images are rebuilt nightly using the latest versions of dependencies.
 9 | 
10 | ## Reporting a Vulnerability
11 | 
12 | If you believe you have discovered a security issue directly affecting FrankenPHP,
13 | please do **NOT** report it publicly.
14 | 
15 | 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).
16 | 
17 | Only vulnerabilities directly affecting FrankenPHP should be reported to this project.
18 | Flaws affecting components used by FrankenPHP (PHP, Caddy, Go...) or using FrankenPHP (Laravel Octane, PHP Runtime...) should be reported to the relevant projects.
19 | 


--------------------------------------------------------------------------------
/app.tar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php/frankenphp/34fbfd467b264dd75e5b02534aceec179217fb35/app.tar


--------------------------------------------------------------------------------
/app_checksum.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php/frankenphp/34fbfd467b264dd75e5b02534aceec179217fb35/app_checksum.txt


--------------------------------------------------------------------------------
/backoff.go:
--------------------------------------------------------------------------------
 1 | package frankenphp
 2 | 
 3 | import (
 4 | 	"sync"
 5 | 	"time"
 6 | )
 7 | 
 8 | type exponentialBackoff struct {
 9 | 	backoff                time.Duration
10 | 	failureCount           int
11 | 	mu                     sync.RWMutex
12 | 	maxBackoff             time.Duration
13 | 	minBackoff             time.Duration
14 | 	maxConsecutiveFailures int
15 | }
16 | 
17 | // recordSuccess resets the backoff and failureCount
18 | func (e *exponentialBackoff) recordSuccess() {
19 | 	e.mu.Lock()
20 | 	e.failureCount = 0
21 | 	e.backoff = e.minBackoff
22 | 	e.mu.Unlock()
23 | }
24 | 
25 | // recordFailure increments the failure count and increases the backoff, it returns true if maxConsecutiveFailures has been reached
26 | func (e *exponentialBackoff) recordFailure() bool {
27 | 	e.mu.Lock()
28 | 	e.failureCount += 1
29 | 	if e.backoff < e.minBackoff {
30 | 		e.backoff = e.minBackoff
31 | 	}
32 | 
33 | 	e.backoff = min(e.backoff*2, e.maxBackoff)
34 | 
35 | 	e.mu.Unlock()
36 | 	return e.maxConsecutiveFailures != -1 && e.failureCount >= e.maxConsecutiveFailures
37 | }
38 | 
39 | // wait sleeps for the backoff duration if failureCount is non-zero.
40 | // NOTE: this is not tested and should be kept 'obviously correct' (i.e., simple)
41 | func (e *exponentialBackoff) wait() {
42 | 	e.mu.RLock()
43 | 	if e.failureCount == 0 {
44 | 		e.mu.RUnlock()
45 | 
46 | 		return
47 | 	}
48 | 	e.mu.RUnlock()
49 | 
50 | 	time.Sleep(e.backoff)
51 | }
52 | 


--------------------------------------------------------------------------------
/backoff_test.go:
--------------------------------------------------------------------------------
 1 | package frankenphp
 2 | 
 3 | import (
 4 | 	"github.com/stretchr/testify/assert"
 5 | 	"testing"
 6 | 	"time"
 7 | )
 8 | 
 9 | func TestExponentialBackoff_Reset(t *testing.T) {
10 | 	e := &exponentialBackoff{
11 | 		maxBackoff:             5 * time.Second,
12 | 		minBackoff:             500 * time.Millisecond,
13 | 		maxConsecutiveFailures: 3,
14 | 	}
15 | 
16 | 	assert.False(t, e.recordFailure())
17 | 	assert.False(t, e.recordFailure())
18 | 	e.recordSuccess()
19 | 
20 | 	e.mu.RLock()
21 | 	defer e.mu.RUnlock()
22 | 	assert.Equal(t, 0, e.failureCount, "expected failureCount to be reset to 0")
23 | 	assert.Equal(t, e.backoff, e.minBackoff, "expected backoff to be reset to minBackoff")
24 | }
25 | 
26 | func TestExponentialBackoff_Trigger(t *testing.T) {
27 | 	e := &exponentialBackoff{
28 | 		maxBackoff:             500 * 3 * time.Millisecond,
29 | 		minBackoff:             500 * time.Millisecond,
30 | 		maxConsecutiveFailures: 3,
31 | 	}
32 | 
33 | 	assert.False(t, e.recordFailure())
34 | 	assert.False(t, e.recordFailure())
35 | 	assert.True(t, e.recordFailure())
36 | 
37 | 	e.mu.RLock()
38 | 	defer e.mu.RUnlock()
39 | 	assert.Equal(t, e.failureCount, e.maxConsecutiveFailures, "expected failureCount to be maxConsecutiveFailures")
40 | 	assert.Equal(t, e.backoff, e.maxBackoff, "expected backoff to be maxBackoff")
41 | }
42 | 


--------------------------------------------------------------------------------
/caddy/admin.go:
--------------------------------------------------------------------------------
 1 | package caddy
 2 | 
 3 | import (
 4 | 	"encoding/json"
 5 | 	"fmt"
 6 | 	"github.com/caddyserver/caddy/v2"
 7 | 	"github.com/dunglas/frankenphp"
 8 | 	"net/http"
 9 | )
10 | 
11 | type FrankenPHPAdmin struct{}
12 | 
13 | // if the id starts with "admin.api" the module will register AdminRoutes via module.Routes()
14 | func (FrankenPHPAdmin) CaddyModule() caddy.ModuleInfo {
15 | 	return caddy.ModuleInfo{
16 | 		ID:  "admin.api.frankenphp",
17 | 		New: func() caddy.Module { return new(FrankenPHPAdmin) },
18 | 	}
19 | }
20 | 
21 | // EXPERIMENTAL: These routes are not yet stable and may change in the future.
22 | func (admin FrankenPHPAdmin) Routes() []caddy.AdminRoute {
23 | 	return []caddy.AdminRoute{
24 | 		{
25 | 			Pattern: "/frankenphp/workers/restart",
26 | 			Handler: caddy.AdminHandlerFunc(admin.restartWorkers),
27 | 		},
28 | 		{
29 | 			Pattern: "/frankenphp/threads",
30 | 			Handler: caddy.AdminHandlerFunc(admin.threads),
31 | 		},
32 | 	}
33 | }
34 | 
35 | func (admin *FrankenPHPAdmin) restartWorkers(w http.ResponseWriter, r *http.Request) error {
36 | 	if r.Method != http.MethodPost {
37 | 		return admin.error(http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
38 | 	}
39 | 
40 | 	frankenphp.RestartWorkers()
41 | 	caddy.Log().Info("workers restarted from admin api")
42 | 	admin.success(w, "workers restarted successfully\n")
43 | 
44 | 	return nil
45 | }
46 | 
47 | func (admin *FrankenPHPAdmin) threads(w http.ResponseWriter, _ *http.Request) error {
48 | 	debugState := frankenphp.DebugState()
49 | 	prettyJson, err := json.MarshalIndent(debugState, "", "    ")
50 | 	if err != nil {
51 | 		return admin.error(http.StatusInternalServerError, err)
52 | 	}
53 | 
54 | 	return admin.success(w, string(prettyJson))
55 | }
56 | 
57 | func (admin *FrankenPHPAdmin) success(w http.ResponseWriter, message string) error {
58 | 	w.WriteHeader(http.StatusOK)
59 | 	_, err := w.Write([]byte(message))
60 | 	return err
61 | }
62 | 
63 | func (admin *FrankenPHPAdmin) error(statusCode int, err error) error {
64 | 	return caddy.APIError{HTTPStatus: statusCode, Err: err}
65 | }
66 | 


--------------------------------------------------------------------------------
/caddy/br-skip.go:
--------------------------------------------------------------------------------
1 | //go:build nobrotli
2 | 
3 | package caddy
4 | 
5 | var brotli = false
6 | 


--------------------------------------------------------------------------------
/caddy/br.go:
--------------------------------------------------------------------------------
1 | //go:build !nobrotli
2 | 
3 | package caddy
4 | 
5 | var brotli = true
6 | 


--------------------------------------------------------------------------------
/caddy/caddy.go:
--------------------------------------------------------------------------------
 1 | // Package caddy provides a PHP module for the Caddy web server.
 2 | // FrankenPHP embeds the PHP interpreter directly in Caddy, giving it the ability to run your PHP scripts directly.
 3 | // No PHP FPM required!
 4 | package caddy
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 
 9 | 	"github.com/caddyserver/caddy/v2"
10 | 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
11 | 	"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
12 | 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
13 | )
14 | 
15 | const (
16 | 	defaultDocumentRoot = "public"
17 | 	defaultWatchPattern = "./**/*.{php,yaml,yml,twig,env}"
18 | )
19 | 
20 | func init() {
21 | 	caddy.RegisterModule(FrankenPHPApp{})
22 | 	caddy.RegisterModule(FrankenPHPModule{})
23 | 	caddy.RegisterModule(FrankenPHPAdmin{})
24 | 
25 | 	httpcaddyfile.RegisterGlobalOption("frankenphp", parseGlobalOption)
26 | 
27 | 	httpcaddyfile.RegisterHandlerDirective("php", parseCaddyfile)
28 | 	httpcaddyfile.RegisterDirectiveOrder("php", "before", "file_server")
29 | 
30 | 	httpcaddyfile.RegisterDirective("php_server", parsePhpServer)
31 | 	httpcaddyfile.RegisterDirectiveOrder("php_server", "before", "file_server")
32 | }
33 | 
34 | // return a nice error message
35 | func wrongSubDirectiveError(module string, allowedDriectives string, wrongValue string) error {
36 | 	return fmt.Errorf("unknown '%s' subdirective: '%s' (allowed directives are: %s)", module, wrongValue, allowedDriectives)
37 | }
38 | 
39 | // Interface guards
40 | var (
41 | 	_ caddy.App                   = (*FrankenPHPApp)(nil)
42 | 	_ caddy.Provisioner           = (*FrankenPHPApp)(nil)
43 | 	_ caddy.Provisioner           = (*FrankenPHPModule)(nil)
44 | 	_ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
45 | 	_ caddyfile.Unmarshaler       = (*FrankenPHPModule)(nil)
46 | )
47 | 


--------------------------------------------------------------------------------
/caddy/extinit.go:
--------------------------------------------------------------------------------
 1 | package caddy
 2 | 
 3 | import (
 4 | 	"errors"
 5 | 	"github.com/dunglas/frankenphp/internal/extgen"
 6 | 	"log"
 7 | 	"os"
 8 | 	"path/filepath"
 9 | 	"strings"
10 | 
11 | 	caddycmd "github.com/caddyserver/caddy/v2/cmd"
12 | 	"github.com/spf13/cobra"
13 | )
14 | 
15 | func init() {
16 | 	caddycmd.RegisterCommand(caddycmd.Command{
17 | 		Name:  "extension-init",
18 | 		Usage: "go_extension.go [--verbose]",
19 | 		Short: "(Experimental) Initializes a PHP extension from a Go file",
20 | 		Long: `
21 | 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.`,
22 | 		CobraFunc: func(cmd *cobra.Command) {
23 | 			cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
24 | 
25 | 			cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdInitExtension)
26 | 		},
27 | 	})
28 | }
29 | 
30 | func cmdInitExtension(fs caddycmd.Flags) (int, error) {
31 | 	if len(os.Args) < 3 {
32 | 		return 1, errors.New("the path to the Go source is required")
33 | 	}
34 | 
35 | 	sourceFile := os.Args[2]
36 | 
37 | 	baseName := strings.TrimSuffix(filepath.Base(sourceFile), ".go")
38 | 
39 | 	baseName = extgen.SanitizePackageName(baseName)
40 | 
41 | 	sourceDir := filepath.Dir(sourceFile)
42 | 	buildDir := filepath.Join(sourceDir, "build")
43 | 
44 | 	generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: buildDir}
45 | 
46 | 	if err := generator.Generate(); err != nil {
47 | 		return 1, err
48 | 	}
49 | 
50 | 	log.Printf("PHP extension %q initialized successfully in %q", baseName, generator.BuildDir)
51 | 
52 | 	return 0, nil
53 | }
54 | 


--------------------------------------------------------------------------------
/caddy/frankenphp/Caddyfile:
--------------------------------------------------------------------------------
 1 | # The Caddyfile is an easy way to configure FrankenPHP and the Caddy web server.
 2 | #
 3 | # https://frankenphp.dev/docs/config
 4 | # https://caddyserver.com/docs/caddyfile
 5 | 
 6 | {
 7 | 	skip_install_trust
 8 | 
 9 | 	{$CADDY_GLOBAL_OPTIONS}
10 | 
11 | 	frankenphp {
12 | 		{$FRANKENPHP_CONFIG}
13 | 	}
14 | }
15 | 
16 | {$CADDY_EXTRA_CONFIG}
17 | 
18 | {$SERVER_NAME:localhost} {
19 | 	#log {
20 | 	#	# Redact the authorization query parameter that can be set by Mercure
21 | 	#	format filter {
22 | 	#		request>uri query {
23 | 	#			replace authorization REDACTED
24 | 	#		}
25 | 	#	}
26 | 	#}
27 | 
28 | 	root {$SERVER_ROOT:public/}
29 | 	encode zstd br gzip
30 | 
31 | 	# Uncomment the following lines to enable Mercure and Vulcain modules
32 | 	#mercure {
33 | 	#	# Transport to use (default to Bolt)
34 | 	#	transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
35 | 	#	# Publisher JWT key
36 | 	#	publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
37 | 	#	# Subscriber JWT key
38 | 	#	subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
39 | 	#	# Allow anonymous subscribers (double-check that it's what you want)
40 | 	#	anonymous
41 | 	#	# Enable the subscription API (double-check that it's what you want)
42 | 	#	subscriptions
43 | 	#	# Extra directives
44 | 	#	{$MERCURE_EXTRA_DIRECTIVES}
45 | 	#}
46 | 	#vulcain
47 | 
48 | 	{$CADDY_SERVER_EXTRA_DIRECTIVES}
49 | 
50 | 	php_server {
51 | 		#worker /path/to/your/worker.php
52 | 	}
53 | }
54 | 
55 | # As an alternative to editing the above site block, you can add your own site
56 | # block files in the Caddyfile.d directory, and they will be included as long
57 | # as they use the .caddyfile extension.
58 | 
59 | import Caddyfile.d/*.caddyfile
60 | 


--------------------------------------------------------------------------------
/caddy/frankenphp/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	caddycmd "github.com/caddyserver/caddy/v2/cmd"
 5 | 
 6 | 	// plug in Caddy modules here.
 7 | 	_ "github.com/caddyserver/caddy/v2/modules/standard"
 8 | 	_ "github.com/dunglas/caddy-cbrotli"
 9 | 	_ "github.com/dunglas/frankenphp/caddy"
10 | 	_ "github.com/dunglas/mercure/caddy"
11 | 	_ "github.com/dunglas/vulcain/caddy"
12 | )
13 | 
14 | func main() {
15 | 	caddycmd.Main()
16 | }
17 | 


--------------------------------------------------------------------------------
/caddy/module_test.go:
--------------------------------------------------------------------------------
 1 | package caddy_test
 2 | 
 3 | import (
 4 | 	"net/http"
 5 | 	"os"
 6 | 	"strconv"
 7 | 	"testing"
 8 | 
 9 | 	"github.com/caddyserver/caddy/v2/caddytest"
10 | )
11 | 
12 | func TestRootBehavesTheSameOutsideAndInsidePhpServer(t *testing.T) {
13 | 	tester := caddytest.NewTester(t)
14 | 	testPortNum, _ := strconv.Atoi(testPort)
15 | 	testPortTwo := strconv.Itoa(testPortNum + 1)
16 | 	expectedFileResponse, _ := os.ReadFile("../testdata/files/static.txt")
17 | 	hostWithRootOutside := "http://localhost:" + testPort
18 | 	hostWithRootInside := "http://localhost:" + testPortTwo
19 | 	tester.InitServer(`
20 | 		{
21 | 			skip_install_trust
22 | 			admin localhost:2999
23 | 		}
24 | 
25 | 		`+hostWithRootOutside+` {
26 | 			root ../testdata
27 | 			php_server
28 | 		}
29 | 
30 | 		`+hostWithRootInside+` {
31 | 			php_server {
32 | 				root ../testdata
33 | 			}
34 | 		}
35 | 		`, "caddyfile")
36 | 
37 | 	// serve a static file
38 | 	tester.AssertGetResponse(hostWithRootOutside+"/files/static.txt", http.StatusOK, string(expectedFileResponse))
39 | 	tester.AssertGetResponse(hostWithRootInside+"/files/static.txt", http.StatusOK, string(expectedFileResponse))
40 | 
41 | 	// serve a php file
42 | 	tester.AssertGetResponse(hostWithRootOutside+"/hello.php", http.StatusOK, "Hello from PHP")
43 | 	tester.AssertGetResponse(hostWithRootInside+"/hello.php", http.StatusOK, "Hello from PHP")
44 | 
45 | 	// fallback to index.php
46 | 	tester.AssertGetResponse(hostWithRootOutside+"/some-path", http.StatusOK, "I am by birth a Genevese (i not set)")
47 | 	tester.AssertGetResponse(hostWithRootInside+"/some-path", http.StatusOK, "I am by birth a Genevese (i not set)")
48 | 
49 | 	// fallback to directory index ('dirIndex' in module.go)
50 | 	tester.AssertGetResponse(hostWithRootOutside+"/dirindex/", http.StatusOK, "Hello from directory index.php")
51 | 	tester.AssertGetResponse(hostWithRootInside+"/dirindex/", http.StatusOK, "Hello from directory index.php")
52 | }
53 | 


--------------------------------------------------------------------------------
/caddy/php-cli.go:
--------------------------------------------------------------------------------
 1 | package caddy
 2 | 
 3 | import (
 4 | 	"errors"
 5 | 	"os"
 6 | 	"path/filepath"
 7 | 
 8 | 	caddycmd "github.com/caddyserver/caddy/v2/cmd"
 9 | 	"github.com/dunglas/frankenphp"
10 | 
11 | 	"github.com/spf13/cobra"
12 | )
13 | 
14 | func init() {
15 | 	caddycmd.RegisterCommand(caddycmd.Command{
16 | 		Name:  "php-cli",
17 | 		Usage: "script.php [args ...]",
18 | 		Short: "Runs a PHP command",
19 | 		Long: `
20 | Executes a PHP script similarly to the CLI SAPI.`,
21 | 		CobraFunc: func(cmd *cobra.Command) {
22 | 			cmd.DisableFlagParsing = true
23 | 			cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdPHPCLI)
24 | 		},
25 | 	})
26 | }
27 | 
28 | func cmdPHPCLI(fs caddycmd.Flags) (int, error) {
29 | 	args := os.Args[2:]
30 | 	if len(args) < 1 {
31 | 		return 1, errors.New("the path to the PHP script is required")
32 | 	}
33 | 
34 | 	if frankenphp.EmbeddedAppPath != "" {
35 | 		if _, err := os.Stat(args[0]); err != nil {
36 | 			args[0] = filepath.Join(frankenphp.EmbeddedAppPath, args[0])
37 | 		}
38 | 	}
39 | 
40 | 	var status int
41 | 	if len(args) >= 2 && args[0] == "-r" {
42 | 		status = frankenphp.ExecutePHPCode(args[1])
43 | 	} else {
44 | 		status = frankenphp.ExecuteScriptCLI(args[0], args)
45 | 	}
46 | 
47 | 	os.Exit(status)
48 | 
49 | 	return status, nil
50 | }
51 | 


--------------------------------------------------------------------------------
/caddy/watcher_test.go:
--------------------------------------------------------------------------------
 1 | //go:build !nowatcher
 2 | 
 3 | package caddy_test
 4 | 
 5 | import (
 6 | 	"net/http"
 7 | 	"testing"
 8 | 
 9 | 	"github.com/caddyserver/caddy/v2/caddytest"
10 | )
11 | 
12 | func TestWorkerWithInactiveWatcher(t *testing.T) {
13 | 	tester := caddytest.NewTester(t)
14 | 	tester.InitServer(`
15 | 		{
16 | 			skip_install_trust
17 | 			admin localhost:2999
18 | 			http_port `+testPort+`
19 | 
20 | 			frankenphp {
21 | 				worker {
22 | 					file ../testdata/worker-with-counter.php
23 | 					num 1
24 | 					watch ./**/*.php
25 | 				}
26 | 			}
27 | 		}
28 | 
29 | 		localhost:`+testPort+` {
30 | 			root ../testdata
31 | 			rewrite worker-with-counter.php
32 | 			php
33 | 		}
34 | 		`, "caddyfile")
35 | 
36 | 	tester.AssertGetResponse("http://localhost:"+testPort, http.StatusOK, "requests:1")
37 | 	tester.AssertGetResponse("http://localhost:"+testPort, http.StatusOK, "requests:2")
38 | }
39 | 


--------------------------------------------------------------------------------
/debugstate.go:
--------------------------------------------------------------------------------
 1 | package frankenphp
 2 | 
 3 | // EXPERIMENTAL: ThreadDebugState prints the state of a single PHP thread - debugging purposes only
 4 | type ThreadDebugState struct {
 5 | 	Index                    int
 6 | 	Name                     string
 7 | 	State                    string
 8 | 	IsWaiting                bool
 9 | 	IsBusy                   bool
10 | 	WaitingSinceMilliseconds int64
11 | }
12 | 
13 | // EXPERIMENTAL: FrankenPHPDebugState prints the state of all PHP threads - debugging purposes only
14 | type FrankenPHPDebugState struct {
15 | 	ThreadDebugStates   []ThreadDebugState
16 | 	ReservedThreadCount int
17 | }
18 | 
19 | // EXPERIMENTAL: DebugState prints the state of all PHP threads - debugging purposes only
20 | func DebugState() FrankenPHPDebugState {
21 | 	fullState := FrankenPHPDebugState{
22 | 		ThreadDebugStates:   make([]ThreadDebugState, 0, len(phpThreads)),
23 | 		ReservedThreadCount: 0,
24 | 	}
25 | 	for _, thread := range phpThreads {
26 | 		if thread.state.is(stateReserved) {
27 | 			fullState.ReservedThreadCount++
28 | 			continue
29 | 		}
30 | 		fullState.ThreadDebugStates = append(fullState.ThreadDebugStates, threadDebugState(thread))
31 | 	}
32 | 
33 | 	return fullState
34 | }
35 | 
36 | // threadDebugState creates a small jsonable status message for debugging purposes
37 | func threadDebugState(thread *phpThread) ThreadDebugState {
38 | 	return ThreadDebugState{
39 | 		Index:                    thread.threadIndex,
40 | 		Name:                     thread.name(),
41 | 		State:                    thread.state.name(),
42 | 		IsWaiting:                thread.state.isInWaitingState(),
43 | 		IsBusy:                   !thread.state.isInWaitingState(),
44 | 		WaitingSinceMilliseconds: thread.state.waitTime(),
45 | 	}
46 | }
47 | 


--------------------------------------------------------------------------------
/dev-alpine.Dockerfile:
--------------------------------------------------------------------------------
 1 | # syntax=docker/dockerfile:1
 2 | #checkov:skip=CKV_DOCKER_2
 3 | #checkov:skip=CKV_DOCKER_3
 4 | FROM golang:1.24-alpine
 5 | 
 6 | ENV GOTOOLCHAIN=local
 7 | ENV CFLAGS="-ggdb3"
 8 | ENV PHPIZE_DEPS="\
 9 | 	autoconf \
10 | 	dpkg-dev \
11 | 	file \
12 | 	g++ \
13 | 	gcc \
14 | 	libc-dev \
15 | 	make \
16 | 	pkgconfig \
17 | 	re2c"
18 | 
19 | SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
20 | 
21 | RUN apk add --no-cache \
22 | 	$PHPIZE_DEPS \
23 | 	argon2-dev \
24 | 	brotli-dev \
25 | 	curl-dev \
26 | 	oniguruma-dev \
27 | 	readline-dev \
28 | 	libsodium-dev \
29 | 	sqlite-dev \
30 | 	openssl-dev \
31 | 	libxml2-dev \
32 | 	zlib-dev \
33 | 	bison \
34 | 	nss-tools \
35 | 	# file watcher
36 | 	libstdc++ \
37 | 	linux-headers \
38 | 	# Dev tools \
39 | 	git \
40 | 	clang \
41 | 	cmake \
42 | 	llvm \
43 | 	gdb \
44 | 	valgrind \
45 | 	neovim \
46 | 	zsh \
47 | 	libtool && \
48 | 	echo 'set auto-load safe-path /' > /root/.gdbinit
49 | 
50 | WORKDIR /usr/local/src/php
51 | RUN git clone --branch=PHP-8.4 https://github.com/php/php-src.git . && \
52 | 	# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
53 | 	./buildconf --force && \
54 | 	EXTENSION_DIR=/usr/lib/frankenphp/modules ./configure \
55 | 		--enable-embed \
56 | 		--enable-zts \
57 | 		--disable-zend-signals \
58 | 		--enable-zend-max-execution-timers \
59 | 		--with-config-file-path=/etc/frankenphp/php.ini \
60 | 		--with-config-file-scan-dir=/etc/frankenphp/php.d \
61 | 		--enable-debug && \
62 | 	make -j"$(nproc)" && \
63 | 	make install && \
64 | 	ldconfig /etc/ld.so.conf.d && \
65 | 		mkdir -p /etc/frankenphp/php.d && \
66 | 			cp php.ini-development /etc/frankenphp/php.ini && \
67 | 			echo "zend_extension=opcache.so" >> /etc/frankenphp/php.ini && \
68 | 			echo "opcache.enable=1" >> /etc/frankenphp/php.ini && \
69 | 	php --version
70 | 
71 | # Install e-dant/watcher (necessary for file watching)
72 | WORKDIR /usr/local/src/watcher
73 | RUN git clone https://github.com/e-dant/watcher . && \
74 |     cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
75 | 	cmake --build build/ && \
76 | 	cmake --install build
77 | 
78 | WORKDIR /go/src/app
79 | COPY . .
80 | 
81 | WORKDIR /go/src/app/caddy/frankenphp
82 | RUN go build
83 | 
84 | WORKDIR /go/src/app
85 | CMD [ "zsh" ]
86 | 


--------------------------------------------------------------------------------
/dev.Dockerfile:
--------------------------------------------------------------------------------
 1 | # syntax=docker/dockerfile:1
 2 | #checkov:skip=CKV_DOCKER_2
 3 | #checkov:skip=CKV_DOCKER_3
 4 | FROM golang:1.24
 5 | 
 6 | ENV GOTOOLCHAIN=local
 7 | ENV CFLAGS="-ggdb3"
 8 | ENV PHPIZE_DEPS="\
 9 | 	autoconf \
10 | 	dpkg-dev \
11 | 	file \
12 | 	g++ \
13 | 	gcc \
14 | 	libc-dev \
15 | 	make \
16 | 	pkg-config \
17 | 	re2c"
18 | 
19 | SHELL ["/bin/bash", "-o", "pipefail", "-c"]
20 | 
21 | # hadolint ignore=DL3009
22 | RUN apt-get update && \
23 | 	apt-get -y --no-install-recommends install \
24 | 	$PHPIZE_DEPS \
25 | 	libargon2-dev \
26 | 	libbrotli-dev \
27 | 	libcurl4-openssl-dev \
28 | 	libonig-dev \
29 | 	libreadline-dev \
30 | 	libsodium-dev \
31 | 	libsqlite3-dev \
32 | 	libssl-dev \
33 | 	libxml2-dev \
34 | 	zlib1g-dev \
35 | 	bison \
36 | 	libnss3-tools \
37 | 	# Dev tools \
38 | 	git \
39 | 	clang \
40 | 	cmake \
41 | 	llvm \
42 | 	gdb \
43 | 	valgrind \
44 | 	neovim \
45 | 	zsh \
46 | 	libtool-bin && \
47 | 	echo 'set auto-load safe-path /' > /root/.gdbinit && \
48 | 	echo '* soft core unlimited' >> /etc/security/limits.conf \
49 | 	&& \
50 | 	apt-get clean
51 | 
52 | WORKDIR /usr/local/src/php
53 | RUN git clone --branch=PHP-8.4 https://github.com/php/php-src.git . && \
54 | 	# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
55 | 	./buildconf --force && \
56 | 	EXTENSION_DIR=/usr/lib/frankenphp/modules ./configure \
57 | 		--enable-embed \
58 | 		--enable-zts \
59 | 		--disable-zend-signals \
60 | 		--enable-zend-max-execution-timers \
61 | 		--with-config-file-path=/etc/frankenphp/php.ini \
62 | 		--with-config-file-scan-dir=/etc/frankenphp/php.d \
63 | 		--enable-debug && \
64 | 	make -j"$(nproc)" && \
65 | 	make install && \
66 | 	ldconfig && \
67 | 		mkdir -p /etc/frankenphp/php.d && \
68 | 			cp php.ini-development /etc/frankenphp/php.ini && \
69 | 			echo "zend_extension=opcache.so" >> /etc/frankenphp/php.ini && \
70 | 			echo "opcache.enable=1" >> /etc/frankenphp/php.ini && \
71 | 	php --version
72 | 
73 | # Install e-dant/watcher (necessary for file watching)
74 | WORKDIR /usr/local/src/watcher
75 | RUN git clone https://github.com/e-dant/watcher . && \
76 | 	cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
77 | 	cmake --build build/ && \
78 | 	cmake --install build && \
79 | 	cp build/libwatcher-c.so /usr/local/lib/libwatcher-c.so && \
80 | 	ldconfig
81 | 
82 | WORKDIR /go/src/app
83 | COPY . .
84 | 
85 | WORKDIR /go/src/app/caddy/frankenphp
86 | RUN go build -buildvcs=false -tags 'nobadger,nomysql,nopgx'
87 | 
88 | WORKDIR /go/src/app
89 | CMD [ "zsh" ]
90 | 


--------------------------------------------------------------------------------
/docs/classic.md:
--------------------------------------------------------------------------------
 1 | # Using Classic Mode
 2 | 
 3 | Without any additional configuration, FrankenPHP operates in classic mode. In this mode, FrankenPHP functions like a traditional PHP server, directly serving PHP files. This makes it a seamless drop-in replacement for PHP-FPM or Apache with mod_php.
 4 | 
 5 | Similar to Caddy, FrankenPHP accepts an unlimited number of connections and uses a [fixed number of threads](config.md#caddyfile-config) to serve them. The number of accepted and queued connections is limited only by the available system resources.
 6 | The PHP thread pool operates with a fixed number of threads initialized at startup, comparable to the static mode of PHP-FPM. It's also possible to let threads [scale automatically at runtime](performance.md#max_threads), similar to the dynamic mode of PHP-FPM.
 7 | 
 8 | Queued connections will wait indefinitely until a PHP thread is available to serve them. To avoid this, you can use the max_wait_time [configuration](config.md#caddyfile-config) in FrankenPHP's global configuration to limit the duration a request can wait for a free PHP thread before being rejected.
 9 | Additionally, you can set a reasonable [write timeout in Caddy](https://caddyserver.com/docs/caddyfile/options#timeouts).
10 | 
11 | Each Caddy instance will only spin up one FrankenPHP thread pool, which will be shared across all `php_server` blocks.
12 | 


--------------------------------------------------------------------------------
/docs/cn/README.md:
--------------------------------------------------------------------------------
  1 | # FrankenPHP: 适用于 PHP 的现代应用服务器
  2 | 
  3 | <h1 align="center"><a href="https://frankenphp.dev"><img src="../../frankenphp.png" alt="FrankenPHP" width="600"></a></h1>
  4 | 
  5 | FrankenPHP 是建立在 [Caddy](https://caddyserver.com/) Web 服务器之上的现代 PHP 应用程序服务器。
  6 | 
  7 | FrankenPHP 凭借其令人惊叹的功能为你的 PHP 应用程序提供了超能力:[早期提示](early-hints.md)、[worker 模式](worker.md)、[实时功能](mercure.md)、自动 HTTPS、HTTP/2 和 HTTP/3 支持......
  8 | 
  9 | FrankenPHP 可与任何 PHP 应用程序一起使用,并且由于提供了与 worker 模式的集成,使你的 Symfony 和 Laravel 项目比以往任何时候都更快。
 10 | 
 11 | FrankenPHP 也可以用作独立的 Go 库,将 PHP 嵌入到任何使用 net/http 的应用程序中。
 12 | 
 13 | [**了解更多** _frankenphp.dev_](https://frankenphp.dev/cn/) 以及查看此演示文稿:
 14 | 
 15 | <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>
 16 | 
 17 | ## 开始
 18 | 
 19 | ### 独立二进制
 20 | 
 21 | 我们为 Linux 和 macOS 提供包含 [PHP 8.4](https://www.php.net/releases/8.4/zh.php) 以及大多数常用 PHP 扩展的 FrankenPHP 静态二进制文件。
 22 | 
 23 | 在 Windows 上,请使用 [WSL](https://learn.microsoft.com/windows/wsl/) 运行 FrankenPHP。
 24 | 
 25 | 你可以 [下载 FrankenPHP](https://github.com/dunglas/frankenphp/releases),或将以下命令复制到终端中,自动安装适用于你平台的版本:
 26 | 
 27 | ```console
 28 | curl https://frankenphp.dev/install.sh | sh
 29 | mv frankenphp /usr/local/bin/
 30 | ```
 31 | 
 32 | 要提供当前目录的内容,请运行:
 33 | 
 34 | ```console
 35 | frankenphp php-server
 36 | ```
 37 | 
 38 | 你还可以使用以下命令运行命令行脚本:
 39 | 
 40 | ```console
 41 | frankenphp php-cli /path/to/your/script.php
 42 | ```
 43 | 
 44 | ### Docker
 45 | 
 46 | 此外,还可以使用 [Docker 镜像](https://frankenphp.dev/docs/docker/):
 47 | 
 48 | ```console
 49 | docker run -v .:/app/public \
 50 |     -p 80:80 -p 443:443 -p 443:443/udp \
 51 |     dunglas/frankenphp
 52 | ```
 53 | 
 54 | 访问 `https://localhost`, 并享受吧!
 55 | 
 56 | > [!TIP]
 57 | >
 58 | > 不要尝试使用 `https://127.0.0.1`。使用 `https://localhost` 并接受自签名证书。
 59 | > 使用 [`SERVER_NAME` 环境变量](config.md#环境变量) 更改要使用的域。
 60 | 
 61 | ### Homebrew
 62 | 
 63 | FrankenPHP 也作为 [Homebrew](https://brew.sh) 软件包提供,适用于 macOS 和 Linux 系统。
 64 | 
 65 | 安装方法:
 66 | 
 67 | ```console
 68 | brew install dunglas/frankenphp/frankenphp
 69 | ```
 70 | 
 71 | 要提供当前目录的内容,请运行:
 72 | 
 73 | ```console
 74 | frankenphp php-server
 75 | ```
 76 | 
 77 | ## 文档
 78 | 
 79 | - [worker 模式](worker.md)
 80 | - [早期提示支持(103 HTTP status code)](early-hints.md)
 81 | - [实时功能](mercure.md)
 82 | - [配置](config.md)
 83 | - [Docker 镜像](docker.md)
 84 | - [在生产环境中部署](production.md)
 85 | - [创建独立、可自行执行的 PHP 应用程序](embed.md)
 86 | - [创建静态二进制文件](static.md)
 87 | - [从源代码编译](compile.md)
 88 | - [Laravel 集成](laravel.md)
 89 | - [已知问题](known-issues.md)
 90 | - [演示应用程序 (Symfony) 和性能测试](https://github.com/dunglas/frankenphp-demo)
 91 | - [Go 库文档](https://pkg.go.dev/github.com/dunglas/frankenphp)
 92 | - [贡献和调试](https://frankenphp.dev/docs/contributing/)
 93 | 
 94 | ## 示例和框架
 95 | 
 96 | - [Symfony](https://github.com/dunglas/symfony-docker)
 97 | - [API Platform](https://api-platform.com/docs/distribution/)
 98 | - [Laravel](laravel.md)
 99 | - [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
100 | - [WordPress](https://github.com/StephenMiracle/frankenwp)
101 | - [Drupal](https://github.com/dunglas/frankenphp-drupal)
102 | - [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
103 | - [TYPO3](https://github.com/ochorocho/franken-typo3)
104 | - [Magento2](https://github.com/ekino/frankenphp-magento2)
105 | 


--------------------------------------------------------------------------------
/docs/cn/compile.md:
--------------------------------------------------------------------------------
 1 | # 从源代码编译
 2 | 
 3 | 本文档解释了如何创建一个 FrankenPHP 构建,它将 PHP 加载为一个动态库。
 4 | 这是推荐的方法。
 5 | 
 6 | 或者,你也可以 [编译静态版本](static.md)。
 7 | 
 8 | ## 安装 PHP
 9 | 
10 | FrankenPHP 支持 PHP 8.2 及更高版本。
11 | 
12 | 首先,[获取 PHP 源代码](https://www.php.net/downloads.php) 并提取它们:
13 | 
14 | ```console
15 | tar xf php-*
16 | cd php-*/
17 | ```
18 | 
19 | 然后,为你的平台配置 PHP.
20 | 
21 | 这些参数是必需的,但你也可以添加其他编译参数(例如额外的扩展)。
22 | 
23 | ### Linux
24 | 
25 | ```console
26 | ./configure \
27 |     --enable-embed \
28 |     --enable-zts \
29 |     --disable-zend-signals \
30 |     --enable-zend-max-execution-timers
31 | ```
32 | 
33 | ### Mac
34 | 
35 | 使用 [Homebrew](https://brew.sh/) 包管理器安装 `libiconv`、`bison`、`re2c` 和 `pkg-config`:
36 | 
37 | ```console
38 | brew install libiconv bison re2c pkg-config
39 | echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
40 | ```
41 | 
42 | 然后运行 `./configure` 脚本:
43 | 
44 | ```console
45 | ./configure \
46 |     --enable-embed=static \
47 |     --enable-zts \
48 |     --disable-zend-signals \
49 |     --disable-opcache-jit \
50 |     --enable-static \
51 |     --enable-shared=no \
52 |     --with-iconv=/opt/homebrew/opt/libiconv/
53 | ```
54 | 
55 | ## 编译并安装 PHP
56 | 
57 | 最后,编译并安装 PHP:
58 | 
59 | ```console
60 | make -j"$(getconf _NPROCESSORS_ONLN)"
61 | sudo make install
62 | ```
63 | 
64 | ## 编译 Go 应用
65 | 
66 | 你现在可以使用 Go 库并编译我们的 Caddy 构建:
67 | 
68 | ```console
69 | curl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz
70 | cd frankenphp-main/caddy/frankenphp
71 | CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build
72 | ```
73 | 
74 | ### 使用 xcaddy
75 | 
76 | 你可以使用 [xcaddy](https://github.com/caddyserver/xcaddy) 来编译 [自定义 Caddy 模块](https://caddyserver.com/docs/modules/) 的 FrankenPHP:
77 | 
78 | ```console
79 | CGO_ENABLED=1 \
80 | XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'" \
81 | xcaddy build \
82 |     --output frankenphp \
83 |     --with github.com/dunglas/frankenphp/caddy \
84 |     --with github.com/dunglas/caddy-cbrotli \
85 |     --with github.com/dunglas/mercure/caddy \
86 |     --with github.com/dunglas/vulcain/caddy
87 |     # Add extra Caddy modules here
88 | ```
89 | 
90 | > [!TIP]
91 | >
92 | > 如果你的系统基于 musl libc(Alpine Linux 上默认使用)并搭配 Symfony 使用,
93 | > 你可能需要增加默认堆栈大小。
94 | > 否则,你可能会收到如下错误 `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`
95 | >
96 | > 请将 `XCADDY_GO_BUILD_FLAGS` 环境变量更改为如下类似的值
97 | > `XCADDY_GO_BUILD_FLAGS=
#39;-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'`
98 | > (根据你的应用需求更改堆栈大小)。
99 | 


--------------------------------------------------------------------------------
/docs/cn/early-hints.md:
--------------------------------------------------------------------------------
 1 | # 早期提示
 2 | 
 3 | FrankenPHP 原生支持 [103 Early Hints 状态码](https://developer.chrome.com/blog/early-hints/)。
 4 | 使用早期提示可以将网页的加载时间缩短 30%。
 5 | 
 6 | ```php
 7 | <?php
 8 | 
 9 | header('Link: </style.css>; rel=preload; as=style');
10 | headers_send(103);
11 | 
12 | // 慢速算法和 SQL 查询
13 | 
14 | echo <<<'HTML'
15 | <!DOCTYPE html>
16 | <title>Hello FrankenPHP</title>
17 | <link rel="stylesheet" href="style.css">
18 | HTML;
19 | ```
20 | 
21 | 早期提示由普通模式和 [worker](worker.md) 模式支持。
22 | 


--------------------------------------------------------------------------------
/docs/cn/embed.md:
--------------------------------------------------------------------------------
  1 | # PHP 应用程序作为独立二进制文件
  2 | 
  3 | FrankenPHP 能够将 PHP 应用程序的源代码和资源文件嵌入到静态的、独立的二进制文件中。
  4 | 
  5 | 由于这个特性,PHP 应用程序可以作为独立的二进制文件分发,包括应用程序本身、PHP 解释器和生产级 Web 服务器 Caddy。
  6 | 
  7 | 了解有关此功能的更多信息 [Kévin 在 SymfonyCon 上的演讲](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/)。
  8 | 
  9 | ## 准备你的应用
 10 | 
 11 | 在创建独立二进制文件之前,请确保应用已准备好进行打包。
 12 | 
 13 | 例如,你可能希望:
 14 | 
 15 | * 给应用安装生产环境的依赖
 16 | * 导出 autoloader
 17 | * 如果可能,为应用启用生产模式
 18 | * 丢弃不需要的文件,例如 `.git` 或测试文件,以减小最终二进制文件的大小
 19 | 
 20 | 例如,对于 Symfony 应用程序,你可以使用以下命令:
 21 | 
 22 | ```console
 23 | # 导出项目以避免 .git/ 等目录
 24 | mkdir $TMPDIR/my-prepared-app
 25 | git archive HEAD | tar -x -C $TMPDIR/my-prepared-app
 26 | cd $TMPDIR/my-prepared-app
 27 | 
 28 | # 设置适当的环境变量
 29 | echo APP_ENV=prod > .env.local
 30 | echo APP_DEBUG=0 >> .env.local
 31 | 
 32 | # 删除测试文件
 33 | rm -Rf tests/
 34 | 
 35 | # 安装依赖项
 36 | composer install --ignore-platform-reqs --no-dev -a
 37 | 
 38 | # 优化 .env
 39 | composer dump-env prod
 40 | ```
 41 | 
 42 | ## 创建 Linux 二进制文件
 43 | 
 44 | 创建 Linux 二进制文件的最简单方法是使用我们提供的基于 Docker 的构建器。
 45 | 
 46 | 1. 在准备好的应用的存储库中创建一个名为 `static-build.Dockerfile` 的文件。
 47 | 
 48 |     ```dockerfile
 49 |     FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
 50 | 
 51 |     # 复制应用代码
 52 |     WORKDIR /go/src/app/dist/app
 53 |     COPY . .
 54 | 
 55 |     # 构建静态二进制文件,只选择你需要的 PHP 扩展
 56 |     WORKDIR /go/src/app/
 57 |     RUN EMBED=dist/app/ \
 58 |         PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
 59 |         ./build-static.sh
 60 |     ```
 61 | 
 62 |     > [!CAUTION]
 63 |     >
 64 |     > 某些 .dockerignore 文件(例如默认的 [symfony-docker .dockerignore](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))会忽略 vendor
 65 |     > 文件夹和环境文件。在构建之前,请务必调整或删除 .dockerignore 文件。
 66 | 
 67 | 2. 构建:
 68 | 
 69 |     ```console
 70 |     docker build -t static-app -f static-build.Dockerfile .
 71 |     ```
 72 | 
 73 | 3. 提取二进制文件
 74 | 
 75 |     ```console
 76 |     docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp
 77 |     ```
 78 | 
 79 | 生成的二进制文件是当前目录中名为 `my-app` 的文件。
 80 | 
 81 | ## 为其他操作系统创建二进制文件
 82 | 
 83 | 如果你不想使用 Docker,或者想要构建 macOS 二进制文件,你可以使用我们提供的 shell 脚本:
 84 | 
 85 | ```console
 86 | git clone https://github.com/php/frankenphp
 87 | cd frankenphp
 88 | EMBED=/path/to/your/app \
 89 |     PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
 90 |     ./build-static.sh
 91 | ```
 92 | 
 93 | 在 `dist/` 目录中生成的二进制文件名称为 `frankenphp-<os>-<arch>`。
 94 | 
 95 | ## 使用二进制文件
 96 | 
 97 | 就是这样!`my-app` 文件(或其他操作系统上的 `dist/frankenphp-<os>-<arch>`)包含你的独立应用程序!
 98 | 
 99 | 若要启动 Web 应用,请执行:
100 | 
101 | ```console
102 | ./my-app php-server
103 | ```
104 | 
105 | 如果你的应用包含 [worker 脚本](worker.md),请使用如下命令启动 worker:
106 | 
107 | ```console
108 | ./my-app php-server --worker public/index.php
109 | ```
110 | 
111 | 要启用 HTTPS(自动创建 Let's Encrypt 证书)、HTTP/2 和 HTTP/3,请指定要使用的域名:
112 | 
113 | ```console
114 | ./my-app php-server --domain localhost
115 | ```
116 | 
117 | 你还可以运行二进制文件中嵌入的 PHP CLI 脚本:
118 | 
119 | ```console
120 | ./my-app php-cli bin/console
121 | ```
122 | 
123 | ## 自定义构建
124 | 
125 | [阅读静态构建文档](static.md) 查看如何自定义二进制文件(扩展、PHP 版本等)。
126 | 
127 | ## 分发二进制文件
128 | 
129 | 创建的二进制文件不会被压缩。
130 | 若要在发送文件之前减小文件的大小,可以对其进行压缩。
131 | 
132 | 我们推荐使用 `xz`。
133 | 


--------------------------------------------------------------------------------
/docs/cn/github-actions.md:
--------------------------------------------------------------------------------
 1 | # 使用 GitHub Actions
 2 | 
 3 | 此存储库构建 Docker 镜像并将其部署到 [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) 上
 4 | 每个批准的拉取请求或设置后在你自己的分支上。
 5 | 
 6 | ## 设置 GitHub Actions
 7 | 
 8 | 在存储库设置中的 `secrets` 下,添加以下字段:
 9 | 
10 | - `REGISTRY_LOGIN_SERVER`: 要使用的 docker registry(如 `docker.io`)。
11 | - `REGISTRY_USERNAME`: 用于登录 registry 的用户名(如 `dunglas`)。
12 | - `REGISTRY_PASSWORD`: 用于登录 registry 的密码(如 `access key`)。
13 | - `IMAGE_NAME`: 镜像的名称(如 `dunglas/frankenphp`)。
14 | 
15 | ## 构建和推送镜像
16 | 
17 | 1. 创建 Pull Request 或推送到你的 Fork 分支。
18 | 2. GitHub Actions 将生成镜像并运行每项测试。
19 | 3. 如果生成成功,则将使用 `pr-x` 推送 registry,其中 `x` 是 PR 编号,作为标记将镜像推送到注册表。
20 | 
21 | ## 部署镜像
22 | 
23 | 1. 合并 Pull Request 后,GitHub Actions 将再次运行测试并生成新镜像。
24 | 2. 如果构建成功,则 Docker 注册表中的 `main` tag 将更新。
25 | 
26 | ## 发布
27 | 
28 | 1. 在项目仓库中创建新 Tag。
29 | 2. GitHub Actions 将生成镜像并运行每项测试。
30 | 3. 如果构建成功,镜像将使用标记名称作为标记推送到 registry(例如,将创建 `v1.2.3` 和 `v1.2`)。
31 | 4. `latest` 标签也将更新。
32 | 


--------------------------------------------------------------------------------
/docs/cn/known-issues.md:
--------------------------------------------------------------------------------
 1 | # 已知问题
 2 | 
 3 | ## 不支持的 PHP 扩展
 4 | 
 5 | 已知以下扩展与 FrankenPHP 不兼容:
 6 | 
 7 | | 名称                                                          | 原因    | 替代方案                                                                                                                 |
 8 | |-------------------------------------------------------------|-------|----------------------------------------------------------------------------------------------------------------------|
 9 | | [imap](https://www.php.net/manual/en/imap.installation.php) | 非线程安全 | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) |
10 | 
11 | ## get_browser
12 | 
13 | [get_browser()](https://www.php.net/manual/en/function.get-browser.php) 函数在一段时间后似乎表现不佳。解决方法是缓存(例如使用 [APCu](https://www.php.net/manual/zh/book.apcu.php))每个 User-Agent,因为它们是不变的。
14 | 
15 | ## 独立的二进制和基于 Alpine 的 Docker 镜像
16 | 
17 | 独立的二进制文件和基于 Alpine 的 docker 镜像 (`dunglas/frankenphp:*-alpine`) 使用的是 [musl libc](https://musl.libc.org/) 而不是 [glibc and friends](https://www.etalabs.net/compare_libcs.html),为的是保持较小的二进制大小。
18 | 这可能会导致一些兼容性问题。特别是,glob 标志 `GLOB_BRACE` [不可用](https://www.php.net/manual/en/function.glob.php)。
19 | 
20 | ## 在 Docker 中使用 `https://127.0.0.1`
21 | 
22 | 默认情况下,FrankenPHP 会为 `localhost` 生成一个 TLS 证书。
23 | 这是本地开发最简单且推荐的选项。
24 | 
25 | 如果确实想使用 `127.0.0.1` 作为主机,可以通过将服务器名称设置为 `127.0.0.1` 来配置它以为其生成证书。
26 | 
27 | 如果你使用 Docker,因为 [Docker 网络](https://docs.docker.com/network/) 问题,只做这些是不够的。
28 | 你将收到类似于以下内容的 TLS 错误 `curl: (35) LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error`。
29 | 
30 | 如果你使用的是 Linux,解决方案是使用 [使用宿主机网络](https://docs.docker.com/network/network-tutorial-host/):
31 | 
32 | ```console
33 | docker run \
34 |     -e SERVER_NAME="127.0.0.1" \
35 |     -v $PWD:/app/public \
36 |     --network host \
37 |     dunglas/frankenphp
38 | ```
39 | 
40 | Mac 和 Windows 不支持 Docker 使用宿主机网络。在这些平台上,你必须猜测容器的 IP 地址并将其包含在服务器名称中。
41 | 
42 | 运行 `docker network inspect bridge` 并查看 `Containers`,找到 `IPv4Address` 当前分配的最后一个 IP 地址,并增加 1。如果没有容器正在运行,则第一个分配的 IP 地址通常为 `172.17.0.2`。
43 | 
44 | 然后将其包含在 `SERVER_NAME` 环境变量中:
45 | 
46 | ```console
47 | docker run \
48 |     -e SERVER_NAME="127.0.0.1, 172.17.0.3" \
49 |     -v $PWD:/app/public \
50 |     -p 80:80 -p 443:443 -p 443:443/udp \
51 |     dunglas/frankenphp
52 | ```
53 | 
54 | > [!CAUTION]
55 | >
56 | > 请务必将 `172.17.0.3` 替换为将分配给容器的 IP。
57 | 
58 | 你现在应该能够从主机访问 `https://127.0.0.1`。
59 | 
60 | 如果不是这种情况,请在调试模式下启动 FrankenPHP 以尝试找出问题:
61 | 
62 | ```console
63 | docker run \
64 |     -e CADDY_GLOBAL_OPTIONS="debug" \
65 |     -e SERVER_NAME="127.0.0.1" \
66 |     -v $PWD:/app/public \
67 |     -p 80:80 -p 443:443 -p 443:443/udp \
68 |     dunglas/frankenphp
69 | ```
70 | 


--------------------------------------------------------------------------------
/docs/cn/laravel.md:
--------------------------------------------------------------------------------
 1 | # Laravel
 2 | 
 3 | ## Docker
 4 | 
 5 | 使用 FrankenPHP 为 [Laravel](https://laravel.com) Web 应用程序提供服务就像将项目挂载到官方 Docker 镜像的 `/app` 目录中一样简单。
 6 | 
 7 | 从 Laravel 应用程序的主目录运行以下命令:
 8 | 
 9 | ```console
10 | docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
11 | ```
12 | 
13 | 尽情享受吧!
14 | 
15 | ## 本地安装
16 | 
17 | 或者,你可以从本地机器上使用 FrankenPHP 运行 Laravel 项目:
18 | 
19 | 1. [下载与你的系统相对应的二进制文件](https://github.com/php/frankenphp/releases)
20 | 2. 将以下配置添加到 Laravel 项目根目录中名为 `Caddyfile` 的文件中:
21 | 
22 |     ```caddyfile
23 |     {
24 |     	frankenphp
25 |     }
26 | 
27 |     # 服务器的域名
28 |     localhost {
29 |     	# 将 webroot 设置为 public/ 目录
30 |     	root public/
31 |     	# 启用压缩(可选)
32 |     	encode zstd br gzip
33 |     	# 执行当前目录中的 PHP 文件并提供资产
34 |     	php_server
35 |     }
36 |     ```
37 | 
38 | 3. 从 Laravel 项目的根目录启动 FrankenPHP:`frankenphp run`
39 | 
40 | ## Laravel Octane
41 | 
42 | Octane 可以通过 Composer 包管理器安装:
43 | 
44 | ```console
45 | composer require laravel/octane
46 | ```
47 | 
48 | 安装 Octane 后,你可以执行 `octane:install` Artisan 命令,该命令会将 Octane 的配置文件安装到你的应用程序中:
49 | 
50 | ```console
51 | php artisan octane:install --server=frankenphp
52 | ```
53 | 
54 | Octane 服务可以通过 `octane:frankenphp` Artisan 命令启动。
55 | 
56 | ```console
57 | php artisan octane:frankenphp
58 | ```
59 | 
60 | `octane:frankenphp` 命令可以采用以下选项:
61 | 
62 | * `--host`: 服务器应绑定到的 IP 地址(默认值: `127.0.0.1`)
63 | * `--port`: 服务器应可用的端口(默认值: `8000`)
64 | * `--admin-port`: 管理服务器应可用的端口(默认值: `2019`)
65 | * `--workers`: 应可用于处理请求的 worker 数(默认值: `auto`)
66 | * `--max-requests`: 在 worker 重启之前要处理的请求数(默认值: `500`)
67 | * `--caddyfile`: FrankenPHP `Caddyfile` 文件的路径
68 | * `--https`: 开启 HTTPS、HTTP/2 和 HTTP/3,自动生成和延长证书
69 | * `--http-redirect`: 启用 HTTP 到 HTTPS 重定向(仅在使用 `--https` 时启用)
70 | * `--watch`: 修改应用程序时自动重新加载服务器
71 | * `--poll`: 在监视时使用文件系统轮询,以便通过网络监视文件
72 | * `--log-level`: 在指定日志级别或高于指定日志级别的日志消息
73 | 
74 | 你可以了解更多关于 [Laravel Octane 官方文档](https://laravel.com/docs/octane)。
75 | 


--------------------------------------------------------------------------------
/docs/cn/mercure.md:
--------------------------------------------------------------------------------
 1 | # 实时
 2 | 
 3 | FrankenPHP 带有一个内置的 Mercure Hub!
 4 | Mercure 允许将事件实时推送到所有连接的设备:它们将立即收到 JavaScript 事件。
 5 | 
 6 | 无需 JS 库或 SDK!
 7 | 
 8 | ![Mercure](../mercure-hub.png)
 9 | 
10 | 要启用 Mercure Hub,请按照 [Mercure 网站](https://mercure.rocks/docs/hub/config) 中的说明更新 `Caddyfile`。
11 | 
12 | 要从你的代码中推送 Mercure 更新,我们推荐 [Symfony Mercure Component](https://symfony.com/components/Mercure)(不需要 Symfony 框架来使用)。
13 | 


--------------------------------------------------------------------------------
/docs/cn/static.md:
--------------------------------------------------------------------------------
  1 | # 创建静态构建
  2 | 
  3 | 基于 [static-php-cli](https://github.com/crazywhalecc/static-php-cli) 项目(这个项目支持所有 SAPI,不仅仅是 `cli`),
  4 | FrankenPHP 已支持创建静态二进制,无需安装本地 PHP。
  5 | 
  6 | 使用这种方法,我们可构建一个包含 PHP 解释器、Caddy Web 服务器和 FrankenPHP 的可移植二进制文件!
  7 | 
  8 | FrankenPHP 还支持 [将 PHP 应用程序嵌入到静态二进制文件中](embed.md)。
  9 | 
 10 | ## Linux
 11 | 
 12 | 我们提供了一个 Docker 镜像来构建 Linux 静态二进制文件:
 13 | 
 14 | ```console
 15 | docker buildx bake --load static-builder
 16 | docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder
 17 | ```
 18 | 
 19 | 生成的静态二进制文件名为 `frankenphp`,可在当前目录中找到。
 20 | 
 21 | 如果你想在没有 Docker 的情况下构建静态二进制文件,请查看 macOS 说明,它也适用于 Linux。
 22 | 
 23 | ### 自定义扩展
 24 | 
 25 | 默认情况下,大多数流行的 PHP 扩展都会被编译。
 26 | 
 27 | 若要减小二进制文件的大小并减少攻击面,可以选择使用 `PHP_EXTENSIONS` Docker 参数来自定义构建的扩展。
 28 | 
 29 | 例如,运行以下命令以生成仅包含 `opcache,pdo_sqlite` 扩展的二进制:
 30 | 
 31 | ```console
 32 | docker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder
 33 | # ...
 34 | ```
 35 | 
 36 | 若要将启用其他功能的库添加到已启用的扩展中,可以使用 `PHP_EXTENSION_LIBS` Docker 参数:
 37 | 
 38 | ```console
 39 | docker buildx bake \
 40 |   --load \
 41 |   --set static-builder.args.PHP_EXTENSIONS=gd \
 42 |   --set static-builder.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \
 43 |   static-builder
 44 | ```
 45 | 
 46 | ### 额外的 Caddy 模块
 47 | 
 48 | 要向 [xcaddy](https://github.com/caddyserver/xcaddy) 添加额外的 Caddy 模块或传递其他参数,请使用 `XCADDY_ARGS` Docker 参数:
 49 | 
 50 | ```console
 51 | docker buildx bake \
 52 |   --load \
 53 |   --set static-builder.args.XCADDY_ARGS="--with github.com/darkweak/souin/plugins/caddy --with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy" \
 54 |   static-builder
 55 | ```
 56 | 
 57 | 在本例中,我们为 Caddy 添加了 [Souin](https://souin.io) HTTP 缓存模块,以及 [cbrotli](https://github.com/dunglas/caddy-cbrotli)、[Mercure](https://mercure.rocks) 和 [Vulcain](https://vulcain.rocks) 模块。
 58 | 
 59 | > [!TIP]
 60 | >
 61 | > 如果 `XCADDY_ARGS` 为空或未设置,则默认包含 cbrotli、Mercure 和 Vulcain 模块。
 62 | > 如果自定义了 `XCADDY_ARGS` 的值,则必须显式地包含它们。
 63 | 
 64 | 参见:[自定义构建](#自定义构建)
 65 | 
 66 | ### GitHub Token
 67 | 
 68 | 如果遇到了 GitHub API 速率限制,请在 `GITHUB_TOKEN` 的环境变量中设置 GitHub Personal Access Token:
 69 | 
 70 | ```console
 71 | GITHUB_TOKEN="xxx" docker --load buildx bake static-builder
 72 | # ...
 73 | ```
 74 | 
 75 | ## macOS
 76 | 
 77 | 运行以下脚本以创建适用于 macOS 的静态二进制文件(需要先安装 [Homebrew](https://brew.sh/)):
 78 | 
 79 | ```console
 80 | git clone https://github.com/php/frankenphp
 81 | cd frankenphp
 82 | ./build-static.sh
 83 | ```
 84 | 
 85 | 注意:此脚本也适用于 Linux(可能也适用于其他 Unix 系统),我们提供的用于构建静态二进制的 Docker 镜像也在内部使用这个脚本。
 86 | 
 87 | ## 自定义构建
 88 | 
 89 | 以下环境变量可以传递给 `docker build` 和 `build-static.sh`
 90 | 脚本来自定义静态构建:
 91 | 
 92 | * `FRANKENPHP_VERSION`: 要使用的 FrankenPHP 版本
 93 | * `PHP_VERSION`: 要使用的 PHP 版本
 94 | * `PHP_EXTENSIONS`: 要构建的 PHP 扩展([支持的扩展列表](https://static-php.dev/zh/guide/extensions.html))
 95 | * `PHP_EXTENSION_LIBS`: 要构建的额外库,为扩展添加额外的功能
 96 | * `XCADDY_ARGS`:传递给 [xcaddy](https://github.com/caddyserver/xcaddy) 的参数,例如用于添加额外的 Caddy 模块
 97 | * `EMBED`: 要嵌入二进制文件的 PHP 应用程序的路径
 98 | * `CLEAN`: 设置后,libphp 及其所有依赖项都是重新构建的(不使用缓存)
 99 | * `DEBUG_SYMBOLS`: 设置后,调试符号将被保留在二进制文件内
100 | * `RELEASE`: (仅限维护者)设置后,生成的二进制文件将上传到 GitHub 上
101 | 


--------------------------------------------------------------------------------
/docs/cn/worker.md:
--------------------------------------------------------------------------------
  1 | # 使用 FrankenPHP Workers
  2 | 
  3 | 启动应用程序一次并将其保存在内存中。
  4 | FrankenPHP 将在几毫秒内处理传入的请求。
  5 | 
  6 | ## 启动 Worker 脚本
  7 | 
  8 | ### Docker
  9 | 
 10 | 将 `FRANKENPHP_CONFIG` 环境变量的值设置为 `worker /path/to/your/worker/script.php`:
 11 | 
 12 | ```console
 13 | docker run \
 14 |     -e FRANKENPHP_CONFIG="worker /app/path/to/your/worker/script.php" \
 15 |     -v $PWD:/app \
 16 |     -p 80:80 -p 443:443 -p 443:443/udp \
 17 |     dunglas/frankenphp
 18 | ```
 19 | 
 20 | ### 独立二进制
 21 | 
 22 | 使用 `php-server` 命令的 `--worker` 选项, 执行命令使当前目录的内容使用 worker:
 23 | 
 24 | ```console
 25 | frankenphp php-server --worker /path/to/your/worker/script.php
 26 | ```
 27 | 
 28 | ## Symfony Runtime
 29 | 
 30 | FrankenPHP 的 worker 模式由 [Symfony Runtime 组件](https://symfony.com/doc/current/components/runtime.html) 支持。
 31 | 要在 worker 中启动任何 Symfony 应用程序,请安装 [PHP Runtime](https://github.com/php-runtime/runtime) 的 FrankenPHP 软件包:
 32 | 
 33 | ```console
 34 | composer require runtime/frankenphp-symfony
 35 | ```
 36 | 
 37 | 通过定义 `APP_RUNTIME` 环境变量来启动你的应用服务器,以使用 FrankenPHP Symfony Runtime:
 38 | 
 39 | ```console
 40 | docker run \
 41 |     -e FRANKENPHP_CONFIG="worker ./public/index.php" \
 42 |     -e APP_RUNTIME=Runtime\\FrankenPhpSymfony\\Runtime \
 43 |     -v $PWD:/app \
 44 |     -p 80:80 -p 443:443 -p 443:443/udp \
 45 |     dunglas/frankenphp
 46 | ```
 47 | 
 48 | ## Laravel Octane
 49 | 
 50 | 请参阅 [文档](laravel.md#laravel-octane)。
 51 | 
 52 | ## 自定义应用程序
 53 | 
 54 | 以下示例演示如何在不依赖第三方库的情况下创建自己的 worker 脚本:
 55 | 
 56 | ```php
 57 | <?php
 58 | // public/index.php
 59 | 
 60 | // 防止在客户端连接中断时 worker 线程脚本终止
 61 | ignore_user_abort(true);
 62 | 
 63 | // 启动应用
 64 | require __DIR__.'/vendor/autoload.php';
 65 | 
 66 | $myApp = new \App\Kernel();
 67 | $myApp->boot();
 68 | 
 69 | // 循环外的处理程序以获得更好的性能(减少工作量)
 70 | $handler = static function () use ($myApp) {
 71 |     // 收到请求时调用
 72 |     // 超全局变量 php://input
 73 |     echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
 74 | };
 75 | for ($nbRequests = 0, $running = true; isset($_SERVER['MAX_REQUESTS']) && ($nbRequests < ((int)$_SERVER['MAX_REQUESTS'])) && $running; ++$nbRequests) {
 76 |     $running = \frankenphp_handle_request($handler);
 77 | 
 78 |     // 发送 HTTP 响应后执行某些操作
 79 |     $myApp->terminate();
 80 | 
 81 |     // 调用垃圾回收器以减少在页面生成过程中触发垃圾回收器的几率
 82 |     gc_collect_cycles();
 83 | }
 84 | // 结束清理
 85 | $myApp->shutdown();
 86 | ```
 87 | 
 88 | 然后,启动应用并使用 `FRANKENPHP_CONFIG` 环境变量来配置你的 worker:
 89 | 
 90 | ```console
 91 | docker run \
 92 |     -e FRANKENPHP_CONFIG="worker ./public/index.php" \
 93 |     -v $PWD:/app \
 94 |     -p 80:80 -p 443:443 -p 443:443/udp \
 95 |     dunglas/frankenphp
 96 | ```
 97 | 
 98 | 默认情况下,每个 CPU 启动一个 worker。
 99 | 你还可以配置要启动的 worker 数:
100 | 
101 | ```console
102 | docker run \
103 |     -e FRANKENPHP_CONFIG="worker ./public/index.php 42" \
104 |     -v $PWD:/app \
105 |     -p 80:80 -p 443:443 -p 443:443/udp \
106 |     dunglas/frankenphp
107 | ```
108 | 
109 | ### 在一定数量的请求后重新启动 Worker
110 | 
111 | 由于 PHP 最初不是为长时间运行的进程而设计的,因此仍然有许多库和遗留代码会发生内存泄露。
112 | 在 worker 模式下使用此类代码的解决方法是在处理一定数量的请求后重新启动 worker 程序脚本:
113 | 
114 | 前面的 worker 代码段允许通过设置名为 `MAX_REQUESTS` 的环境变量来配置要处理的最大请求数。
115 | 


--------------------------------------------------------------------------------
/docs/digitalocean-dns.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php/frankenphp/34fbfd467b264dd75e5b02534aceec179217fb35/docs/digitalocean-dns.png


--------------------------------------------------------------------------------
/docs/digitalocean-droplet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php/frankenphp/34fbfd467b264dd75e5b02534aceec179217fb35/docs/digitalocean-droplet.png


--------------------------------------------------------------------------------
/docs/early-hints.md:
--------------------------------------------------------------------------------
 1 | # Early Hints
 2 | 
 3 | FrankenPHP natively supports the [103 Early Hints status code](https://developer.chrome.com/blog/early-hints/).
 4 | Using Early Hints can improve the load time of your web pages by 30%.
 5 | 
 6 | ```php
 7 | <?php
 8 | 
 9 | header('Link: </style.css>; rel=preload; as=style');
10 | headers_send(103);
11 | 
12 | // your slow algorithms and SQL queries 🤪
13 | 
14 | echo <<<'HTML'
15 | <!DOCTYPE html>
16 | <title>Hello FrankenPHP</title>
17 | <link rel="stylesheet" href="style.css">
18 | HTML;
19 | ```
20 | 
21 | Early Hints are supported both by the normal and the [worker](worker.md) modes.
22 | 


--------------------------------------------------------------------------------
/docs/fr/classic.md:
--------------------------------------------------------------------------------
 1 | # Utilisation du mode classique
 2 | 
 3 | Sans aucune configuration additionnelle, FrankenPHP fonctionne en mode classique. Dans ce mode, FrankenPHP fonctionne comme un serveur PHP traditionnel, en servant directement les fichiers PHP. Cela en fait un remplaçant parfait à PHP-FPM ou Apache avec mod_php.
 4 | 
 5 | Comme Caddy, FrankenPHP accepte un nombre illimité de connexions et utilise un [nombre fixe de threads](config.md#configuration-du-caddyfile) pour les servir. Le nombre de connexions acceptées et en attente n'est limité que par les ressources système disponibles.
 6 | Le pool de threads PHP fonctionne avec un nombre fixe de threads initialisés au démarrage, comparable au mode statique de PHP-FPM. Il est également possible de laisser les threads [s'adapter automatiquement à l'exécution](performance.md#max_threads), comme dans le mode dynamique de PHP-FPM.
 7 | 
 8 | Les connexions en file d'attente attendront indéfiniment jusqu'à ce qu'un thread PHP soit disponible pour les servir. Pour éviter cela, vous pouvez utiliser la [configuration](config.md#configuration-du-caddyfile) `max_wait_time` dans la configuration globale de FrankenPHP pour limiter la durée pendant laquelle une requête peut attendre un thread PHP libre avant d'être rejetée.
 9 | En outre, vous pouvez définir un [délai d'écriture dans Caddy](https://caddyserver.com/docs/caddyfile/options#timeouts) raisonnable.
10 | 
11 | Chaque instance de Caddy n'utilisera qu'un seul pool de threads FrankenPHP, qui sera partagé par tous les blocs `php_server`.
12 | 


--------------------------------------------------------------------------------
/docs/fr/early-hints.md:
--------------------------------------------------------------------------------
 1 | # Early Hints
 2 | 
 3 | FrankenPHP prend nativement en charge le code de statut [103 Early Hints](https://developer.chrome.com/blog/early-hints/).
 4 | L'utilisation des Early Hints peut améliorer le temps de chargement de vos pages web de 30 %.
 5 | 
 6 | ```php
 7 | <?php
 8 | 
 9 | header('Link: </style.css>; rel=preload; as=style');
10 | headers_send(103);
11 | 
12 | // vos algorithmes lents et requêtes SQL 🤪
13 | 
14 | echo <<<'HTML'
15 | <!DOCTYPE html>
16 | <title>Hello FrankenPHP</title>
17 | <link rel="stylesheet" href="style.css">
18 | HTML;
19 | ```
20 | 
21 | Les Early Hints sont pris en charge à la fois par les modes "standard" et [worker](worker.md).
22 | 


--------------------------------------------------------------------------------
/docs/fr/github-actions.md:
--------------------------------------------------------------------------------
 1 | # Utilisation de GitHub Actions
 2 | 
 3 | Ce dépôt construit et déploie l'image Docker sur [le Hub Docker](https://hub.docker.com/r/dunglas/frankenphp) pour
 4 | chaque pull request approuvée ou sur votre propre fork une fois configuré.
 5 | 
 6 | ## Configuration de GitHub Actions
 7 | 
 8 | Dans les paramètres du dépôt, sous "secrets", ajoutez les secrets suivants :
 9 | 
10 | - `REGISTRY_LOGIN_SERVER` : Le registre Docker à utiliser (par exemple, `docker.io`).
11 | - `REGISTRY_USERNAME` : Le nom d'utilisateur à utiliser pour se connecter au registre (par exemple, `dunglas`).
12 | - `REGISTRY_PASSWORD` : Le mot de passe à utiliser pour se connecter au registre (par exemple, une clé d'accès).
13 | - `IMAGE_NAME` : Le nom de l'image (par exemple, `dunglas/frankenphp`).
14 | 
15 | ## Construction et push de l'image
16 | 
17 | 1. Créez une Pull Request ou poussez vers votre fork.
18 | 2. GitHub Actions va construire l'image et exécuter tous les tests.
19 | 3. Si la construction est réussie, l'image sera poussée vers le registre en utilisant le tag `pr-x`, où `x` est le numéro de la PR.
20 | 
21 | ## Déploiement de l'image
22 | 
23 | 1. Une fois la Pull Request fusionnée, GitHub Actions exécutera à nouveau les tests et construira une nouvelle image.
24 | 2. Si la construction est réussie, le tag `main` sera mis à jour dans le registre Docker.
25 | 
26 | ## Releases
27 | 
28 | 1. Créez un nouveau tag dans le dépôt.
29 | 2. GitHub Actions va construire l'image et exécuter tous les tests.
30 | 3. Si la compilation est réussie, l'image sera poussée vers le registre en utilisant le nom du tag comme tag (par exemple, `v1.2.3` et `v1.2` seront créés).
31 | 4. Le tag `latest` sera également mis à jour.
32 | 


--------------------------------------------------------------------------------
/docs/fr/mercure.md:
--------------------------------------------------------------------------------
 1 | # Temps Réel
 2 | 
 3 | FrankenPHP est livré avec un hub [Mercure](https://mercure.rocks) intégré.
 4 | Mercure permet de pousser des événements en temps réel vers tous les appareils connectés : ils recevront un événement JavaScript instantanément.
 5 | 
 6 | Aucune bibliothèque JS ou SDK requis !
 7 | 
 8 | ![Mercure](../mercure-hub.png)
 9 | 
10 | Pour activer le hub Mercure, mettez à jour le `Caddyfile` comme décrit [sur le site de Mercure](https://mercure.rocks/docs/hub/config).
11 | 
12 | Pour pousser des mises à jour Mercure depuis votre code, nous recommandons le [Composant Mercure de Symfony](https://symfony.com/components/Mercure) (vous n'avez pas besoin du framework full stack Symfony pour l'utiliser).
13 | 


--------------------------------------------------------------------------------
/docs/fr/metrics.md:
--------------------------------------------------------------------------------
 1 | # Métriques
 2 | 
 3 | Lorsque les [métriques Caddy](https://caddyserver.com/docs/metrics) sont activées, FrankenPHP expose les métriques suivantes :
 4 | 
 5 | - `frankenphp_total_threads` : Le nombre total de threads PHP.
 6 | - `frankenphp_busy_threads` : Le nombre de threads PHP en cours de traitement d'une requête (les workers en cours d'exécution consomment toujours un thread).
 7 | - `frankenphp_queue_depth` : Le nombre de requêtes régulières en file d'attente
 8 | - `frankenphp_total_workers{worker=« [nom_du_worker] »}` : Le nombre total de workers.
 9 | - `frankenphp_busy_workers{worker=« [nom_du_worker] »}` : Le nombre de workers qui traitent actuellement une requête.
10 | - `frankenphp_worker_request_time{worker=« [nom_du_worker] »}` : Le temps passé à traiter les requêtes par tous les workers.
11 | - `frankenphp_worker_request_count{worker=« [nom_du_worker] »}` : Le nombre de requêtes traitées par tous les workers.
12 | - `frankenphp_ready_workers{worker=« [nom_du_worker] »}` : Le nombre de workers qui ont appelé `frankenphp_handle_request` au moins une fois.
13 | - `frankenphp_worker_crashes{worker=« [nom_du_worker] »}` : Le nombre de fois où un worker s'est arrêté de manière inattendue.
14 | - `frankenphp_worker_restarts{worker=« [nom_du_worker] »}` : Le nombre de fois où un worker a été délibérément redémarré.
15 | - `frankenphp_worker_queue_depth{worker=« [nom_du_worker] »}` : Le nombre de requêtes en file d'attente.
16 | 
17 | Pour les métriques de worker, le placeholder `[nom_du_worker]` est remplacé par le nom du worker dans le Caddyfile, sinon le chemin absolu du fichier du worker sera utilisé.
18 | 


--------------------------------------------------------------------------------
/docs/fr/x-sendfile.md:
--------------------------------------------------------------------------------
 1 | # Servir efficacement les gros fichiers statiques (`X-Sendfile`/`X-Accel-Redirect`)
 2 | 
 3 | Habituellement, les fichiers statiques peuvent être servis directement par le serveur web,
 4 | mais parfois, il est nécessaire d'exécuter du code PHP avant de les envoyer :
 5 | contrôle d'accès, statistiques, en-têtes HTTP personnalisés...
 6 | 
 7 | Malheureusement, utiliser PHP pour servir de gros fichiers statiques est inefficace comparé à
 8 | à l'utilisation directe du serveur web (surcharge mémoire, diminution des performances...).
 9 | 
10 | FrankenPHP permet de déléguer l'envoi des fichiers statiques au serveur web
11 | **après** avoir exécuté du code PHP personnalisé.
12 | 
13 | Pour ce faire, votre application PHP n'a qu'à définir un en-tête HTTP personnalisé
14 | contenant le chemin du fichier à servir. FrankenPHP se chargera du reste.
15 | 
16 | Cette fonctionnalité est connue sous le nom de **`X-Sendfile`** pour Apache, et **`X-Accel-Redirect`** pour NGINX.
17 | 
18 | Dans les exemples suivants, nous supposons que le "document root" du projet est le répertoire `public/`
19 | et que nous voulons utiliser PHP pour servir des fichiers stockés en dehors du dossier `public/`,
20 | depuis un répertoire nommé `private-files/`.
21 | 
22 | ## Configuration
23 | 
24 | Tout d'abord, ajoutez la configuration suivante à votre `Caddyfile` pour activer cette fonctionnalité :
25 | 
26 | ```patch
27 | 	root public/
28 | 	# ...
29 | 
30 | +	# Needed for Symfony, Laravel and other projects using the Symfony HttpFoundation component
31 | +	request_header X-Sendfile-Type x-accel-redirect
32 | +	request_header X-Accel-Mapping ../private-files=/private-files
33 | +
34 | +	intercept {
35 | +		@accel header X-Accel-Redirect *
36 | +		handle_response @accel {
37 | +			root private-files/
38 | +			rewrite * {resp.header.X-Accel-Redirect}
39 | +			method * GET
40 | +
41 | +			# Remove the X-Accel-Redirect header set by PHP for increased security
42 | +			header -X-Accel-Redirect
43 | +
44 | +			file_server
45 | +		}
46 | +	}
47 | 
48 | 	php_server
49 | ```
50 | 
51 | ## PHP simple
52 | 
53 | Définissez le chemin relatif du fichier (à partir de `private-files/`) comme valeur de l'en-tête `X-Accel-Redirect` :
54 | 
55 | ```php
56 | header('X-Accel-Redirect: file.txt') ;
57 | ```
58 | 
59 | ## Projets utilisant le composant Symfony HttpFoundation (Symfony, Laravel, Drupal...)
60 | 
61 | Symfony HttpFoundation [supporte nativement cette fonctionnalité](https://symfony.com/doc/current/components/http_foundation.html#serving-files).
62 | Il va automatiquement déterminer la bonne valeur pour l'en-tête `X-Accel-Redirect` et l'ajoutera à la réponse.
63 | 
64 | ```php
65 | use Symfony\Component\HttpFoundation\BinaryFileResponse;
66 | 
67 | BinaryFileResponse::trustXSendfileTypeHeader();
68 | $response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt');
69 | 
70 | // ...
71 | ```
72 | 


--------------------------------------------------------------------------------
/docs/github-actions.md:
--------------------------------------------------------------------------------
 1 | # Using GitHub Actions
 2 | 
 3 | This repository builds and deploys the Docker image to [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) on
 4 | every approved pull request or on your own fork once setup.
 5 | 
 6 | ## Setting up GitHub Actions
 7 | 
 8 | In the repository settings, under secrets, add the following secrets:
 9 | 
10 | - `REGISTRY_LOGIN_SERVER`: The docker registry to use (e.g. `docker.io`).
11 | - `REGISTRY_USERNAME`: The username to use to log in to the registry (e.g. `dunglas`).
12 | - `REGISTRY_PASSWORD`: The password to use to log in to the registry (e.g. an access key).
13 | - `IMAGE_NAME`: The name of the image (e.g. `dunglas/frankenphp`).
14 | 
15 | ## Building and Pushing the Image
16 | 
17 | 1. Create a Pull Request or push to your fork.
18 | 2. GitHub Actions will build the image and run any tests.
19 | 3. If the build is successful, the image will be pushed to the registry using the `pr-x`, where `x` is the PR number, as the tag.
20 | 
21 | ## Deploying the Image
22 | 
23 | 1. Once the Pull Request is merged, GitHub Actions will again run the tests and build a new image.
24 | 2. If the build is successful, the `main` tag will be updated in the Docker registry.
25 | 
26 | ## Releases
27 | 
28 | 1. Create a new tag in the repository.
29 | 2. GitHub Actions will build the image and run any tests.
30 | 3. If the build is successful, the image will be pushed to the registry using the tag name as the tag (e.g. `v1.2.3` and `v1.2` will be created).
31 | 4. The `latest` tag will also be updated.
32 | 


--------------------------------------------------------------------------------
/docs/mercure-hub.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php/frankenphp/34fbfd467b264dd75e5b02534aceec179217fb35/docs/mercure-hub.png


--------------------------------------------------------------------------------
/docs/mercure.md:
--------------------------------------------------------------------------------
 1 | # Real-time
 2 | 
 3 | FrankenPHP comes with a built-in [Mercure](https://mercure.rocks) hub!
 4 | Mercure allows you to push real-time events to all the connected devices: they will receive a JavaScript event instantly.
 5 | 
 6 | No JS library or SDK is required!
 7 | 
 8 | ![Mercure](mercure-hub.png)
 9 | 
10 | To enable the Mercure hub, update the `Caddyfile` as described [on Mercure's site](https://mercure.rocks/docs/hub/config).
11 | 
12 | The path of the Mercure hub is `/.well-known/mercure`.
13 | When running FrankenPHP inside Docker, the full send URL would look like `http://php/.well-known/mercure` (with `php` being the container's name running FrankenPHP).
14 | 
15 | To push Mercure updates from your code, we recommend the [Symfony Mercure Component](https://symfony.com/components/Mercure) (you don't need the Symfony full-stack framework to use it).
16 | 


--------------------------------------------------------------------------------
/docs/metrics.md:
--------------------------------------------------------------------------------
 1 | # Metrics
 2 | 
 3 | When [Caddy metrics](https://caddyserver.com/docs/metrics) are enabled, FrankenPHP exposes the following metrics:
 4 | 
 5 | - `frankenphp_total_threads`: The total number of PHP threads.
 6 | - `frankenphp_busy_threads`: The number of PHP threads currently processing a request (running workers always consume a thread).
 7 | - `frankenphp_queue_depth`: The number of regular queued requests
 8 | - `frankenphp_total_workers{worker="[worker_name]"}`: The total number of workers.
 9 | - `frankenphp_busy_workers{worker="[worker_name]"}`: The number of workers currently processing a request.
10 | - `frankenphp_worker_request_time{worker="[worker_name]"}`: The time spent processing requests by all workers.
11 | - `frankenphp_worker_request_count{worker="[worker_name]"}`: The number of requests processed by all workers.
12 | - `frankenphp_ready_workers{worker="[worker_name]"}`: The number of workers that have called `frankenphp_handle_request` at least once.
13 | - `frankenphp_worker_crashes{worker="[worker_name]"}`: The number of times a worker has unexpectedly terminated.
14 | - `frankenphp_worker_restarts{worker="[worker_name]"}`: The number of times a worker has been deliberately restarted.
15 | - `frankenphp_worker_queue_depth{worker="[worker_name]"}`: The number of queued requests.
16 | 
17 | For worker metrics, the `[worker_name]` placeholder is replaced by the worker name in the Caddyfile, otherwise absolute path of worker file will be used.
18 | 


--------------------------------------------------------------------------------
/docs/ru/early-hints.md:
--------------------------------------------------------------------------------
 1 | # Early Hints
 2 | 
 3 | FrankenPHP изначально поддерживает [Early Hints (103 HTTP статус код)](https://developer.chrome.com/blog/early-hints/).  
 4 | Использование Early Hints может улучшить время загрузки ваших веб-страниц на 30%.
 5 | 
 6 | ```php
 7 | <?php
 8 | 
 9 | header('Link: </style.css>; rel=preload; as=style');
10 | headers_send(103);
11 | 
12 | // ваши медленные алгоритмы и SQL-запросы 🤪
13 | 
14 | echo <<<'HTML'
15 | <!DOCTYPE html>
16 | <title>Hello FrankenPHP</title>
17 | <link rel="stylesheet" href="style.css">
18 | HTML;
19 | ```
20 | 
21 | Early Hints поддерживается как в обычном, так и в [worker режиме](worker.md).
22 | 


--------------------------------------------------------------------------------
/docs/ru/github-actions.md:
--------------------------------------------------------------------------------
 1 | # Использование GitHub Actions
 2 | 
 3 | Этот репозиторий автоматически собирает и публикует Docker-образы в [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) для каждого одобренного pull request или вашего собственного форка после настройки.
 4 | 
 5 | ## Настройка GitHub Actions
 6 | 
 7 | В настройках репозитория, в разделе "Secrets", добавьте следующие секреты:
 8 | 
 9 | - `REGISTRY_LOGIN_SERVER`: Docker-реестр, который будет использоваться (например, `docker.io`).
10 | - `REGISTRY_USERNAME`: Имя пользователя для входа в реестр (например, `dunglas`).
11 | - `REGISTRY_PASSWORD`: Пароль для входа в реестр (например, токен доступа).
12 | - `IMAGE_NAME`: Имя образа (например, `dunglas/frankenphp`).
13 | 
14 | ## Сборка и загрузка образа
15 | 
16 | 1. Создайте Pull Request или выполните push в ваш форк.
17 | 2. GitHub Actions соберёт образ и выполнит тесты.
18 | 3. Если сборка пройдёт успешно, образ будет отправлен в реестр с тегом `pr-x`, где `x` — номер PR.
19 | 
20 | ## Развёртывание образа
21 | 
22 | 1. После слияния Pull Request GitHub Actions выполнит повторные тесты и соберёт новый образ.
23 | 2. Если сборка пройдёт успешно, тег `main` будет обновлён в Docker-реестре.
24 | 
25 | ## Релизы
26 | 
27 | 1. Создайте новый тег в репозитории.
28 | 2. GitHub Actions соберёт образ и выполнит тесты.
29 | 3. Если сборка пройдёт успешно, образ будет отправлен в реестр с именем тега (например, `v1.2.3` и `v1.2` будут созданы).
30 | 4. Также будет обновлён тег `latest`.
31 | 


--------------------------------------------------------------------------------
/docs/ru/mercure.md:
--------------------------------------------------------------------------------
 1 | # Real-time режим
 2 | 
 3 | FrankenPHP поставляется с встроенным хабом [Mercure](https://mercure.rocks)!  
 4 | Mercure позволяет отправлять события в режиме реального времени на все подключённые устройства: они мгновенно получат JavaScript-событие.
 5 | 
 6 | Не требуются JS-библиотеки или SDK!
 7 | 
 8 | ![Mercure](../mercure-hub.png)
 9 | 
10 | Чтобы включить хаб Mercure, обновите `Caddyfile` в соответствии с инструкциями [на сайте Mercure](https://mercure.rocks/docs/hub/config).
11 | 
12 | Для отправки обновлений Mercure из вашего кода мы рекомендуем использовать [Symfony Mercure Component](https://symfony.com/components/Mercure) (для его использования не требуется полный стек Symfony).
13 | 


--------------------------------------------------------------------------------
/docs/ru/metrics.md:
--------------------------------------------------------------------------------
 1 | # Метрики
 2 | 
 3 | При включении [метрик Caddy](https://caddyserver.com/docs/metrics) FrankenPHP предоставляет следующие метрики:
 4 | 
 5 | - `frankenphp_[worker]_total_workers`: Общее количество worker-скриптов.
 6 | - `frankenphp_[worker]_busy_workers`: Количество worker-скриптов, которые в данный момент обрабатывают запрос.
 7 | - `frankenphp_[worker]_worker_request_time`: Время, затраченное всеми worker-скриптами на обработку запросов.
 8 | - `frankenphp_[worker]_worker_request_count`: Количество запросов, обработанных всеми worker-скриптами.
 9 | - `frankenphp_[worker]_ready_workers`: Количество worker-скриптов, которые вызвали `frankenphp_handle_request` хотя бы один раз.
10 | - `frankenphp_[worker]_worker_crashes`: Количество случаев неожиданного завершения worker-скриптов.
11 | - `frankenphp_[worker]_worker_restarts`: Количество случаев, когда worker-скрипт был перезапущен целенаправленно.
12 | - `frankenphp_total_threads`: Общее количество потоков PHP.
13 | - `frankenphp_busy_threads`: Количество потоков PHP, которые в данный момент обрабатывают запрос (работающие worker-скрипты всегда используют поток).
14 | 
15 | Для метрик worker-скриптов плейсхолдер `[worker]` заменяется на путь к Worker-скрипту, указанному в Caddyfile.
16 | 


--------------------------------------------------------------------------------
/docs/tr/README.md:
--------------------------------------------------------------------------------
 1 | # FrankenPHP: PHP için Modern Uygulama Sunucusu
 2 | 
 3 | <h1 align="center"><a href="https://frankenphp.dev"><img src="../../frankenphp.png" alt="FrankenPHP" width="600"></a></h1>
 4 | 
 5 | FrankenPHP, [Caddy](https://caddyserver.com/) web sunucusunun üzerine inşa edilmiş PHP için modern bir uygulama sunucusudur.
 6 | 
 7 | FrankenPHP, çarpıcı özellikleri sayesinde PHP uygulamalarınıza süper güçler kazandırır: [Early Hints\*](https://frankenphp.dev/docs/early-hints/), [worker modu](https://frankenphp.dev/docs/worker/), [real-time yetenekleri](https://frankenphp.dev/docs/mercure/), otomatik HTTPS, HTTP/2 ve HTTP/3 desteği...
 8 | 
 9 | FrankenPHP herhangi bir PHP uygulaması ile çalışır ve worker modu ile resmi entegrasyonları sayesinde Laravel ve Symfony projelerinizi her zamankinden daha performanslı hale getirir.
10 | 
11 | FrankenPHP, PHP'yi `net/http` kullanarak herhangi bir uygulamaya yerleştirmek için bağımsız bir Go kütüphanesi olarak da kullanılabilir.
12 | 
13 | [_Frankenphp.dev_](https://frankenphp.dev) adresinden ve bu slayt üzerinden daha fazlasını öğrenin:
14 | 
15 | <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>
16 | 
17 | ## Başlarken
18 | 
19 | ### Docker
20 | 
21 | ```console
22 | docker run -v $PWD:/app/public \
23 |     -p 80:80 -p 443:443 -p 443:443/udp \
24 |     dunglas/frankenphp
25 | ```
26 | 
27 | `https://localhost` adresine gidin ve keyfini çıkarın!
28 | 
29 | > [!TIP]
30 | >
31 | > `https://127.0.0.1` kullanmaya çalışmayın. `https://localhost` kullanın ve kendinden imzalı sertifikayı kabul edin.
32 | > Kullanılacak alan adını değiştirmek için [`SERVER_NAME` ortam değişkenini](https://frankenphp.dev/tr/docs/config#ortam-değişkenleri) kullanın.
33 | 
34 | ### Binary Çıktısı
35 | 
36 | Docker kullanmayı tercih etmiyorsanız, Linux ve macOS için bağımsız FrankenPHP binary dosyası sağlıyoruz
37 | [PHP 8.4](https://www.php.net/releases/8.4/en.php) ve en popüler PHP eklentilerini de içermekte: [FrankenPHP](https://github.com/php/frankenphp/releases) indirin
38 | 
39 | Geçerli dizinin içeriğini başlatmak için çalıştırın:
40 | 
41 | ```console
42 | frankenphp php-server
43 | ```
44 | 
45 | Ayrıca aşağıdaki tek komut satırı ile de çalıştırabilirsiniz:
46 | 
47 | ```console
48 | frankenphp php-cli /path/to/your/script.php
49 | ```
50 | 
51 | ## Docs
52 | 
53 | - [Worker modu](worker.md)
54 | - [Early Hints desteği (103 HTTP durum kodu)](early-hints.md)
55 | - [Real-time](mercure.md)
56 | - [Konfigürasyon](config.md)
57 | - [Docker imajları](docker.md)
58 | - [Production'a dağıtım](production.md)
59 | - [**Bağımsız** kendiliğinden çalıştırılabilir PHP uygulamaları oluşturma](embed.md)
60 | - [Statik binary'leri oluşturma](static.md)
61 | - [Kaynak dosyalarından derleme](config.md)
62 | - [Laravel entegrasyonu](laravel.md)
63 | - [Bilinen sorunlar](known-issues.md)
64 | - [Demo uygulama (Symfony) ve kıyaslamalar](https://github.com/dunglas/frankenphp-demo)
65 | - [Go kütüphane dokümantasonu](https://pkg.go.dev/github.com/dunglas/frankenphp)
66 | - [Katkıda bulunma ve hata ayıklama](CONTRIBUTING.md)
67 | 
68 | ## Örnekler ve İskeletler
69 | 
70 | - [Symfony](https://github.com/dunglas/symfony-docker)
71 | - [API Platform](https://api-platform.com/docs/distribution/)
72 | - [Laravel](https://frankenphp.dev/docs/laravel/)
73 | - [Sulu](https://sulu.io/blog/running-sulu-with-frankenphp)
74 | - [WordPress](https://github.com/StephenMiracle/frankenwp)
75 | - [Drupal](https://github.com/dunglas/frankenphp-drupal)
76 | - [Joomla](https://github.com/alexandreelise/frankenphp-joomla)
77 | - [TYPO3](https://github.com/ochorocho/franken-typo3)
78 | - [Magento2](https://github.com/ekino/frankenphp-magento2)
79 | 


--------------------------------------------------------------------------------
/docs/tr/compile.md:
--------------------------------------------------------------------------------
  1 | # Kaynak Kodlardan Derleme
  2 | 
  3 | Bu doküman, PHP'yi dinamik bir kütüphane olarak yükleyecek bir FrankenPHP yapısının nasıl oluşturulacağını açıklamaktadır.
  4 | Önerilen yöntem bu şekildedir.
  5 | 
  6 | Alternatif olarak, [statik yapılar oluşturma](static.md) da mümkündür.
  7 | 
  8 | ## PHP'yi yükleyin
  9 | 
 10 | FrankenPHP, PHP 8.2 ve üstü ile uyumludur.
 11 | 
 12 | İlk olarak, [PHP'nin kaynaklarını edinin](https://www.php.net/downloads.php) ve bunları çıkarın:
 13 | 
 14 | ```console
 15 | tar xf php-*
 16 | cd php-*/
 17 | ```
 18 | 
 19 | Ardından, PHP'yi platformunuz için yapılandırın.
 20 | 
 21 | Bu şekilde yapılandırma gereklidir, ancak başka opsiyonlar da ekleyebilirsiniz (örn. ekstra uzantılar)
 22 | İhtiyaç halinde.
 23 | 
 24 | ### Linux
 25 | 
 26 | ```console
 27 | ./configure \
 28 |     --enable-embed \
 29 |     --enable-zts \
 30 |     --disable-zend-signals \
 31 |     --enable-zend-max-execution-timers
 32 | ```
 33 | 
 34 | ### Mac
 35 | 
 36 | Yüklemek için [Homebrew](https://brew.sh/) paket yöneticisini kullanın
 37 | `libiconv`, `bison`, `re2c` ve `pkg-config`:
 38 | 
 39 | ```console
 40 | brew install libiconv bison re2c pkg-config
 41 | echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
 42 | ```
 43 | 
 44 | Ardından yapılandırma betiğini çalıştırın:
 45 | 
 46 | ```console
 47 | ./configure \
 48 |     --enable-embed=static \
 49 |     --enable-zts \
 50 |     --disable-zend-signals \
 51 |     --disable-opcache-jit \
 52 |     --enable-static \
 53 |     --enable-shared=no \
 54 |     --with-iconv=/opt/homebrew/opt/libiconv/
 55 | ```
 56 | 
 57 | ## PHP Derleyin
 58 | 
 59 | Son olarak, PHP'yi derleyin ve kurun:
 60 | 
 61 | ```console
 62 | make -j"$(getconf _NPROCESSORS_ONLN)"
 63 | sudo make install
 64 | ```
 65 | 
 66 | ## Go Uygulamasını Derleyin
 67 | 
 68 | Artık Go kütüphanesini kullanabilir ve Caddy yapımızı derleyebilirsiniz:
 69 | 
 70 | ```console
 71 | curl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz
 72 | cd frankenphp-main/caddy/frankenphp
 73 | CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build
 74 | ```
 75 | 
 76 | ### Xcaddy kullanımı
 77 | 
 78 | Alternatif olarak, FrankenPHP'yi [özel Caddy modülleri](https://caddyserver.com/docs/modules/) ile derlemek için [xcaddy](https://github.com/caddyserver/xcaddy) kullanın:
 79 | 
 80 | ```console
 81 | CGO_ENABLED=1 \
 82 | XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'" \
 83 | xcaddy build \
 84 |     --output frankenphp \
 85 |     --with github.com/dunglas/frankenphp/caddy \
 86 |     --with github.com/dunglas/caddy-cbrotli \
 87 |     --with github.com/dunglas/mercure/caddy \
 88 |     --with github.com/dunglas/vulcain/caddy
 89 |     # Add extra Caddy modules here
 90 | ```
 91 | 
 92 | > [!TIP]
 93 | >
 94 | > Eğer musl libc (Alpine Linux'ta varsayılan) ve Symfony kullanıyorsanız,
 95 | > varsayılan yığın boyutunu artırmanız gerekebilir.
 96 | > Aksi takdirde, şu tarz hatalar alabilirsiniz `PHP Fatal error: Maximum call stack size of 83360 bytes reached during compilation. Try splitting expression`
 97 | >
 98 | > Bunu yapmak için, `XCADDY_GO_BUILD_FLAGS` ortam değişkenini bu şekilde değiştirin
 99 | > `XCADDY_GO_BUILD_FLAGS=
#39;-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'`
100 | > (yığın boyutunun değerini uygulamanızın ihtiyaçlarına göre değiştirin).
101 | 


--------------------------------------------------------------------------------
/docs/tr/early-hints.md:
--------------------------------------------------------------------------------
 1 | # Early Hints
 2 | 
 3 | FrankenPHP [103 Early Hints durum kodunu](https://developer.chrome.com/blog/early-hints/) yerel olarak destekler.
 4 | Early Hints kullanmak web sayfalarınızın yüklenme süresini %30 oranında artırabilir.
 5 | 
 6 | ```php
 7 | <?php
 8 | 
 9 | header('Link: </style.css>; rel=preload; as=style');
10 | headers_send(103);
11 | 
12 | // yavaş algoritmalarınız ve SQL sorgularınız 🤪
13 | 
14 | echo <<<'HTML'
15 | <!DOCTYPE html>
16 | <title>Hello FrankenPHP</title>
17 | <link rel="stylesheet" href="style.css">
18 | HTML;
19 | ```
20 | 
21 | Early Hints hem normal hem de [worker](worker.md) modları tarafından desteklenir.
22 | 


--------------------------------------------------------------------------------
/docs/tr/github-actions.md:
--------------------------------------------------------------------------------
 1 | # GitHub Actions Kullanma
 2 | 
 3 | Bu depo Docker imajını [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) üzerinde derler ve dağıtır.
 4 | Bu durum onaylanan her çekme (pull) isteğinde veya çatallandıktan (fork) sonra gerçekleşir.
 5 | 
 6 | ## GitHub Eylemlerini Ayarlama
 7 | 
 8 | Depo ayarlarında, gizli değerler altında aşağıdaki gizli değerleri ekleyin:
 9 | 
10 | - `REGISTRY_LOGIN_SERVER`: Kullanılacak Docker Registry bilgisi (örneğin `docker.io`).
11 | - `REGISTRY_USERNAME`: Giriş yapmak için kullanılacak kullanıcı adı (örn. `dunglas`).
12 | - `REGISTRY_PASSWORD`: Oturum açmak için kullanılacak parola (örn. bir erişim anahtarı).
13 | - `IMAGE_NAME`: İmajın adı (örn. `dunglas/frankenphp`).
14 | 
15 | ## İmajı Oluşturma ve Dağıtma
16 | 
17 | 1. Bir Çekme (pull) İsteği oluşturun veya çatala (forka) dağıtın.
18 | 2. GitHub Actions imajı oluşturacak ve tüm testleri çalıştıracaktır.
19 | 3. Derleme başarılı olursa, görüntü `pr-x` (burada `x` PR numarasıdır) etiketi kullanılarak ilgili saklanan yere (registry'e) gönderilir.
20 | 
21 | ## İmajı Dağıtma
22 | 
23 | 1. Çekme (pull) isteği birleştirildikten sonra, GitHub Actions testleri tekrar çalıştıracak ve yeni bir imaj oluşturacaktır.
24 | 2. Derleme başarılı olursa, `main` etiketi Docker Registry'de güncellenecektir.
25 | 
26 | ## Bültenler
27 | 
28 | 1. Depoda yeni bir etiket oluşturun.
29 | 2. GitHub Actions imajı oluşturacak ve tüm testleri çalıştıracaktır.
30 | 3. Derleme başarılı olursa, etiket adı etiket olarak kullanılarak imaj saklanan yere (registry'e) gönderilir (örneğin `v1.2.3` ve `v1.2` oluşturulur).
31 | 4. `latest` etiketi de güncellenecektir.
32 | 


--------------------------------------------------------------------------------
/docs/tr/laravel.md:
--------------------------------------------------------------------------------
 1 | # Laravel
 2 | 
 3 | ## Docker
 4 | 
 5 | Bir [Laravel](https://laravel.com) web uygulamasını FrankenPHP ile çalıştırmak, projeyi resmi Docker imajının `/app` dizinine monte etmek kadar kolaydır.
 6 | 
 7 | Bu komutu Laravel uygulamanızın ana dizininden çalıştırın:
 8 | 
 9 | ```console
10 | docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
11 | ```
12 | 
13 | And tadını çıkarın!
14 | 
15 | ## Yerel Kurulum
16 | 
17 | Alternatif olarak, Laravel projelerinizi FrankenPHP ile yerel makinenizden çalıştırabilirsiniz:
18 | 
19 | 1. [Sisteminize karşılık gelen binary dosyayı indirin](https://github.com/php/frankenphp/releases)
20 | 2. Aşağıdaki yapılandırmayı Laravel projenizin kök dizinindeki `Caddyfile` adlı bir dosyaya ekleyin:
21 | 
22 |    ```caddyfile
23 |    {
24 |    	frankenphp
25 |    }
26 | 
27 |    # Sunucunuzun alan adı
28 |    localhost {
29 |    	# Webroot'u public/ dizinine ayarlayın
30 |    	root public/
31 |    	# Sıkıştırmayı etkinleştir (isteğe bağlı)
32 |    	encode zstd br gzip
33 |    	# PHP dosyalarını public/ dizininden çalıştırın ve varlıkları sunun
34 |    	php_server
35 |    }
36 |    ```
37 | 
38 | 3. FrankenPHP'yi Laravel projenizin kök dizininden başlatın: `frankenphp run`
39 | 
40 | ## Laravel Octane
41 | 
42 | Octane, Composer paket yöneticisi aracılığıyla kurulabilir:
43 | 
44 | ```console
45 | composer require laravel/octane
46 | ```
47 | 
48 | Octane'ı kurduktan sonra, Octane'ın yapılandırma dosyasını uygulamanıza yükleyecek olan `octane:install` Artisan komutunu çalıştırabilirsiniz:
49 | 
50 | ```console
51 | php artisan octane:install --server=frankenphp
52 | ```
53 | 
54 | Octane sunucusu `octane:frankenphp` Artisan komutu aracılığıyla başlatılabilir.
55 | 
56 | ```console
57 | php artisan octane:frankenphp
58 | ```
59 | 
60 | `octane:frankenphp` komutu aşağıdaki seçenekleri alabilir:
61 | 
62 | - `--host`: Sunucunun bağlanması gereken IP adresi (varsayılan: `127.0.0.1`)
63 | - `--port`: Sunucunun erişilebilir olması gereken port (varsayılan: `8000`)
64 | - `--admin-port`: Yönetici sunucusunun erişilebilir olması gereken port (varsayılan: `2019`)
65 | - `--workers`: İstekleri işlemek için hazır olması gereken worker sayısı (varsayılan: `auto`)
66 | - `--max-requests`: Sunucu yeniden yüklenmeden önce işlenecek istek sayısı (varsayılan: `500`)
67 | - `--caddyfile`: FrankenPHP `Caddyfile` dosyasının yolu
68 | - `--https`: HTTPS, HTTP/2 ve HTTP/3'ü etkinleştirin ve sertifikaları otomatik olarak oluşturup yenileyin
69 | - `--http-redirect`: HTTP'den HTTPS'ye yeniden yönlendirmeyi etkinleştir (yalnızca --https geçilirse etkinleştirilir)
70 | - `--watch`: Uygulamada kod değişikliği olduğunda sunucuyu otomatik olarak yeniden yükle
71 | - `--poll`: Dosyaları bir ağ üzerinden izlemek için izleme sırasında dosya sistemi yoklamasını kullanın
72 | - `--log-level`: Belirtilen günlük seviyesinde veya üzerinde günlük mesajları
73 | 
74 | Laravel Octane hakkında daha fazla bilgi edinmek için [Laravel Octane resmi belgelerine](https://laravel.com/docs/octane) göz atın.
75 | 


--------------------------------------------------------------------------------
/docs/tr/mercure.md:
--------------------------------------------------------------------------------
 1 | # Gerçek Zamanlı
 2 | 
 3 | FrankenPHP yerleşik bir [Mercure](https://mercure.rocks) hub ile birlikte gelir!
 4 | Mercure, olayları tüm bağlı cihazlara gerçek zamanlı olarak göndermeye olanak tanır: anında bir JavaScript olayı alırlar.
 5 | 
 6 | JS kütüphanesi veya SDK gerekmez!
 7 | 
 8 | ![Mercure](../mercure-hub.png)
 9 | 
10 | Mercure hub'ını etkinleştirmek için [Mercure'ün sitesinde](https://mercure.rocks/docs/hub/config) açıklandığı gibi `Caddyfile`'ı güncelleyin.
11 | 
12 | Mercure güncellemelerini kodunuzdan göndermek için [Symfony Mercure Bileşenini](https://symfony.com/components/Mercure) öneririz (kullanmak için Symfony tam yığın çerçevesine ihtiyacınız yoktur).
13 | 


--------------------------------------------------------------------------------
/docs/x-sendfile.md:
--------------------------------------------------------------------------------
 1 | # Efficiently Serving Large Static Files (`X-Sendfile`/`X-Accel-Redirect`)
 2 | 
 3 | Usually, static files can be served directly by the web server,
 4 | but sometimes it's necessary to execute some PHP code before sending them:
 5 | access control, statistics, custom HTTP headers...
 6 | 
 7 | Unfortunately, using PHP to serve large static files is inefficient compared to
 8 | direct use of the web server (memory overload, reduced performance...).
 9 | 
10 | FrankenPHP lets you delegate the sending of static files to the web server
11 | **after** executing customized PHP code.
12 | 
13 | To do this, your PHP application simply needs to define a custom HTTP header
14 | containing the path of the file to be served. FrankenPHP takes care of the rest.
15 | 
16 | This feature is known as **`X-Sendfile`** for Apache, and **`X-Accel-Redirect`** for NGINX.
17 | 
18 | In the following examples, we assume that the document root of the project is the `public/` directory.
19 | and that we want to use PHP to serve files stored outside the `public/` directory,
20 | from a directory named `private-files/`.
21 | 
22 | ## Configuration
23 | 
24 | First, add the following configuration to your `Caddyfile` to enable this feature:
25 | 
26 | ```patch
27 | 	root public/
28 | 	# ...
29 | 
30 | +	# Needed for Symfony, Laravel and other projects using the Symfony HttpFoundation component
31 | +	request_header X-Sendfile-Type x-accel-redirect
32 | +	request_header X-Accel-Mapping ../private-files=/private-files
33 | +
34 | +	intercept {
35 | +		@accel header X-Accel-Redirect *
36 | +		handle_response @accel {
37 | +			root private-files/
38 | +			rewrite * {resp.header.X-Accel-Redirect}
39 | +			method * GET
40 | +
41 | +			# Remove the X-Accel-Redirect header set by PHP for increased security
42 | +			header -X-Accel-Redirect
43 | +
44 | +			file_server
45 | +		}
46 | +	}
47 | 
48 | 	php_server
49 | ```
50 | 
51 | ## Plain PHP
52 | 
53 | Set the relative file path (from `private-files/`) as the value of the `X-Accel-Redirect` header:
54 | 
55 | ```php
56 | header('X-Accel-Redirect: file.txt');
57 | ```
58 | 
59 | ## Projects using the Symfony HttpFoundation component (Symfony, Laravel, Drupal...)
60 | 
61 | Symfony HttpFoundation [natively supports this feature](https://symfony.com/doc/current/components/http_foundation.html#serving-files).
62 | It will automatically determine the correct value for the `X-Accel-Redirect` header and add it to the response.
63 | 
64 | ```php
65 | use Symfony\Component\HttpFoundation\BinaryFileResponse;
66 | 
67 | BinaryFileResponse::trustXSendfileTypeHeader();
68 | $response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt');
69 | 
70 | // ...
71 | ```
72 | 


--------------------------------------------------------------------------------
/env.go:
--------------------------------------------------------------------------------
  1 | package frankenphp
  2 | 
  3 | // #cgo nocallback frankenphp_init_persistent_string
  4 | // #cgo nocallback frankenphp_add_assoc_str_ex
  5 | // #cgo noescape frankenphp_init_persistent_string
  6 | // #cgo noescape frankenphp_add_assoc_str_ex
  7 | // #include "frankenphp.h"
  8 | import "C"
  9 | import (
 10 | 	"os"
 11 | 	"strings"
 12 | 	"unsafe"
 13 | )
 14 | 
 15 | func initializeEnv() map[string]*C.zend_string {
 16 | 	env := os.Environ()
 17 | 	envMap := make(map[string]*C.zend_string, len(env))
 18 | 
 19 | 	for _, envVar := range env {
 20 | 		key, val, _ := strings.Cut(envVar, "=")
 21 | 		envMap[key] = C.frankenphp_init_persistent_string(toUnsafeChar(val), C.size_t(len(val)))
 22 | 	}
 23 | 
 24 | 	return envMap
 25 | }
 26 | 
 27 | // get the main thread env or the thread specific env
 28 | func getSandboxedEnv(thread *phpThread) map[string]*C.zend_string {
 29 | 	if thread.sandboxedEnv != nil {
 30 | 		return thread.sandboxedEnv
 31 | 	}
 32 | 
 33 | 	return mainThread.sandboxedEnv
 34 | }
 35 | 
 36 | func clearSandboxedEnv(thread *phpThread) {
 37 | 	if thread.sandboxedEnv == nil {
 38 | 		return
 39 | 	}
 40 | 
 41 | 	for key, val := range thread.sandboxedEnv {
 42 | 		valInMainThread, ok := mainThread.sandboxedEnv[key]
 43 | 		if !ok || val != valInMainThread {
 44 | 			C.free(unsafe.Pointer(val))
 45 | 		}
 46 | 	}
 47 | 
 48 | 	thread.sandboxedEnv = nil
 49 | }
 50 | 
 51 | // if an env var already exists, it needs to be freed
 52 | func removeEnvFromThread(thread *phpThread, key string) {
 53 | 	valueInThread, existsInThread := thread.sandboxedEnv[key]
 54 | 	if !existsInThread {
 55 | 		return
 56 | 	}
 57 | 
 58 | 	valueInMainThread, ok := mainThread.sandboxedEnv[key]
 59 | 	if !ok || valueInThread != valueInMainThread {
 60 | 		C.free(unsafe.Pointer(valueInThread))
 61 | 	}
 62 | 
 63 | 	delete(thread.sandboxedEnv, key)
 64 | }
 65 | 
 66 | // copy the main thread env to the thread specific env
 67 | func cloneSandboxedEnv(thread *phpThread) {
 68 | 	if thread.sandboxedEnv != nil {
 69 | 		return
 70 | 	}
 71 | 	thread.sandboxedEnv = make(map[string]*C.zend_string, len(mainThread.sandboxedEnv))
 72 | 	for key, value := range mainThread.sandboxedEnv {
 73 | 		thread.sandboxedEnv[key] = value
 74 | 	}
 75 | }
 76 | 
 77 | //export go_putenv
 78 | func go_putenv(threadIndex C.uintptr_t, str *C.char, length C.int) C.bool {
 79 | 	thread := phpThreads[threadIndex]
 80 | 	envString := C.GoStringN(str, length)
 81 | 	cloneSandboxedEnv(thread)
 82 | 
 83 | 	// Check if '=' is present in the string
 84 | 	if key, val, found := strings.Cut(envString, "="); found {
 85 | 		removeEnvFromThread(thread, key)
 86 | 		thread.sandboxedEnv[key] = C.frankenphp_init_persistent_string(toUnsafeChar(val), C.size_t(len(val)))
 87 | 		return os.Setenv(key, val) == nil
 88 | 	}
 89 | 
 90 | 	// No '=', unset the environment variable
 91 | 	removeEnvFromThread(thread, envString)
 92 | 	return os.Unsetenv(envString) == nil
 93 | }
 94 | 
 95 | //export go_getfullenv
 96 | func go_getfullenv(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
 97 | 	thread := phpThreads[threadIndex]
 98 | 	env := getSandboxedEnv(thread)
 99 | 
100 | 	for key, val := range env {
101 | 		C.frankenphp_add_assoc_str_ex(trackVarsArray, toUnsafeChar(key), C.size_t(len(key)), val)
102 | 	}
103 | }
104 | 
105 | //export go_getenv
106 | func go_getenv(threadIndex C.uintptr_t, name *C.char) (C.bool, *C.zend_string) {
107 | 	thread := phpThreads[threadIndex]
108 | 
109 | 	// Get the environment variable value
110 | 	envValue, exists := getSandboxedEnv(thread)[C.GoString(name)]
111 | 	if !exists {
112 | 		// Environment variable does not exist
113 | 		return false, nil // Return 0 to indicate failure
114 | 	}
115 | 
116 | 	return true, envValue // Return 1 to indicate success
117 | }
118 | 


--------------------------------------------------------------------------------
/ext.go:
--------------------------------------------------------------------------------
 1 | package frankenphp
 2 | 
 3 | //#include "frankenphp.h"
 4 | import "C"
 5 | import (
 6 | 	"sync"
 7 | 	"unsafe"
 8 | )
 9 | 
10 | var (
11 | 	extensions   []*C.zend_module_entry
12 | 	registerOnce sync.Once
13 | )
14 | 
15 | // RegisterExtension registers a new PHP extension.
16 | func RegisterExtension(me unsafe.Pointer) {
17 | 	extensions = append(extensions, (*C.zend_module_entry)(me))
18 | }
19 | 
20 | func registerExtensions() {
21 | 	if len(extensions) == 0 {
22 | 		return
23 | 	}
24 | 
25 | 	registerOnce.Do(func() {
26 | 		C.register_extensions(extensions[0], C.int(len(extensions)))
27 | 		extensions = nil
28 | 	})
29 | }
30 | 


--------------------------------------------------------------------------------
/frankenphp.h:
--------------------------------------------------------------------------------
 1 | #ifndef _FRANKENPPHP_H
 2 | #define _FRANKENPPHP_H
 3 | 
 4 | #include <Zend/zend_modules.h>
 5 | #include <Zend/zend_types.h>
 6 | #include <stdbool.h>
 7 | #include <stdint.h>
 8 | 
 9 | #ifndef FRANKENPHP_VERSION
10 | #define FRANKENPHP_VERSION dev
11 | #endif
12 | #define STRINGIFY(x) #x
13 | #define TOSTRING(x) STRINGIFY(x)
14 | 
15 | typedef struct go_string {
16 |   size_t len;
17 |   char *data;
18 | } go_string;
19 | 
20 | typedef struct ht_key_value_pair {
21 |   zend_string *key;
22 |   char *val;
23 |   size_t val_len;
24 | } ht_key_value_pair;
25 | 
26 | typedef struct php_variable {
27 |   const char *var;
28 |   size_t data_len;
29 |   char *data;
30 | } php_variable;
31 | 
32 | typedef struct frankenphp_version {
33 |   unsigned char major_version;
34 |   unsigned char minor_version;
35 |   unsigned char release_version;
36 |   const char *extra_version;
37 |   const char *version;
38 |   unsigned long version_id;
39 | } frankenphp_version;
40 | frankenphp_version frankenphp_get_version();
41 | 
42 | typedef struct frankenphp_config {
43 |   frankenphp_version version;
44 |   bool zts;
45 |   bool zend_signals;
46 |   bool zend_max_execution_timers;
47 | } frankenphp_config;
48 | frankenphp_config frankenphp_get_config();
49 | 
50 | int frankenphp_new_main_thread(int num_threads);
51 | bool frankenphp_new_php_thread(uintptr_t thread_index);
52 | 
53 | bool frankenphp_shutdown_dummy_request(void);
54 | int frankenphp_update_server_context(bool is_worker_request,
55 | 
56 |                                      const char *request_method,
57 |                                      char *query_string,
58 |                                      zend_long content_length,
59 |                                      char *path_translated, char *request_uri,
60 |                                      const char *content_type, char *auth_user,
61 |                                      char *auth_password, int proto_num);
62 | int frankenphp_request_startup();
63 | int frankenphp_execute_script(char *file_name);
64 | 
65 | int frankenphp_execute_script_cli(char *script, int argc, char **argv,
66 |                                   bool eval);
67 | 
68 | void frankenphp_register_variables_from_request_info(
69 |     zval *track_vars_array, zend_string *content_type,
70 |     zend_string *path_translated, zend_string *query_string,
71 |     zend_string *auth_user, zend_string *request_method);
72 | void frankenphp_register_variable_safe(char *key, char *var, size_t val_len,
73 |                                        zval *track_vars_array);
74 | zend_string *frankenphp_init_persistent_string(const char *string, size_t len);
75 | int frankenphp_reset_opcache(void);
76 | int frankenphp_get_current_memory_limit();
77 | void frankenphp_add_assoc_str_ex(zval *track_vars_array, char *key,
78 |                                  size_t keylen, zend_string *val);
79 | 
80 | void frankenphp_register_single(zend_string *z_key, char *value, size_t val_len,
81 |                                 zval *track_vars_array);
82 | void frankenphp_register_bulk(
83 |     zval *track_vars_array, ht_key_value_pair remote_addr,
84 |     ht_key_value_pair remote_host, ht_key_value_pair remote_port,
85 |     ht_key_value_pair document_root, ht_key_value_pair path_info,
86 |     ht_key_value_pair php_self, ht_key_value_pair document_uri,
87 |     ht_key_value_pair script_filename, ht_key_value_pair script_name,
88 |     ht_key_value_pair https, ht_key_value_pair ssl_protocol,
89 |     ht_key_value_pair request_scheme, ht_key_value_pair server_name,
90 |     ht_key_value_pair server_port, ht_key_value_pair content_length,
91 |     ht_key_value_pair gateway_interface, ht_key_value_pair server_protocol,
92 |     ht_key_value_pair server_software, ht_key_value_pair http_host,
93 |     ht_key_value_pair auth_type, ht_key_value_pair remote_ident,
94 |     ht_key_value_pair request_uri, ht_key_value_pair ssl_cipher);
95 | 
96 | void register_extensions(zend_module_entry *m, int len);
97 | 
98 | #endif
99 | 


--------------------------------------------------------------------------------
/frankenphp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php/frankenphp/34fbfd467b264dd75e5b02534aceec179217fb35/frankenphp.png


--------------------------------------------------------------------------------
/frankenphp.stub.php:
--------------------------------------------------------------------------------
 1 | <?php
 2 | 
 3 | /** @generate-class-entries */
 4 | 
 5 | function frankenphp_handle_request(callable $callback): bool {}
 6 | 
 7 | function headers_send(int $status = 200): int {}
 8 | 
 9 | function frankenphp_finish_request(): bool {}
10 | 
11 | /**
12 |  * @alias frankenphp_finish_request
13 |  */
14 | function fastcgi_finish_request(): bool {}
15 | 
16 | function frankenphp_request_headers(): array {}
17 | 
18 | /**
19 |  * @alias frankenphp_request_headers
20 |  */
21 | function apache_request_headers(): array {}
22 | 
23 | /**
24 |  * @alias frankenphp_request_headers
25 | */
26 | function getallheaders(): array {}
27 | 
28 | function frankenphp_response_headers(): array|bool {}
29 | 
30 | /**
31 |  * @alias frankenphp_response_headers
32 |  */
33 | function apache_response_headers(): array|bool {}
34 | 
35 | 


--------------------------------------------------------------------------------
/frankenphp_arginfo.h:
--------------------------------------------------------------------------------
 1 | /* This is a generated file, edit the .stub.php file instead.
 2 |  * Stub hash: 05ebde17137c559e891362fba6524fad1e0a2dfe */
 3 | 
 4 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1,
 5 |                                         _IS_BOOL, 0)
 6 | ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
 7 | ZEND_END_ARG_INFO()
 8 | 
 9 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_headers_send, 0, 0, IS_LONG, 0)
10 | ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200")
11 | ZEND_END_ARG_INFO()
12 | 
13 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0,
14 |                                         _IS_BOOL, 0)
15 | ZEND_END_ARG_INFO()
16 | 
17 | #define arginfo_fastcgi_finish_request arginfo_frankenphp_finish_request
18 | 
19 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_request_headers, 0,
20 |                                         0, IS_ARRAY, 0)
21 | ZEND_END_ARG_INFO()
22 | 
23 | #define arginfo_apache_request_headers arginfo_frankenphp_request_headers
24 | 
25 | #define arginfo_getallheaders arginfo_frankenphp_request_headers
26 | 
27 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_frankenphp_response_headers, 0,
28 |                                         0, MAY_BE_ARRAY | MAY_BE_BOOL)
29 | ZEND_END_ARG_INFO()
30 | 
31 | #define arginfo_apache_response_headers arginfo_frankenphp_response_headers
32 | 
33 | ZEND_FUNCTION(frankenphp_handle_request);
34 | ZEND_FUNCTION(headers_send);
35 | ZEND_FUNCTION(frankenphp_finish_request);
36 | ZEND_FUNCTION(frankenphp_request_headers);
37 | ZEND_FUNCTION(frankenphp_response_headers);
38 | 
39 | // clang-format off
40 | static const zend_function_entry ext_functions[] = {
41 |   ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
42 |   ZEND_FE(headers_send, arginfo_headers_send)
43 |   ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)
44 |   ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request)
45 |   ZEND_FE(frankenphp_request_headers, arginfo_frankenphp_request_headers)
46 |   ZEND_FALIAS(apache_request_headers, frankenphp_request_headers, arginfo_apache_request_headers)
47 |   ZEND_FALIAS(getallheaders, frankenphp_request_headers, arginfo_getallheaders)
48 |   ZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers)
49 |   ZEND_FALIAS(apache_response_headers, frankenphp_response_headers, arginfo_apache_response_headers)
50 |   ZEND_FE_END
51 | };
52 | // clang-format on
53 | 


--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
 1 | module github.com/dunglas/frankenphp
 2 | 
 3 | go 1.24.0
 4 | 
 5 | retract v1.0.0-rc.1 // Human error
 6 | 
 7 | require (
 8 | 	github.com/Masterminds/sprig/v3 v3.3.0
 9 | 	github.com/maypok86/otter v1.2.4
10 | 	github.com/prometheus/client_golang v1.22.0
11 | 	github.com/stretchr/testify v1.10.0
12 | 	go.uber.org/zap v1.27.0
13 | 	go.uber.org/zap/exp v0.3.0
14 | 	golang.org/x/net v0.41.0
15 | )
16 | 
17 | require (
18 | 	dario.cat/mergo v1.0.2 // indirect
19 | 	github.com/Masterminds/goutils v1.1.1 // indirect
20 | 	github.com/Masterminds/semver/v3 v3.4.0 // indirect
21 | 	github.com/beorn7/perks v1.0.1 // indirect
22 | 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
23 | 	github.com/davecgh/go-spew v1.1.1 // indirect
24 | 	github.com/dolthub/maphash v0.1.0 // indirect
25 | 	github.com/gammazero/deque v1.0.0 // indirect
26 | 	github.com/google/uuid v1.6.0 // indirect
27 | 	github.com/huandu/xstrings v1.5.0 // indirect
28 | 	github.com/kylelemons/godebug v1.1.0 // indirect
29 | 	github.com/mitchellh/copystructure v1.2.0 // indirect
30 | 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
31 | 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
32 | 	github.com/pmezard/go-difflib v1.0.0 // indirect
33 | 	github.com/prometheus/client_model v0.6.2 // indirect
34 | 	github.com/prometheus/common v0.65.0 // indirect
35 | 	github.com/prometheus/procfs v0.16.1 // indirect
36 | 	github.com/rogpeppe/go-internal v1.12.0 // indirect
37 | 	github.com/shopspring/decimal v1.4.0 // indirect
38 | 	github.com/spf13/cast v1.9.2 // indirect
39 | 	go.uber.org/multierr v1.11.0 // indirect
40 | 	golang.org/x/crypto v0.39.0 // indirect
41 | 	golang.org/x/sys v0.33.0 // indirect
42 | 	golang.org/x/text v0.26.0 // indirect
43 | 	google.golang.org/protobuf v1.36.6 // indirect
44 | 	gopkg.in/yaml.v3 v3.0.1 // indirect
45 | )
46 | 


--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | set -e
 4 | 
 5 | if [ -z "${BIN_DIR}" ]; then
 6 | 	BIN_DIR=$(pwd)
 7 | fi
 8 | 
 9 | THE_ARCH_BIN=""
10 | DEST=${BIN_DIR}/frankenphp
11 | 
12 | OS=$(uname -s)
13 | ARCH=$(uname -m)
14 | GNU=""
15 | 
16 | if type "tput" >/dev/null 2>&1; then
17 | 	bold=$(tput bold || true)
18 | 	italic=$(tput sitm || true)
19 | 	normal=$(tput sgr0 || true)
20 | fi
21 | 
22 | case ${OS} in
23 | Linux*)
24 | 	case ${ARCH} in
25 | 	aarch64)
26 | 		THE_ARCH_BIN="frankenphp-linux-aarch64"
27 | 		;;
28 | 	x86_64)
29 | 		THE_ARCH_BIN="frankenphp-linux-x86_64"
30 | 		;;
31 | 	*)
32 | 		THE_ARCH_BIN=""
33 | 		;;
34 | 	esac
35 | 
36 | 	if getconf GNU_LIBC_VERSION >/dev/null 2>&1; then
37 | 		THE_ARCH_BIN="${THE_ARCH_BIN}-gnu"
38 | 		GNU=" (glibc)"
39 | 	fi
40 | 	;;
41 | Darwin*)
42 | 	case ${ARCH} in
43 | 	arm64)
44 | 		THE_ARCH_BIN="frankenphp-mac-arm64"
45 | 		;;
46 | 	*)
47 | 		THE_ARCH_BIN="frankenphp-mac-x86_64"
48 | 		;;
49 | 	esac
50 | 	;;
51 | Windows | MINGW64_NT*)
52 | 	echo "❗ Use WSL to run FrankenPHP on Windows: https://learn.microsoft.com/windows/wsl/"
53 | 	exit 1
54 | 	;;
55 | *)
56 | 	THE_ARCH_BIN=""
57 | 	;;
58 | esac
59 | 
60 | if [ -z "${THE_ARCH_BIN}" ]; then
61 | 	echo "❗ FrankenPHP is not supported on ${OS} and ${ARCH}"
62 | 	exit 1
63 | fi
64 | 
65 | SUDO=""
66 | 
67 | echo "📦 Downloading ${bold}FrankenPHP${normal} for ${OS}${GNU} (${ARCH}):"
68 | 
69 | # check if $DEST is writable and suppress an error message
70 | touch "${DEST}" 2>/dev/null
71 | 
72 | # we need sudo powers to write to DEST
73 | if [ $? -eq 1 ]; then
74 | 	echo "❗ You do not have permission to write to ${italic}${DEST}${normal}, enter your password to grant sudo powers"
75 | 	SUDO="sudo"
76 | fi
77 | 
78 | if type "curl" >/dev/null 2>&1; then
79 | 	curl -L --progress-bar "https://github.com/php/frankenphp/releases/latest/download/${THE_ARCH_BIN}" -o "${DEST}"
80 | elif type "wget" >/dev/null 2>&1; then
81 | 	${SUDO} wget "https://github.com/php/frankenphp/releases/latest/download/${THE_ARCH_BIN}" -O "${DEST}"
82 | else
83 | 	echo "❗ Please install ${italic}curl${normal} or ${italic}wget${normal} to download FrankenPHP"
84 | 	exit 1
85 | fi
86 | 
87 | ${SUDO} chmod +x "${DEST}"
88 | 
89 | echo
90 | echo "🥳 FrankenPHP downloaded successfully to ${italic}${DEST}${normal}"
91 | echo "🔧 Move the binary to ${italic}/usr/local/bin/${normal} or another directory in your ${italic}PATH${normal} to use it globally:"
92 | echo "   ${bold}sudo mv ${DEST} /usr/local/bin/${normal}"
93 | echo
94 | echo "⭐ If you like FrankenPHP, please give it a star on GitHub: ${italic}https://github.com/php/frankenphp${normal}"
95 | 


--------------------------------------------------------------------------------
/internal/cpu/cpu_unix.go:
--------------------------------------------------------------------------------
 1 | package cpu
 2 | 
 3 | // #include <time.h>
 4 | import "C"
 5 | import (
 6 | 	"runtime"
 7 | 	"time"
 8 | )
 9 | 
10 | var cpuCount = runtime.GOMAXPROCS(0)
11 | 
12 | // ProbeCPUs probes the CPU usage of the process
13 | // if CPUs are not busy, most threads are likely waiting for I/O, so we should scale
14 | // if CPUs are already busy we won't gain much by scaling and want to avoid the overhead of doing so
15 | func ProbeCPUs(probeTime time.Duration, maxCPUUsage float64, abort chan struct{}) bool {
16 | 	var cpuStart, cpuEnd C.struct_timespec
17 | 
18 | 	// note: clock_gettime is a POSIX function
19 | 	// on Windows we'd need to use QueryPerformanceCounter instead
20 | 	start := time.Now()
21 | 	C.clock_gettime(C.CLOCK_PROCESS_CPUTIME_ID, &cpuStart)
22 | 
23 | 	select {
24 | 	case <-abort:
25 | 		return false
26 | 	case <-time.After(probeTime):
27 | 	}
28 | 
29 | 	C.clock_gettime(C.CLOCK_PROCESS_CPUTIME_ID, &cpuEnd)
30 | 	elapsedTime := float64(time.Since(start).Nanoseconds())
31 | 	elapsedCpuTime := float64(cpuEnd.tv_sec-cpuStart.tv_sec)*1e9 + float64(cpuEnd.tv_nsec-cpuStart.tv_nsec)
32 | 	cpuUsage := elapsedCpuTime / elapsedTime / float64(cpuCount)
33 | 
34 | 	return cpuUsage < maxCPUUsage
35 | }
36 | 


--------------------------------------------------------------------------------
/internal/cpu/cpu_windows.go:
--------------------------------------------------------------------------------
 1 | package cpu
 2 | 
 3 | import (
 4 | 	"time"
 5 | )
 6 | 
 7 | // ProbeCPUs fallback that always determines that the CPU limits are not reached
 8 | func ProbeCPUs(probeTime time.Duration, maxCPUUsage float64, abort chan struct{}) bool {
 9 | 	select {
10 | 	case <-abort:
11 | 		return false
12 | 	case <-time.After(probeTime):
13 | 		return true
14 | 	}
15 | }
16 | 


--------------------------------------------------------------------------------
/internal/extgen/arginfo.go:
--------------------------------------------------------------------------------
 1 | package extgen
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 	"os/exec"
 7 | 	"path/filepath"
 8 | 	"strings"
 9 | )
10 | 
11 | type arginfoGenerator struct {
12 | 	generator *Generator
13 | }
14 | 
15 | func (ag *arginfoGenerator) generate() error {
16 | 	genStubPath := os.Getenv("GEN_STUB_SCRIPT")
17 | 	if genStubPath == "" {
18 | 		genStubPath = "/usr/local/src/php/build/gen_stub.php"
19 | 	}
20 | 
21 | 	if _, err := os.Stat(genStubPath); err != nil {
22 | 		return fmt.Errorf(`the PHP "gen_stub.php" file couldn't be found under %q, you can set the "GEN_STUB_SCRIPT" environement variable to set a custom location`, genStubPath)
23 | 	}
24 | 
25 | 	stubFile := ag.generator.BaseName + ".stub.php"
26 | 	cmd := exec.Command("php", genStubPath, filepath.Join(ag.generator.BuildDir, stubFile))
27 | 
28 | 	if err := cmd.Run(); err != nil {
29 | 		return fmt.Errorf("running gen_stub script: %w", err)
30 | 	}
31 | 
32 | 	return ag.fixArginfoFile(stubFile)
33 | }
34 | 
35 | func (ag *arginfoGenerator) fixArginfoFile(stubFile string) error {
36 | 	arginfoFile := strings.TrimSuffix(stubFile, ".stub.php") + "_arginfo.h"
37 | 	arginfoPath := filepath.Join(ag.generator.BuildDir, arginfoFile)
38 | 
39 | 	content, err := readFile(arginfoPath)
40 | 	if err != nil {
41 | 		return fmt.Errorf("reading arginfo file: %w", err)
42 | 	}
43 | 
44 | 	// FIXME: the script generate "zend_register_internal_class_with_flags" but it is not recognized by the compiler
45 | 	fixedContent := strings.ReplaceAll(content,
46 | 		"zend_register_internal_class_with_flags(&ce, NULL, 0)",
47 | 		"zend_register_internal_class(&ce)")
48 | 
49 | 	return writeFile(arginfoPath, fixedContent)
50 | }
51 | 


--------------------------------------------------------------------------------
/internal/extgen/cfile.go:
--------------------------------------------------------------------------------
 1 | package extgen
 2 | 
 3 | import (
 4 | 	"github.com/Masterminds/sprig/v3"
 5 | 
 6 | 	"bytes"
 7 | 	_ "embed"
 8 | 	"path/filepath"
 9 | 	"strings"
10 | 	"text/template"
11 | )
12 | 
13 | //go:embed templates/extension.c.tpl
14 | var cFileContent string
15 | 
16 | type cFileGenerator struct {
17 | 	generator *Generator
18 | }
19 | 
20 | type cTemplateData struct {
21 | 	BaseName  string
22 | 	Functions []phpFunction
23 | 	Classes   []phpClass
24 | 	Constants []phpConstant
25 | 	Namespace string
26 | }
27 | 
28 | func (cg *cFileGenerator) generate() error {
29 | 	filename := filepath.Join(cg.generator.BuildDir, cg.generator.BaseName+".c")
30 | 	content, err := cg.buildContent()
31 | 	if err != nil {
32 | 		return err
33 | 	}
34 | 
35 | 	return writeFile(filename, content)
36 | }
37 | 
38 | func (cg *cFileGenerator) buildContent() (string, error) {
39 | 	var builder strings.Builder
40 | 
41 | 	templateContent, err := cg.getTemplateContent()
42 | 	if err != nil {
43 | 		return "", err
44 | 	}
45 | 	builder.WriteString(templateContent)
46 | 
47 | 	for _, fn := range cg.generator.Functions {
48 | 		fnGen := PHPFuncGenerator{
49 | 			paramParser: &ParameterParser{},
50 | 			namespace:   cg.generator.Namespace,
51 | 		}
52 | 		builder.WriteString(fnGen.generate(fn))
53 | 	}
54 | 
55 | 	return builder.String(), nil
56 | }
57 | 
58 | func (cg *cFileGenerator) getTemplateContent() (string, error) {
59 | 	funcMap := sprig.FuncMap()
60 | 	funcMap["namespacedClassName"] = NamespacedName
61 | 
62 | 	tmpl := template.Must(template.New("cfile").Funcs(funcMap).Parse(cFileContent))
63 | 
64 | 	var buf bytes.Buffer
65 | 	if err := tmpl.Execute(&buf, cTemplateData{
66 | 		BaseName:  cg.generator.BaseName,
67 | 		Functions: cg.generator.Functions,
68 | 		Classes:   cg.generator.Classes,
69 | 		Constants: cg.generator.Constants,
70 | 		Namespace: cg.generator.Namespace,
71 | 	}); err != nil {
72 | 		return "", err
73 | 	}
74 | 
75 | 	return buf.String(), nil
76 | }
77 | 


--------------------------------------------------------------------------------
/internal/extgen/constparser.go:
--------------------------------------------------------------------------------
  1 | package extgen
  2 | 
  3 | import (
  4 | 	"bufio"
  5 | 	"fmt"
  6 | 	"os"
  7 | 	"regexp"
  8 | 	"strconv"
  9 | 	"strings"
 10 | )
 11 | 
 12 | var constRegex = regexp.MustCompile(`//\s*export_php:const



    
    

    
    

    
    
    
    

    
    
    
    




    

    
The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
) 13 | var classConstRegex = regexp.MustCompile(`//\s*export_php:classconst\s+(\w+)
The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
) 14 | var constDeclRegex = regexp.MustCompile(`const\s+(\w+)\s*=\s*(.+)`) 15 | 16 | type ConstantParser struct{} 17 | 18 | func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err error) { 19 | file, err := os.Open(filename) 20 | if err != nil { 21 | return nil, err 22 | } 23 | defer func() { 24 | e := file.Close() 25 | if err == nil { 26 | err = e 27 | } 28 | }() 29 | 30 | scanner := bufio.NewScanner(file) 31 | 32 | lineNumber := 0 33 | expectConstDecl := false 34 | expectClassConstDecl := false 35 | currentClassName := "" 36 | currentConstantValue := 0 37 | 38 | for scanner.Scan() { 39 | lineNumber++ 40 | line := strings.TrimSpace(scanner.Text()) 41 | 42 | if constRegex.MatchString(line) { 43 | expectConstDecl = true 44 | expectClassConstDecl = false 45 | currentClassName = "" 46 | 47 | continue 48 | } 49 | 50 | if matches := classConstRegex.FindStringSubmatch(line); len(matches) == 2 { 51 | expectClassConstDecl = true 52 | expectConstDecl = false 53 | currentClassName = matches[1] 54 | 55 | continue 56 | } 57 | 58 | if (expectConstDecl || expectClassConstDecl) && strings.HasPrefix(line, "const ") { 59 | matches := constDeclRegex.FindStringSubmatch(line) 60 | if len(matches) == 3 { 61 | name := matches[1] 62 | value := strings.TrimSpace(matches[2]) 63 | 64 | constant := phpConstant{ 65 | Name: name, 66 | Value: value, 67 | IsIota: value == "iota", 68 | lineNumber: lineNumber, 69 | ClassName: currentClassName, 70 | } 71 | 72 | constant.PhpType = determineConstantType(value) 73 | 74 | if constant.IsIota { 75 | // affect a default value because user didn't give one 76 | constant.Value = fmt.Sprintf("%d", currentConstantValue) 77 | constant.PhpType = phpInt 78 | currentConstantValue++ 79 | } 80 | 81 | constants = append(constants, constant) 82 | } else { 83 | return nil, fmt.Errorf("invalid constant declaration at line %d: %s", lineNumber, line) 84 | } 85 | expectConstDecl = false 86 | expectClassConstDecl = false 87 | } else if (expectConstDecl || expectClassConstDecl) && !strings.HasPrefix(line, "//") && line != "" { 88 | // we expected a const declaration but found something else, reset 89 | expectConstDecl = false 90 | expectClassConstDecl = false 91 | currentClassName = "" 92 | } 93 | } 94 | 95 | return constants, scanner.Err() 96 | } 97 | 98 | // determineConstantType analyzes the value and determines its type 99 | func determineConstantType(value string) phpType { 100 | value = strings.TrimSpace(value) 101 | 102 | if (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) || 103 | (strings.HasPrefix(value, "`") && strings.HasSuffix(value, "`")) { 104 | return phpString 105 | } 106 | 107 | if value == "true" || value == "false" { 108 | return phpBool 109 | } 110 | 111 | // check for integer literals, including hex, octal, binary 112 | if _, err := strconv.ParseInt(value, 0, 64); err == nil { 113 | return phpInt 114 | } 115 | 116 | if _, err := strconv.ParseFloat(value, 64); err == nil { 117 | return phpFloat 118 | } 119 | 120 | return phpInt 121 | } 122 | -------------------------------------------------------------------------------- /internal/extgen/docs.go: -------------------------------------------------------------------------------- 1 | package extgen 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "path/filepath" 7 | "text/template" 8 | ) 9 | 10 | //go:embed templates/README.md.tpl 11 | var docFileContent string 12 | 13 | type DocumentationGenerator struct { 14 | generator *Generator 15 | } 16 | 17 | type DocTemplateData struct { 18 | BaseName string 19 | Functions []phpFunction 20 | Classes []phpClass 21 | } 22 | 23 | func (dg *DocumentationGenerator) generate() error { 24 | filename := filepath.Join(dg.generator.BuildDir, "README.md") 25 | content, err := dg.generateMarkdown() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return writeFile(filename, content) 31 | } 32 | 33 | func (dg *DocumentationGenerator) generateMarkdown() (string, error) { 34 | tmpl := template.Must(template.New("readme").Parse(docFileContent)) 35 | 36 | var buf bytes.Buffer 37 | if err := tmpl.Execute(&buf, DocTemplateData{ 38 | BaseName: dg.generator.BaseName, 39 | Functions: dg.generator.Functions, 40 | Classes: dg.generator.Classes, 41 | }); err != nil { 42 | return "", err 43 | } 44 | 45 | return buf.String(), nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/extgen/errors.go: -------------------------------------------------------------------------------- 1 | package extgen 2 | 3 | import "fmt" 4 | 5 | type GeneratorError struct { 6 | Stage string 7 | Message string 8 | Err error 9 | } 10 | 11 | func (e *GeneratorError) Error() string { 12 | if e.Err == nil { 13 | return fmt.Sprintf("generator error at %s: %s", e.Stage, e.Message) 14 | } 15 | 16 | return fmt.Sprintf("generator error at %s: %s: %v", e.Stage, e.Message, e.Err) 17 | } 18 | -------------------------------------------------------------------------------- /internal/extgen/gofile.go: -------------------------------------------------------------------------------- 1 | package extgen 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "fmt" 7 | "path/filepath" 8 | "text/template" 9 | 10 | "github.com/Masterminds/sprig/v3" 11 | ) 12 | 13 | //go:embed templates/extension.go.tpl 14 | var goFileContent string 15 | 16 | type GoFileGenerator struct { 17 | generator *Generator 18 | } 19 | 20 | type goTemplateData struct { 21 | PackageName string 22 | BaseName string 23 | Imports []string 24 | Constants []phpConstant 25 | InternalFunctions []string 26 | Functions []phpFunction 27 | Classes []phpClass 28 | } 29 | 30 | func (gg *GoFileGenerator) generate() error { 31 | filename := filepath.Join(gg.generator.BuildDir, gg.generator.BaseName+".go") 32 | content, err := gg.buildContent() 33 | if err != nil { 34 | return fmt.Errorf("building Go file content: %w", err) 35 | } 36 | 37 | return writeFile(filename, content) 38 | } 39 | 40 | func (gg *GoFileGenerator) buildContent() (string, error) { 41 | sourceAnalyzer := SourceAnalyzer{} 42 | imports, internalFunctions, err := sourceAnalyzer.analyze(gg.generator.SourceFile) 43 | if err != nil { 44 | return "", fmt.Errorf("analyzing source file: %w", err) 45 | } 46 | 47 | filteredImports := make([]string, 0, len(imports)) 48 | for _, imp := range imports { 49 | if imp != `"C"` { 50 | filteredImports = append(filteredImports, imp) 51 | } 52 | } 53 | 54 | classes := make([]phpClass, len(gg.generator.Classes)) 55 | copy(classes, gg.generator.Classes) 56 | 57 | templateContent, err := gg.getTemplateContent(goTemplateData{ 58 | PackageName: SanitizePackageName(gg.generator.BaseName), 59 | BaseName: gg.generator.BaseName, 60 | Imports: filteredImports, 61 | Constants: gg.generator.Constants, 62 | InternalFunctions: internalFunctions, 63 | Functions: gg.generator.Functions, 64 | Classes: classes, 65 | }) 66 | 67 | if err != nil { 68 | return "", fmt.Errorf("executing template: %w", err) 69 | } 70 | 71 | return templateContent, nil 72 | } 73 | 74 | func (gg *GoFileGenerator) getTemplateContent(data goTemplateData) (string, error) { 75 | funcMap := sprig.FuncMap() 76 | funcMap["phpTypeToGoType"] = gg.phpTypeToGoType 77 | funcMap["isStringOrArray"] = func(t phpType) bool { 78 | return t == phpString || t == phpArray 79 | } 80 | funcMap["isVoid"] = func(t phpType) bool { 81 | return t == phpVoid 82 | } 83 | 84 | tmpl := template.Must(template.New("gofile").Funcs(funcMap).Parse(goFileContent)) 85 | 86 | var buf bytes.Buffer 87 | if err := tmpl.Execute(&buf, data); err != nil { 88 | return "", err 89 | } 90 | 91 | return buf.String(), nil 92 | } 93 | 94 | type GoMethodSignature struct { 95 | MethodName string 96 | Params []GoParameter 97 | ReturnType string 98 | } 99 | 100 | type GoParameter struct { 101 | Name string 102 | Type string 103 | } 104 | 105 | func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string { 106 | typeMap := map[phpType]string{ 107 | phpString: "string", 108 | phpInt: "int64", 109 | phpFloat: "float64", 110 | phpBool: "bool", 111 | phpArray: "*frankenphp.Array", 112 | phpMixed: "interface{}", 113 | phpVoid: "", 114 | } 115 | 116 | if goType, exists := typeMap[phpT]; exists { 117 | return goType 118 | } 119 | 120 | return "interface{}" 121 | } 122 | -------------------------------------------------------------------------------- /internal/extgen/hfile.go: -------------------------------------------------------------------------------- 1 | // header.go 2 | package extgen 3 | 4 | import ( 5 | "bytes" 6 | _ "embed" 7 | "path/filepath" 8 | "strings" 9 | "text/template" 10 | ) 11 | 12 | //go:embed templates/extension.h.tpl 13 | var hFileContent string 14 | 15 | type HeaderGenerator struct { 16 | generator *Generator 17 | } 18 | 19 | type TemplateData struct { 20 | HeaderGuard string 21 | Constants []phpConstant 22 | Classes []phpClass 23 | } 24 | 25 | func (hg *HeaderGenerator) generate() error { 26 | filename := filepath.Join(hg.generator.BuildDir, hg.generator.BaseName+".h") 27 | content, err := hg.buildContent() 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return writeFile(filename, content) 33 | } 34 | 35 | func (hg *HeaderGenerator) buildContent() (string, error) { 36 | headerGuard := strings.Map(func(r rune) rune { 37 | if r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' { 38 | return r 39 | } 40 | 41 | return '_' 42 | }, hg.generator.BaseName) 43 | 44 | headerGuard = strings.ToUpper(headerGuard) + "_H" 45 | 46 | tmpl, err := template.New("header").Parse(hFileContent) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | var buf bytes.Buffer 52 | err = tmpl.Execute(&buf, TemplateData{ 53 | HeaderGuard: headerGuard, 54 | Constants: hg.generator.Constants, 55 | Classes: hg.generator.Classes, 56 | }) 57 | 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | return buf.String(), nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/extgen/namespace_test.go: -------------------------------------------------------------------------------- 1 | package extgen 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/stretchr/testify/require" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestNamespaceParser(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | content string 14 | expected string 15 | shouldError bool 16 | }{ 17 | { 18 | name: "basic namespace", 19 | content: `package main 20 | 21 | //export_php:namespace My\Test\Namespace 22 | 23 | func main() {}`, 24 | expected: `My\Test\Namespace`, 25 | }, 26 | { 27 | name: "namespace with spaces", 28 | content: `package main 29 | 30 | //export_php:namespace My\Test\Namespace 31 | 32 | func main() {}`, 33 | expected: `My\Test\Namespace`, 34 | }, 35 | { 36 | name: "no namespace", 37 | content: `package main 38 | 39 | func main() {}`, 40 | expected: "", 41 | }, 42 | { 43 | name: "multiple namespaces should error", 44 | content: `package main 45 | 46 | //export_php:namespace First\Namespace 47 | //export_php:namespace Second\Namespace 48 | 49 | func main() {}`, 50 | expected: "", 51 | shouldError: true, 52 | }, 53 | } 54 | 55 | for _, tt := range tests { 56 | t.Run(tt.name, func(t *testing.T) { 57 | tmpfile, err := os.CreateTemp("", "test_namespace_*.go") 58 | require.NoError(t, err, "Failed to create temp file") 59 | defer func() { 60 | err := os.Remove(tmpfile.Name()) 61 | assert.NoError(t, err, "Failed to remove temp file: %v", err) 62 | }() 63 | 64 | _, err = tmpfile.Write([]byte(tt.content)) 65 | require.NoError(t, err, "Failed to write to temp file") 66 | 67 | err = tmpfile.Close() 68 | require.NoError(t, err, "Failed to close temp file") 69 | 70 | parser := NamespaceParser{} 71 | result, err := parser.parse(tmpfile.Name()) 72 | 73 | if tt.shouldError { 74 | require.Error(t, err, "expected error but got none") 75 | return 76 | } 77 | require.NoError(t, err, "unexpected error") 78 | require.Equal(t, tt.expected, result, "expected %q, got %q", tt.expected, result) 79 | }) 80 | } 81 | } 82 | 83 | func TestGeneratorWithNamespace(t *testing.T) { 84 | content := `package main 85 | 86 | //export_php:namespace My\Test\Namespace 87 | 88 | //export_php:function hello(): string 89 | func hello() string { 90 | return "Hello from namespace!" 91 | } 92 | 93 | //export_php:constant TEST_CONSTANT = "test_value" 94 | const TEST_CONSTANT = "test_value" 95 | ` 96 | 97 | tmpfile, err := os.CreateTemp("", "test_generator_namespace_*.go") 98 | require.NoError(t, err, "Failed to create temp file") 99 | defer func() { 100 | if err := os.Remove(tmpfile.Name()); err != nil { 101 | t.Logf("Failed to remove temp file: %v", err) 102 | } 103 | }() 104 | 105 | _, err = tmpfile.Write([]byte(content)) 106 | require.NoError(t, err, "Failed to write to temp file") 107 | 108 | err = tmpfile.Close() 109 | require.NoError(t, err, "Failed to close temp file") 110 | 111 | parser := SourceParser{} 112 | namespace, err := parser.ParseNamespace(tmpfile.Name()) 113 | require.NoErrorf(t, err, "Failed to parse namespace from %s: %v", tmpfile.Name(), err) 114 | 115 | require.Equal(t, `My\Test\Namespace`, namespace, "Namespace should match the parsed namespace") 116 | 117 | generator := &Generator{ 118 | SourceFile: tmpfile.Name(), 119 | Namespace: namespace, 120 | } 121 | 122 | require.Equal(t, `My\Test\Namespace`, generator.Namespace, "Namespace should match the parsed namespace") 123 | } 124 | -------------------------------------------------------------------------------- /internal/extgen/nodes.go: -------------------------------------------------------------------------------- 1 | package extgen 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // phpType represents a PHP type 9 | type phpType string 10 | 11 | const ( 12 | phpString phpType = "string" 13 | phpInt phpType = "int" 14 | phpFloat phpType = "float" 15 | phpBool phpType = "bool" 16 | phpArray phpType = "array" 17 | phpObject phpType = "object" 18 | phpMixed phpType = "mixed" 19 | phpVoid phpType = "void" 20 | phpNull phpType = "null" 21 | phpTrue phpType = "true" 22 | phpFalse phpType = "false" 23 | ) 24 | 25 | type phpFunction struct { 26 | Name string 27 | Signature string 28 | GoFunction string 29 | Params []phpParameter 30 | ReturnType phpType 31 | IsReturnNullable bool 32 | lineNumber int 33 | } 34 | 35 | type phpParameter struct { 36 | Name string 37 | PhpType phpType 38 | IsNullable bool 39 | DefaultValue string 40 | HasDefault bool 41 | } 42 | 43 | type phpClass struct { 44 | Name string 45 | GoStruct string 46 | Properties []phpClassProperty 47 | Methods []phpClassMethod 48 | } 49 | 50 | type phpClassMethod struct { 51 | Name string 52 | PhpName string 53 | Signature string 54 | GoFunction string 55 | Wrapper string 56 | Params []phpParameter 57 | ReturnType phpType 58 | isReturnNullable bool 59 | lineNumber int 60 | ClassName string // used by the "//export_php:method" directive 61 | } 62 | 63 | type phpClassProperty struct { 64 | Name string 65 | PhpType phpType 66 | GoType string 67 | IsNullable bool 68 | } 69 | 70 | type phpConstant struct { 71 | Name string 72 | Value string 73 | PhpType phpType 74 | IsIota bool 75 | lineNumber int 76 | ClassName string // empty for global constants, set for class constants 77 | } 78 | 79 | // CValue returns the constant value in C-compatible format 80 | func (c phpConstant) CValue() string { 81 | if c.PhpType != phpInt { 82 | return c.Value 83 | } 84 | 85 | if strings.HasPrefix(c.Value, "0o") { 86 | if val, err := strconv.ParseInt(c.Value, 0, 64); err == nil { 87 | return strconv.FormatInt(val, 10) 88 | } 89 | } 90 | 91 | return c.Value 92 | } 93 | -------------------------------------------------------------------------------- /internal/extgen/nsparser.go: -------------------------------------------------------------------------------- 1 | package extgen 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | type NamespaceParser struct{} 12 | 13 | var namespaceRegex = regexp.MustCompile(`//\s*export_php:namespace\s+(.+)`) 14 | 15 | func (np *NamespaceParser) parse(filename string) (string, error) { 16 | file, err := os.Open(filename) 17 | if err != nil { 18 | return "", err 19 | } 20 | defer func() { 21 | if err := file.Close(); err != nil { 22 | fmt.Printf("Error closing file %s: %v\n", filename, err) 23 | } 24 | }() 25 | 26 | var foundNamespace string 27 | var lineNumber int 28 | var foundLineNumber int 29 | 30 | scanner := bufio.NewScanner(file) 31 | for scanner.Scan() { 32 | lineNumber++ 33 | line := strings.TrimSpace(scanner.Text()) 34 | if matches := namespaceRegex.FindStringSubmatch(line); matches != nil { 35 | namespace := strings.TrimSpace(matches[1]) 36 | if foundNamespace != "" { 37 | return "", fmt.Errorf("multiple namespace declarations found: first at line %d, second at line %d", foundLineNumber, lineNumber) 38 | } 39 | foundNamespace = namespace 40 | foundLineNumber = lineNumber 41 | } 42 | } 43 | 44 | return foundNamespace, scanner.Err() 45 | } 46 | -------------------------------------------------------------------------------- /internal/extgen/parser.go: -------------------------------------------------------------------------------- 1 | package extgen 2 | 3 | type SourceParser struct{} 4 | 5 | // EXPERIMENTAL 6 | func (p *SourceParser) ParseFunctions(filename string) ([]phpFunction, error) { 7 | functionParser := &FuncParser{} 8 | return functionParser.parse(filename) 9 | } 10 | 11 | // EXPERIMENTAL 12 | func (p *SourceParser) ParseClasses(filename string) ([]phpClass, error) { 13 | classParser := classParser{} 14 | return classParser.parse(filename) 15 | } 16 | 17 | // EXPERIMENTAL 18 | func (p *SourceParser) ParseConstants(filename string) ([]phpConstant, error) { 19 | constantParser := &ConstantParser{} 20 | return constantParser.parse(filename) 21 | } 22 | 23 | // EXPERIMENTAL 24 | func (p *SourceParser) ParseNamespace(filename string) (string, error) { 25 | namespaceParser := NamespaceParser{} 26 | return namespaceParser.parse(filename) 27 | } 28 | -------------------------------------------------------------------------------- /internal/extgen/phpfunc.go: -------------------------------------------------------------------------------- 1 | package extgen 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type PHPFuncGenerator struct { 9 | paramParser *ParameterParser 10 | namespace string 11 | } 12 | 13 | func (pfg *PHPFuncGenerator) generate(fn phpFunction) string { 14 | var builder strings.Builder 15 | 16 | paramInfo := pfg.paramParser.analyzeParameters(fn.Params) 17 | 18 | funcName := NamespacedName(pfg.namespace, fn.Name) 19 | builder.WriteString(fmt.Sprintf("PHP_FUNCTION(%s)\n{\n", funcName)) 20 | 21 | if decl := pfg.paramParser.generateParamDeclarations(fn.Params); decl != "" { 22 | builder.WriteString(decl + "\n") 23 | } 24 | 25 | builder.WriteString(pfg.paramParser.generateParamParsing(fn.Params, paramInfo.RequiredCount) + "\n") 26 | 27 | builder.WriteString(pfg.generateGoCall(fn) + "\n") 28 | 29 | if returnCode := pfg.generateReturnCode(fn.ReturnType); returnCode != "" { 30 | builder.WriteString(returnCode + "\n") 31 | } 32 | 33 | builder.WriteString("}\n\n") 34 | 35 | return builder.String() 36 | } 37 | 38 | func (pfg *PHPFuncGenerator) generateGoCall(fn phpFunction) string { 39 | callParams := pfg.paramParser.generateGoCallParams(fn.Params) 40 | 41 | if fn.ReturnType == phpVoid { 42 | return fmt.Sprintf(" %s(%s);", fn.Name, callParams) 43 | } 44 | 45 | if fn.ReturnType == phpString { 46 | return fmt.Sprintf(" zend_string *result = %s(%s);", fn.Name, callParams) 47 | } 48 | 49 | if fn.ReturnType == phpArray { 50 | return fmt.Sprintf(" zend_array *result = %s(%s);", fn.Name, callParams) 51 | } 52 | 53 | return fmt.Sprintf(" %s result = %s(%s);", pfg.getCReturnType(fn.ReturnType), fn.Name, callParams) 54 | } 55 | 56 | func (pfg *PHPFuncGenerator) getCReturnType(returnType phpType) string { 57 | switch returnType { 58 | case phpString: 59 | return "zend_string*" 60 | case phpInt: 61 | return "long" 62 | case phpFloat: 63 | return "double" 64 | case phpBool: 65 | return "int" 66 | case phpArray: 67 | return "zend_array*" 68 | default: 69 | return "void" 70 | } 71 | } 72 | 73 | func (pfg *PHPFuncGenerator) generateReturnCode(returnType phpType) string { 74 | switch returnType { 75 | case phpString: 76 | return ` if (result) { 77 | RETURN_STR(result); 78 | } 79 | 80 | RETURN_EMPTY_STRING();` 81 | case phpInt: 82 | return ` RETURN_LONG(result);` 83 | case phpFloat: 84 | return ` RETURN_DOUBLE(result);` 85 | case phpBool: 86 | return ` RETURN_BOOL(result);` 87 | case phpArray: 88 | return ` if (result) { 89 | RETURN_ARR(result); 90 | } 91 | 92 | RETURN_EMPTY_ARRAY();` 93 | default: 94 | return "" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /internal/extgen/srcanalyzer.go: -------------------------------------------------------------------------------- 1 | package extgen 2 | 3 | import ( 4 | "fmt" 5 | "go/parser" 6 | "go/token" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | type SourceAnalyzer struct{} 12 | 13 | func (sa *SourceAnalyzer) analyze(filename string) (imports []string, internalFunctions []string, err error) { 14 | fset := token.NewFileSet() 15 | node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) 16 | if err != nil { 17 | return nil, nil, fmt.Errorf("parsing file: %w", err) 18 | } 19 | 20 | for _, imp := range node.Imports { 21 | if imp.Path != nil { 22 | importPath := imp.Path.Value 23 | if imp.Name != nil { 24 | imports = append(imports, fmt.Sprintf("%s %s", imp.Name.Name, importPath)) 25 | } else { 26 | imports = append(imports, importPath) 27 | } 28 | } 29 | } 30 | 31 | sourceContent, err := os.ReadFile(filename) 32 | if err != nil { 33 | return nil, nil, fmt.Errorf("reading source file: %w", err) 34 | } 35 | 36 | internalFunctions = sa.extractInternalFunctions(string(sourceContent)) 37 | 38 | return imports, internalFunctions, nil 39 | } 40 | 41 | func (sa *SourceAnalyzer) extractInternalFunctions(content string) []string { 42 | lines := strings.Split(content, "\n") 43 | var ( 44 | functions []string 45 | currentFunc strings.Builder 46 | inFunction, hasPHPFunc bool 47 | braceCount int 48 | ) 49 | 50 | for i, line := range lines { 51 | trimmedLine := strings.TrimSpace(line) 52 | 53 | if strings.HasPrefix(trimmedLine, "func ") && !inFunction { 54 | inFunction = true 55 | braceCount = 0 56 | hasPHPFunc = false 57 | currentFunc.Reset() 58 | 59 | // look backwards for export_php comment 60 | for j := i - 1; j >= 0 && j >= i-5; j-- { 61 | prevLine := strings.TrimSpace(lines[j]) 62 | if prevLine == "" { 63 | continue 64 | } 65 | 66 | if strings.Contains(prevLine, "export_php:") { 67 | hasPHPFunc = true 68 | 69 | break 70 | } 71 | 72 | if !strings.HasPrefix(prevLine, "//") { 73 | break 74 | } 75 | } 76 | } 77 | 78 | if inFunction { 79 | currentFunc.WriteString(line + "\n") 80 | 81 | for _, char := range line { 82 | switch char { 83 | case '{': 84 | braceCount++ 85 | case '}': 86 | braceCount-- 87 | } 88 | } 89 | 90 | if braceCount == 0 && strings.Contains(line, "}") { 91 | funcContent := currentFunc.String() 92 | 93 | if !hasPHPFunc { 94 | functions = append(functions, strings.TrimSpace(funcContent)) 95 | } 96 | 97 | inFunction = false 98 | currentFunc.Reset() 99 | } 100 | } 101 | } 102 | 103 | return functions 104 | } 105 | -------------------------------------------------------------------------------- /internal/extgen/stub.go: -------------------------------------------------------------------------------- 1 | package extgen 2 | 3 | import ( 4 | _ "embed" 5 | "path/filepath" 6 | "strings" 7 | "text/template" 8 | ) 9 | 10 | //go:embed templates/stub.php.tpl 11 | var templateContent string 12 | 13 | type StubGenerator struct { 14 | Generator *Generator 15 | } 16 | 17 | func (sg *StubGenerator) generate() error { 18 | filename := filepath.Join(sg.Generator.BuildDir, sg.Generator.BaseName+".stub.php") 19 | content, err := sg.buildContent() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | return writeFile(filename, content) 25 | } 26 | 27 | func (sg *StubGenerator) buildContent() (string, error) { 28 | tmpl, err := template.New("stub.php.tpl").Funcs(template.FuncMap{ 29 | "phpType": getPhpTypeAnnotation, 30 | }).Parse(templateContent) 31 | if err != nil { 32 | return "", err 33 | } 34 | 35 | var buf strings.Builder 36 | if err := tmpl.Execute(&buf, sg.Generator); err != nil { 37 | return "", err 38 | } 39 | 40 | return buf.String(), nil 41 | } 42 | 43 | // getPhpTypeAnnotation converts phpType to PHP type annotation 44 | func getPhpTypeAnnotation(t phpType) string { 45 | switch t { 46 | case phpString: 47 | return "string" 48 | case phpBool: 49 | return "bool" 50 | case phpFloat: 51 | return "float" 52 | case phpInt: 53 | return "int" 54 | case phpArray: 55 | return "array" 56 | default: 57 | return "int" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/extgen/templates/README.md.tpl: -------------------------------------------------------------------------------- 1 | # {{.BaseName}} Extension 2 | 3 | Auto-generated PHP extension from Go code. 4 | 5 | {{if .Functions}}## Functions 6 | 7 | {{range .Functions}}### {{.Name}} 8 | 9 | ```php 10 | {{.Signature}} 11 | ``` 12 | 13 | {{if .Params}}**Parameters:** 14 | 15 | {{range .Params}}- `{{.Name}}` ({{.PhpType}}){{if .IsNullable}} (nullable){{end}}{{if .HasDefault}} (default: {{.DefaultValue}}){{end}} 16 | {{end}} 17 | {{end}}**Returns:** {{.ReturnType}}{{if .IsReturnNullable}} (nullable){{end}} 18 | 19 | {{end}}{{end}}{{if .Classes}}## Classes 20 | 21 | {{range .Classes}}### {{.Name}} 22 | 23 | {{if .Properties}}**Properties:** 24 | 25 | {{range .Properties}}- `{{.Name}}`: {{.PhpType}}{{if .IsNullable}} (nullable){{end}} 26 | {{end}} 27 | {{end}}{{end}}{{end}} 28 | -------------------------------------------------------------------------------- /internal/extgen/templates/extension.go.tpl: -------------------------------------------------------------------------------- 1 | package {{.PackageName}} 2 | 3 | /* 4 | #include <stdlib.h> 5 | #include "{{.BaseName}}.h" 6 | */ 7 | import "C" 8 | import "runtime/cgo" 9 | {{- range .Imports}} 10 | import {{.}} 11 | {{- end}} 12 | 13 | func init() { 14 | frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry)) 15 | } 16 | {{range .Constants}} 17 | const {{.Name}} = {{.Value}} 18 | {{- end}} 19 | {{range .InternalFunctions}} 20 | {{.}} 21 | {{- end}} 22 | 23 | {{- range .Functions}} 24 | //export {{.Name}} 25 | {{.GoFunction}} 26 | {{- end}} 27 | 28 | {{- range .Classes}} 29 | type {{.GoStruct}} struct { 30 | {{- range .Properties}} 31 | {{.Name}} {{.GoType}} 32 | {{- end}} 33 | } 34 | {{- end}} 35 | 36 | {{- if .Classes}} 37 | 38 | //export registerGoObject 39 | func registerGoObject(obj interface{}) C.uintptr_t { 40 | handle := cgo.NewHandle(obj) 41 | return C.uintptr_t(handle) 42 | } 43 | 44 | //export getGoObject 45 | func getGoObject(handle C.uintptr_t) interface{} { 46 | h := cgo.Handle(handle) 47 | return h.Value() 48 | } 49 | 50 | //export removeGoObject 51 | func removeGoObject(handle C.uintptr_t) { 52 | h := cgo.Handle(handle) 53 | h.Delete() 54 | } 55 | 56 | {{- end}} 57 | 58 | {{- range $class := .Classes}} 59 | //export create_{{.GoStruct}}_object 60 | func create_{{.GoStruct}}_object() C.uintptr_t { 61 | obj := &{{.GoStruct}}{} 62 | return registerGoObject(obj) 63 | } 64 | 65 | {{- range .Methods}} 66 | {{- if .GoFunction}} 67 | {{.GoFunction}} 68 | {{- end}} 69 | {{- end}} 70 | 71 | {{- range .Methods}} 72 | //export {{.Name}}_wrapper 73 | func {{.Name}}_wrapper(handle C.uintptr_t{{range .Params}}{{if eq .PhpType "string"}}, {{.Name}} *C.zend_string{{else if eq .PhpType "array"}}, {{.Name}} *C.zval{{else}}, {{.Name}} {{if .IsNullable}}*{{end}}{{phpTypeToGoType .PhpType}}{{end}}{{end}}){{if not (isVoid .ReturnType)}}{{if isStringOrArray .ReturnType}} unsafe.Pointer{{else}} {{phpTypeToGoType .ReturnType}}{{end}}{{end}} { 74 | obj := getGoObject(handle) 75 | if obj == nil { 76 | {{- if not (isVoid .ReturnType)}} 77 | {{- if isStringOrArray .ReturnType}} 78 | return nil 79 | {{- else}} 80 | var zero {{phpTypeToGoType .ReturnType}} 81 | return zero 82 | {{- end}} 83 | {{- else}} 84 | return 85 | {{- end}} 86 | } 87 | structObj := obj.(*{{$class.GoStruct}}) 88 | {{if not (isVoid .ReturnType)}}return {{end}}structObj.{{.Name | title}}({{range $i, $param := .Params}}{{if $i}}, {{end}}{{$param.Name}}{{end}}) 89 | } 90 | {{end}} 91 | {{- end}} 92 | -------------------------------------------------------------------------------- /internal/extgen/templates/extension.h.tpl: -------------------------------------------------------------------------------- 1 | #ifndef _{{.HeaderGuard}} 2 | #define _{{.HeaderGuard}} 3 | 4 | #include <php.h> 5 | #include <stdint.h> 6 | 7 | extern zend_module_entry ext_module_entry; 8 | 9 | {{if .Constants}} 10 | /* User defined constants */{{end}} 11 | {{range .Constants}}#define {{.Name}} {{.CValue}} 12 | {{end}} 13 | #endif 14 | -------------------------------------------------------------------------------- /internal/extgen/templates/stub.php.tpl: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | /** @generate-class-entries */ 4 | {{if .Namespace}} 5 | namespace {{.Namespace}}; 6 | {{end}} 7 | {{range .Constants}}{{if eq .ClassName ""}}{{if .IsIota}}/** 8 | * @var int 9 | * @cvalue {{.Name}} 10 | */ 11 | const {{.Name}} = UNKNOWN; 12 | 13 | {{else}}/** 14 | * @var {{phpType .PhpType}} 15 | */ 16 | const {{.Name}} = {{.Value}}; 17 | 18 | {{end}}{{end}}{{end}}{{range .Functions}}function {{.Signature}} {} 19 | 20 | {{end}}{{range .Classes}}{{$className := .Name}}class {{.Name}} { 21 | {{range $.Constants}}{{if eq .ClassName $className}}{{if .IsIota}} /** 22 | * @var int 23 | * @cvalue {{.Name}} 24 | */ 25 | public const {{.Name}} = UNKNOWN; 26 | 27 | {{else}} /** 28 | * @var {{phpType .PhpType}} 29 | */ 30 | public const {{.Name}} = {{.Value}}; 31 | 32 | {{end}}{{end}}{{end}} 33 | public function __construct() {} 34 | {{range .Methods}} 35 | public function {{.Signature}} {} 36 | {{end}} 37 | } 38 | 39 | {{end}} 40 | -------------------------------------------------------------------------------- /internal/extgen/utils.go: -------------------------------------------------------------------------------- 1 | package extgen 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "unicode" 7 | ) 8 | 9 | func writeFile(filename, content string) error { 10 | return os.WriteFile(filename, []byte(content), 0644) 11 | } 12 | 13 | func readFile(filename string) (string, error) { 14 | content, err := os.ReadFile(filename) 15 | if err != nil { 16 | return "", err 17 | } 18 | 19 | return string(content), nil 20 | } 21 | 22 | // NamespacedName converts a namespace and name to a C-compatible format. 23 | // E.g., namespace "Go\Extension" and name "MyClass" become "Go_Extension_MyClass". 24 | // This symbol remains exported, so it's usable in templates. 25 | func NamespacedName(namespace, name string) string { 26 | if namespace == "" { 27 | return name 28 | } 29 | namespacePart := strings.ReplaceAll(namespace, "\\", "_") 30 | return namespacePart + "_" + name 31 | } 32 | 33 | // EXPERIMENTAL 34 | func SanitizePackageName(name string) string { 35 | sanitized := strings.ReplaceAll(name, "-", "_") 36 | sanitized = strings.ReplaceAll(sanitized, ".", "_") 37 | 38 | if len(sanitized) > 0 && !unicode.IsLetter(rune(sanitized[0])) && sanitized[0] != '_' { 39 | sanitized = "_" + sanitized 40 | } 41 | 42 | return sanitized 43 | } 44 | -------------------------------------------------------------------------------- /internal/extgen/utils_namespace_test.go: -------------------------------------------------------------------------------- 1 | package extgen 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "testing" 6 | ) 7 | 8 | func TestNamespacedName(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | namespace string 12 | itemName string 13 | expected string 14 | }{ 15 | { 16 | name: "no namespace", 17 | namespace: "", 18 | itemName: "TestItem", 19 | expected: "TestItem", 20 | }, 21 | { 22 | name: "single level namespace", 23 | namespace: "MyNamespace", 24 | itemName: "TestItem", 25 | expected: "MyNamespace_TestItem", 26 | }, 27 | { 28 | name: "multi level namespace", 29 | namespace: `Go\Extension`, 30 | itemName: "TestItem", 31 | expected: "Go_Extension_TestItem", 32 | }, 33 | { 34 | name: "deep namespace", 35 | namespace: `Very\Deep\Nested\Namespace`, 36 | itemName: "MyItem", 37 | expected: "Very_Deep_Nested_Namespace_MyItem", 38 | }, 39 | { 40 | name: "function name", 41 | namespace: `Go\Extension`, 42 | itemName: "multiply", 43 | expected: "Go_Extension_multiply", 44 | }, 45 | { 46 | name: "class name", 47 | namespace: `Go\Extension`, 48 | itemName: "MySuperClass", 49 | expected: "Go_Extension_MySuperClass", 50 | }, 51 | } 52 | 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | result := NamespacedName(tt.namespace, tt.itemName) 56 | require.Equal(t, tt.expected, result, "NamespacedName(%q, %q) = %q, expected %q", tt.namespace, tt.itemName, result, tt.expected) 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/fastabs/filepath.go: -------------------------------------------------------------------------------- 1 | //go:build !unix 2 | 3 | package fastabs 4 | 5 | import ( 6 | "path/filepath" 7 | ) 8 | 9 | // FastAbs can't be optimized on Windows because the 10 | // syscall.FullPath function takes an input. 11 | func FastAbs(path string) (string, error) { 12 | return filepath.Abs(path) 13 | } 14 | -------------------------------------------------------------------------------- /internal/fastabs/filepath_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package fastabs 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | // FastAbs is an optimized version of filepath.Abs for Unix systems, 11 | // since we don't expect the working directory to ever change once 12 | // Caddy is running. Avoid the os.Getwd syscall overhead. 13 | func FastAbs(path string) (string, error) { 14 | if filepath.IsAbs(path) { 15 | return filepath.Clean(path), nil 16 | } 17 | 18 | if wderr != nil { 19 | return "", wderr 20 | } 21 | 22 | return filepath.Join(wd, path), nil 23 | } 24 | 25 | var wd, wderr = os.Getwd() 26 | -------------------------------------------------------------------------------- /internal/memory/memory_linux.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import "syscall" 4 | 5 | func TotalSysMemory() uint64 { 6 | sysInfo := &syscall.Sysinfo_t{} 7 | err := syscall.Sysinfo(sysInfo) 8 | if err != nil { 9 | return 0 10 | } 11 | 12 | return uint64(sysInfo.Totalram) * uint64(sysInfo.Unit) 13 | } 14 | -------------------------------------------------------------------------------- /internal/memory/memory_others.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package memory 4 | 5 | // TotalSysMemory returns 0 if the total system memory cannot be determined 6 | func TotalSysMemory() uint64 { 7 | return 0 8 | } 9 | -------------------------------------------------------------------------------- /internal/testcli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/dunglas/frankenphp" 8 | ) 9 | 10 | func main() { 11 | if len(os.Args) <= 1 { 12 | log.Println("Usage: testcli script.php") 13 | os.Exit(1) 14 | } 15 | 16 | if len(os.Args) == 3 && os.Args[1] == "-r" { 17 | os.Exit(frankenphp.ExecutePHPCode(os.Args[2])) 18 | } 19 | 20 | os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args)) 21 | } 22 | -------------------------------------------------------------------------------- /internal/testext/ext_test.go: -------------------------------------------------------------------------------- 1 | package testext 2 | 3 | import "testing" 4 | 5 | func TestRegisterExtension(t *testing.T) { 6 | testRegisterExtension(t) 7 | } 8 | -------------------------------------------------------------------------------- /internal/testext/extension.h: -------------------------------------------------------------------------------- 1 | #ifndef _EXTENSIONS_H 2 | #define _EXTENSIONS_H 3 | 4 | #include <php.h> 5 | 6 | extern zend_module_entry module1_entry; 7 | extern zend_module_entry module2_entry; 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /internal/testext/extensions.c: -------------------------------------------------------------------------------- 1 | #include <php.h> 2 | #include <zend_exceptions.h> 3 | 4 | #include "_cgo_export.h" 5 | 6 | zend_module_entry module1_entry = {STANDARD_MODULE_HEADER, 7 | "ext1", 8 | NULL, /* Functions */ 9 | NULL, /* MINIT */ 10 | NULL, /* MSHUTDOWN */ 11 | NULL, /* RINIT */ 12 | NULL, /* RSHUTDOWN */ 13 | NULL, /* MINFO */ 14 | "0.1.0", 15 | STANDARD_MODULE_PROPERTIES}; 16 | 17 | zend_module_entry module2_entry = {STANDARD_MODULE_HEADER, 18 | "ext2", 19 | NULL, /* Functions */ 20 | NULL, /* MINIT */ 21 | NULL, /* MSHUTDOWN */ 22 | NULL, /* RINIT */ 23 | NULL, /* RSHUTDOWN */ 24 | NULL, /* MINFO */ 25 | "0.1.0", 26 | STANDARD_MODULE_PROPERTIES}; 27 | -------------------------------------------------------------------------------- /internal/testext/exttest.go: -------------------------------------------------------------------------------- 1 | package testext 2 | 3 | // #cgo darwin pkg-config: libxml-2.0 4 | // #cgo CFLAGS: -Wall -Werror 5 | // #cgo CFLAGS: -I/usr/local/include -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib 6 | // #cgo linux CFLAGS: -D_GNU_SOURCE 7 | // #cgo darwin CFLAGS: -I/opt/homebrew/include 8 | // #cgo LDFLAGS: -L/usr/local/lib -L/usr/lib -lphp -lm -lutil 9 | // #cgo linux LDFLAGS: -ldl -lresolv 10 | // #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -L/opt/homebrew/lib -L/opt/homebrew/opt/libiconv/lib -liconv -ldl 11 | // #include "extension.h" 12 | import "C" 13 | import ( 14 | "github.com/dunglas/frankenphp" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/require" 17 | "io" 18 | "net/http/httptest" 19 | "testing" 20 | "unsafe" 21 | ) 22 | 23 | func testRegisterExtension(t *testing.T) { 24 | frankenphp.RegisterExtension(unsafe.Pointer(&C.module1_entry)) 25 | frankenphp.RegisterExtension(unsafe.Pointer(&C.module2_entry)) 26 | 27 | err := frankenphp.Init() 28 | require.Nil(t, err) 29 | defer frankenphp.Shutdown() 30 | 31 | req := httptest.NewRequest("GET", "http://example.com/index.php", nil) 32 | w := httptest.NewRecorder() 33 | 34 | req, err = frankenphp.NewRequestWithContext(req, frankenphp.WithRequestDocumentRoot("./testdata", false)) 35 | assert.NoError(t, err) 36 | 37 | err = frankenphp.ServeHTTP(w, req) 38 | assert.NoError(t, err) 39 | 40 | resp := w.Result() 41 | body, _ := io.ReadAll(resp.Body) 42 | assert.Contains(t, string(body), "ext1") 43 | assert.Contains(t, string(body), "ext2") 44 | } 45 | -------------------------------------------------------------------------------- /internal/testext/testdata/index.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | print_r(get_loaded_extensions()); 4 | -------------------------------------------------------------------------------- /internal/testserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/dunglas/frankenphp" 10 | ) 11 | 12 | func main() { 13 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) 14 | if err := frankenphp.Init(frankenphp.WithLogger(logger)); err != nil { 15 | panic(err) 16 | } 17 | defer frankenphp.Shutdown() 18 | 19 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 20 | req, err := frankenphp.NewRequestWithContext(r) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | if err := frankenphp.ServeHTTP(w, req); err != nil { 26 | panic(err) 27 | } 28 | }) 29 | 30 | port := os.Getenv("PORT") 31 | if port == "" { 32 | port = "8080" 33 | } 34 | 35 | logger.LogAttrs(context.Background(), slog.LevelError, "server error", slog.Any("error", http.ListenAndServe(":"+port, nil))) 36 | os.Exit(1) 37 | } 38 | -------------------------------------------------------------------------------- /internal/watcher/watcher-skip.go: -------------------------------------------------------------------------------- 1 | //go:build nowatcher 2 | 3 | package watcher 4 | 5 | import "log/slog" 6 | 7 | func InitWatcher(filePatterns []string, callback func(), logger *slog.Logger) error { 8 | logger.Error("watcher support is not enabled") 9 | 10 | return nil 11 | } 12 | 13 | func DrainWatcher() { 14 | } 15 | -------------------------------------------------------------------------------- /internal/watcher/watcher.c: -------------------------------------------------------------------------------- 1 | // clang-format off 2 | //go:build !nowatcher 3 | // clang-format on 4 | #include "_cgo_export.h" 5 | #include "wtr/watcher-c.h" 6 | 7 | void handle_event(struct wtr_watcher_event event, void *data) { 8 | go_handle_file_watcher_event( 9 | (char *)event.path_name, (char *)event.associated_path_name, 10 | event.effect_type, event.path_type, (uintptr_t)data); 11 | } 12 | 13 | uintptr_t start_new_watcher(char const *const path, uintptr_t data) { 14 | void *watcher = wtr_watcher_open(path, handle_event, (void *)data); 15 | if (watcher == NULL) { 16 | return 0; 17 | } 18 | return (uintptr_t)watcher; 19 | } 20 | 21 | int stop_watcher(uintptr_t watcher) { 22 | if (!wtr_watcher_close((void *)watcher)) { 23 | return 0; 24 | } 25 | return 1; 26 | } 27 | -------------------------------------------------------------------------------- /internal/watcher/watcher.h: -------------------------------------------------------------------------------- 1 | #include <stdint.h> 2 | #include <stdlib.h> 3 | 4 | uintptr_t start_new_watcher(char const *const path, uintptr_t data); 5 | 6 | int stop_watcher(uintptr_t watcher); 7 | -------------------------------------------------------------------------------- /package/Caddyfile: -------------------------------------------------------------------------------- 1 | # The Caddyfile is an easy way to configure FrankenPHP and the Caddy web server. 2 | # 3 | # https://frankenphp.dev/docs/config 4 | # https://caddyserver.com/docs/caddyfile 5 | { 6 | frankenphp 7 | } 8 | 9 | http:// { 10 | root /usr/share/frankenphp/ 11 | encode zstd br gzip 12 | 13 | php_server 14 | } 15 | 16 | # As an alternative to editing the above site block, you can add your own site 17 | # block files in the Caddyfile.d directory, and they will be included as long 18 | # as they use the .caddyfile extension. 19 | import Caddyfile.d/*.caddyfile 20 | -------------------------------------------------------------------------------- /package/debian/frankenphp.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=FrankenPHP 3 | Documentation=https://frankenphp.dev/docs/ 4 | After=network.target network-online.target 5 | Requires=network-online.target 6 | 7 | [Service] 8 | Type=notify 9 | User=frankenphp 10 | Group=frankenphp 11 | ExecStart=/usr/bin/frankenphp run --environ --config /etc/frankenphp/Caddyfile 12 | ExecReload=/usr/bin/frankenphp reload --config /etc/frankenphp/Caddyfile --force 13 | TimeoutStopSec=5s 14 | LimitNOFILE=1048576 15 | LimitNPROC=512 16 | PrivateTmp=true 17 | ProtectSystem=full 18 | AmbientCapabilities=CAP_NET_BIND_SERVICE 19 | 20 | [Install] 21 | WantedBy=multi-user.target 22 | -------------------------------------------------------------------------------- /package/debian/postinst.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ "$1" = "configure" ]; then 5 | # Add user and group 6 | if ! getent group frankenphp >/dev/null; then 7 | groupadd --system frankenphp 8 | fi 9 | if ! getent passwd frankenphp >/dev/null; then 10 | useradd --system \ 11 | --gid frankenphp \ 12 | --create-home \ 13 | --home-dir /var/lib/frankenphp \ 14 | --shell /usr/sbin/nologin \ 15 | --comment "FrankenPHP web server" \ 16 | frankenphp 17 | fi 18 | if getent group www-data >/dev/null; then 19 | usermod -aG www-data frankenphp 20 | fi 21 | 22 | # Handle cases where package was installed and then purged; 23 | # user and group will still exist but with no home dir 24 | if [ ! -d /var/lib/frankenphp ]; then 25 | mkdir -p /var/lib/frankenphp 26 | chown frankenphp:frankenphp /var/lib/frankenphp 27 | fi 28 | 29 | # Add log directory with correct permissions 30 | if [ ! -d /var/log/frankenphp ]; then 31 | mkdir -p /var/log/frankenphp 32 | chown frankenphp:frankenphp /var/log/frankenphp 33 | fi 34 | fi 35 | 36 | if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ]; then 37 | # This will only remove masks created by d-s-h on package removal. 38 | deb-systemd-helper unmask frankenphp.service >/dev/null || true 39 | 40 | # was-enabled defaults to true, so new installations run enable. 41 | if deb-systemd-helper --quiet was-enabled frankenphp.service; then 42 | # Enables the unit on first installation, creates new 43 | # symlinks on upgrades if the unit file has changed. 44 | deb-systemd-helper enable frankenphp.service >/dev/null || true 45 | deb-systemd-invoke start frankenphp.service >/dev/null || true 46 | else 47 | # Update the statefile to add new symlinks (if any), which need to be 48 | # cleaned up on purge. Also remove old symlinks. 49 | deb-systemd-helper update-state frankenphp.service >/dev/null || true 50 | fi 51 | 52 | # Restart only if it was already started 53 | if [ -d /run/systemd/system ]; then 54 | systemctl --system daemon-reload >/dev/null || true 55 | if [ -n "$2" ]; then 56 | deb-systemd-invoke try-restart frankenphp.service >/dev/null || true 57 | fi 58 | fi 59 | fi 60 | 61 | if command -v setcap >/dev/null 2>&1; then 62 | setcap cap_net_bind_service=+ep /usr/bin/frankenphp || true 63 | fi 64 | 65 | if [ -x /usr/bin/frankenphp ]; then 66 | HOME=/var/lib/frankenphp /usr/bin/frankenphp trust || true 67 | fi 68 | -------------------------------------------------------------------------------- /package/debian/postrm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ -d /run/systemd/system ]; then 5 | systemctl --system daemon-reload >/dev/null || true 6 | fi 7 | 8 | if [ "$1" = "remove" ]; then 9 | if [ -x "/usr/bin/deb-systemd-helper" ]; then 10 | deb-systemd-helper mask frankenphp.service >/dev/null || true 11 | fi 12 | fi 13 | 14 | if [ "$1" = "purge" ]; then 15 | if [ -x "/usr/bin/deb-systemd-helper" ]; then 16 | deb-systemd-helper purge frankenphp.service >/dev/null || true 17 | deb-systemd-helper unmask frankenphp.service >/dev/null || true 18 | fi 19 | rm -rf /var/lib/frankenphp /var/log/frankenphp /etc/frankenphp 20 | fi 21 | -------------------------------------------------------------------------------- /package/debian/prerm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ -d /run/systemd/system ] && [ "$1" = remove ]; then 5 | deb-systemd-invoke stop frankenphp.service >/dev/null || true 6 | fi 7 | -------------------------------------------------------------------------------- /package/rhel/frankenphp.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=FrankenPHP server 3 | After=network.target 4 | 5 | [Service] 6 | Type=notify 7 | User=frankenphp 8 | Group=frankenphp 9 | ExecStartPre=/usr/bin/frankenphp validate --config /etc/frankenphp/Caddyfile 10 | ExecStart=/usr/bin/frankenphp run --environ --config /etc/frankenphp/Caddyfile 11 | ExecReload=/usr/bin/frankenphp reload --config /etc/frankenphp/Caddyfile 12 | TimeoutStopSec=5s 13 | LimitNOFILE=1048576 14 | LimitNPROC=512 15 | PrivateTmp=true 16 | ProtectHome=true 17 | ProtectSystem=full 18 | AmbientCapabilities=CAP_NET_BIND_SERVICE 19 | 20 | [Install] 21 | WantedBy=multi-user.target 22 | -------------------------------------------------------------------------------- /package/rhel/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$1" -eq 1 ] && [ -x "/usr/lib/systemd/systemd-update-helper" ]; then 4 | # Initial installation 5 | /usr/lib/systemd/systemd-update-helper install-system-units frankenphp.service || : 6 | fi 7 | 8 | if [ -x /usr/sbin/getsebool ]; then 9 | # Connect to ACME endpoint to request certificates 10 | setsebool -P httpd_can_network_connect on 11 | fi 12 | 13 | if [ -x /usr/sbin/semanage ] && [ -x /usr/sbin/restorecon ]; then 14 | # file contexts 15 | semanage fcontext --add --type httpd_exec_t '/usr/bin/frankenphp' 2>/dev/null || : 16 | semanage fcontext --add --type httpd_sys_content_t '/usr/share/frankenphp(/.*)?' 2>/dev/null || : 17 | semanage fcontext --add --type httpd_config_t '/etc/frankenphp(/.*)?' 2>/dev/null || : 18 | semanage fcontext --add --type httpd_var_lib_t '/var/lib/frankenphp(/.*)?' 2>/dev/null || : 19 | restorecon -r /usr/bin/frankenphp /usr/share/frankenphp /etc/frankenphp /var/lib/frankenphp || : 20 | fi 21 | 22 | if [ -x /usr/sbin/semanage ]; then 23 | # QUIC 24 | semanage port --add --type http_port_t --proto udp 80 2>/dev/null || : 25 | semanage port --add --type http_port_t --proto udp 443 2>/dev/null || : 26 | # admin endpoint 27 | semanage port --add --type http_port_t --proto tcp 2019 2>/dev/null || : 28 | fi 29 | 30 | if command -v setcap >/dev/null 2>&1; then 31 | setcap cap_net_bind_service=+ep /usr/bin/frankenphp || : 32 | fi 33 | 34 | if [ -x /usr/bin/frankenphp ]; then 35 | HOME=/var/lib/frankenphp /usr/bin/frankenphp trust || : 36 | fi 37 | -------------------------------------------------------------------------------- /package/rhel/postuninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$1" -ge 1 ] && [ -x "/usr/lib/systemd/systemd-update-helper" ]; then 4 | # Package upgrade, not uninstall 5 | /usr/lib/systemd/systemd-update-helper mark-restart-system-units frankenphp.service || : 6 | fi 7 | 8 | if [ "$1" -eq 0 ]; then 9 | if [ -x /usr/sbin/getsebool ]; then 10 | # connect to ACME endpoint to request certificates 11 | setsebool -P httpd_can_network_connect off 12 | fi 13 | if [ -x /usr/sbin/semanage ]; then 14 | # file contexts 15 | semanage fcontext --delete --type httpd_exec_t '/usr/bin/frankenphp' 2>/dev/null || : 16 | semanage fcontext --delete --type httpd_sys_content_t '/usr/share/frankenphp(/.*)?' 2>/dev/null || : 17 | semanage fcontext --delete --type httpd_config_t '/etc/frankenphp(/.*)?' 2>/dev/null || : 18 | semanage fcontext --delete --type httpd_var_lib_t '/var/lib/frankenphp(/.*)?' 2>/dev/null || : 19 | # QUIC 20 | semanage port --delete --type http_port_t --proto udp 80 2>/dev/null || : 21 | semanage port --delete --type http_port_t --proto udp 443 2>/dev/null || : 22 | # admin endpoint 23 | semanage port --delete --type http_port_t --proto tcp 2019 2>/dev/null || : 24 | fi 25 | fi 26 | -------------------------------------------------------------------------------- /package/rhel/preinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | getent group frankenphp &>/dev/null || 4 | groupadd -r frankenphp &>/dev/null 5 | getent passwd frankenphp &>/dev/null || 6 | useradd -r -g frankenphp -d /var/lib/frankenphp -s /sbin/nologin -c 'FrankenPHP web server' frankenphp &>/dev/null 7 | exit 0 8 | -------------------------------------------------------------------------------- /package/rhel/preuninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$1" -eq 0 ] && [ -x "/usr/lib/systemd/systemd-update-helper" ]; then 4 | # Package removal, not upgrade 5 | /usr/lib/systemd/systemd-update-helper remove-system-units frankenphp.service || : 6 | fi 7 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Creates the tags for the library and the Caddy module. 4 | 5 | set -o nounset 6 | set -o errexit 7 | trap 'echo "Aborting due to errexit on line $LINENO. Exit code: $?" >&2' ERR 8 | set -o errtrace 9 | set -o pipefail 10 | set -o xtrace 11 | 12 | if ! type "git" >/dev/null; then 13 | echo "The \"git\" command must be installed." 14 | exit 1 15 | fi 16 | 17 | if ! type "gh" >/dev/null; then 18 | echo "The \"gh\" command must be installed." 19 | exit 1 20 | fi 21 | 22 | if ! type "brew" >/dev/null; then 23 | echo "The \"brew\" command must be installed." 24 | exit 1 25 | fi 26 | 27 | if [[ $# -ne 1 ]]; then 28 | echo "Usage: ./release.sh version" >&2 29 | exit 1 30 | fi 31 | 32 | # Adapted from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string 33 | if [[ ! $1 =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then 34 | echo "Invalid version number: $1" >&2 35 | exit 1 36 | fi 37 | 38 | git checkout main 39 | git pull 40 | 41 | cd caddy/ 42 | go get "github.com/dunglas/frankenphp@v$1" 43 | cd - 44 | 45 | git commit -S -a -m "chore: prepare release $1" || echo "skip" 46 | 47 | git tag -s -m "Version $1" "v$1" 48 | git tag -s -m "Version $1" "caddy/v$1" 49 | git push --follow-tags 50 | 51 | tags=$(git tag --list --sort=-version:refname 'v*') 52 | previous_tag=$(awk 'NR==2 {print;exit}' <<<"${tags}") 53 | 54 | gh release create --draft --generate-notes --latest --notes-start-tag "${previous_tag}" --verify-tag "v$1" 55 | brew bump-formula-pr dunglas/frankenphp/frankenphp --version "$1" 56 | -------------------------------------------------------------------------------- /reload_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | for ((i = 0; i < 100; i++)); do 3 | curl --no-progress-meter -o /dev/null http://localhost:2019/config/apps/frankenphp -: --no-progress-meter -o /dev/null -H 'Cache-Control: must-revalidate' -H 'Content-Type: application/json' --data-binary '{"workers":[{"file_name":"./index.php"}]}' -X PATCH http://localhost:2019/config/apps/frankenphp 4 | done 5 | -------------------------------------------------------------------------------- /scaling_test.go: -------------------------------------------------------------------------------- 1 | package frankenphp 2 | 3 | import ( 4 | "io" 5 | "log/slog" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestScaleARegularThreadUpAndDown(t *testing.T) { 13 | assert.NoError(t, Init( 14 | WithNumThreads(1), 15 | WithMaxThreads(2), 16 | WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil))), 17 | )) 18 | 19 | autoScaledThread := phpThreads[1] 20 | 21 | // scale up 22 | scaleRegularThread() 23 | assert.Equal(t, stateReady, autoScaledThread.state.get()) 24 | assert.IsType(t, &regularThread{}, autoScaledThread.handler) 25 | 26 | // on down-scale, the thread will be marked as inactive 27 | setLongWaitTime(autoScaledThread) 28 | deactivateThreads() 29 | assert.IsType(t, &inactiveThread{}, autoScaledThread.handler) 30 | 31 | Shutdown() 32 | } 33 | 34 | func TestScaleAWorkerThreadUpAndDown(t *testing.T) { 35 | workerName := "worker1" 36 | workerPath := testDataPath + "/transition-worker-1.php" 37 | assert.NoError(t, Init( 38 | WithNumThreads(2), 39 | WithMaxThreads(3), 40 | WithWorkers(workerName, workerPath, 1, 41 | WithWorkerEnv(map[string]string{}), 42 | WithWorkerWatchMode([]string{}), 43 | WithWorkerMaxFailures(0), 44 | ), 45 | WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil))), 46 | )) 47 | 48 | autoScaledThread := phpThreads[2] 49 | 50 | // scale up 51 | scaleWorkerThread(getWorkerByPath(workerPath)) 52 | assert.Equal(t, stateReady, autoScaledThread.state.get()) 53 | 54 | // on down-scale, the thread will be marked as inactive 55 | setLongWaitTime(autoScaledThread) 56 | deactivateThreads() 57 | assert.IsType(t, &inactiveThread{}, autoScaledThread.handler) 58 | 59 | Shutdown() 60 | } 61 | 62 | func setLongWaitTime(thread *phpThread) { 63 | thread.state.mu.Lock() 64 | thread.state.waitingSince = time.Now().Add(-time.Hour) 65 | thread.state.mu.Unlock() 66 | } 67 | -------------------------------------------------------------------------------- /state_test.go: -------------------------------------------------------------------------------- 1 | package frankenphp 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test2GoroutinesYieldToEachOtherViaStates(t *testing.T) { 11 | threadState := &threadState{currentState: stateBooting} 12 | 13 | go func() { 14 | threadState.waitFor(stateInactive) 15 | assert.True(t, threadState.is(stateInactive)) 16 | threadState.set(stateReady) 17 | }() 18 | 19 | threadState.set(stateInactive) 20 | threadState.waitFor(stateReady) 21 | assert.True(t, threadState.is(stateReady)) 22 | } 23 | 24 | func TestStateShouldHaveCorrectAmountOfSubscribers(t *testing.T) { 25 | threadState := &threadState{currentState: stateBooting} 26 | 27 | // 3 subscribers waiting for different states 28 | go threadState.waitFor(stateInactive) 29 | go threadState.waitFor(stateInactive, stateShuttingDown) 30 | go threadState.waitFor(stateShuttingDown) 31 | 32 | assertNumberOfSubscribers(t, threadState, 3) 33 | 34 | threadState.set(stateInactive) 35 | assertNumberOfSubscribers(t, threadState, 1) 36 | 37 | assert.True(t, threadState.compareAndSwap(stateInactive, stateShuttingDown)) 38 | assertNumberOfSubscribers(t, threadState, 0) 39 | } 40 | 41 | func assertNumberOfSubscribers(t *testing.T, threadState *threadState, expected int) { 42 | maxWaits := 10_000 // wait for 1 second max 43 | 44 | for i := 0; i < maxWaits; i++ { 45 | time.Sleep(100 * time.Microsecond) 46 | threadState.mu.RLock() 47 | if len(threadState.subscribers) == expected { 48 | threadState.mu.RUnlock() 49 | break 50 | } 51 | threadState.mu.RUnlock() 52 | } 53 | threadState.mu.RLock() 54 | assert.Len(t, threadState.subscribers, expected) 55 | threadState.mu.RUnlock() 56 | } 57 | -------------------------------------------------------------------------------- /static-builder-musl.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | #checkov:skip=CKV_DOCKER_2 3 | #checkov:skip=CKV_DOCKER_3 4 | #checkov:skip=CKV_DOCKER_7 5 | FROM golang-base 6 | 7 | ARG TARGETARCH 8 | 9 | ARG FRANKENPHP_VERSION='' 10 | ENV FRANKENPHP_VERSION=${FRANKENPHP_VERSION} 11 | 12 | ARG PHP_VERSION='' 13 | ENV PHP_VERSION=${PHP_VERSION} 14 | 15 | ARG PHP_EXTENSIONS='' 16 | ARG PHP_EXTENSION_LIBS='' 17 | ARG XCADDY_ARGS='' 18 | ARG CLEAN='' 19 | ARG EMBED='' 20 | ARG DEBUG_SYMBOLS='' 21 | ARG MIMALLOC='' 22 | ARG NO_COMPRESS='' 23 | 24 | ENV GOTOOLCHAIN=local 25 | 26 | SHELL ["/bin/ash", "-eo", "pipefail", "-c"] 27 | 28 | LABEL org.opencontainers.image.title=FrankenPHP 29 | LABEL org.opencontainers.image.description="The modern PHP app server" 30 | LABEL org.opencontainers.image.url=https://frankenphp.dev 31 | LABEL org.opencontainers.image.source=https://github.com/php/frankenphp 32 | LABEL org.opencontainers.image.licenses=MIT 33 | LABEL org.opencontainers.image.vendor="Kévin Dunglas" 34 | 35 | RUN apk update; \ 36 | apk add --no-cache \ 37 | alpine-sdk \ 38 | autoconf \ 39 | automake \ 40 | bash \ 41 | binutils \ 42 | bison \ 43 | build-base \ 44 | cmake \ 45 | curl \ 46 | file \ 47 | flex \ 48 | g++ \ 49 | gcc \ 50 | git \ 51 | jq \ 52 | libgcc \ 53 | libstdc++ \ 54 | libtool \ 55 | linux-headers \ 56 | m4 \ 57 | make \ 58 | pkgconfig \ 59 | php84 \ 60 | php84-common \ 61 | php84-ctype \ 62 | php84-curl \ 63 | php84-dom \ 64 | php84-mbstring \ 65 | php84-openssl \ 66 | php84-pcntl \ 67 | php84-phar \ 68 | php84-posix \ 69 | php84-session \ 70 | php84-sodium \ 71 | php84-tokenizer \ 72 | php84-xml \ 73 | php84-xmlwriter \ 74 | upx \ 75 | wget \ 76 | xz ; \ 77 | ln -sf /usr/bin/php84 /usr/bin/php && \ 78 | go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest 79 | 80 | # https://getcomposer.org/doc/03-cli.md#composer-allow-superuser 81 | ENV COMPOSER_ALLOW_SUPERUSER=1 82 | COPY --from=composer/composer:2-bin /composer /usr/bin/composer 83 | 84 | WORKDIR /go/src/app 85 | COPY go.mod go.sum ./ 86 | RUN go mod download 87 | 88 | WORKDIR /go/src/app/caddy 89 | COPY caddy/go.mod caddy/go.sum ./ 90 | RUN go mod download 91 | 92 | WORKDIR /go/src/app 93 | COPY --link . ./ 94 | 95 | ENV SPC_DEFAULT_C_FLAGS='-fPIE -fPIC -O3' 96 | ENV SPC_LIBC='musl' 97 | ENV SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM='-Wl,-O3 -pie' 98 | ENV SPC_OPT_BUILD_ARGS='--with-config-file-path=/etc/frankenphp --with-config-file-scan-dir=/etc/frankenphp/php.d' 99 | ENV SPC_REL_TYPE='binary' 100 | ENV EXTENSION_DIR='/usr/lib/frankenphp/modules' 101 | 102 | RUN --mount=type=secret,id=github-token GITHUB_TOKEN=$(cat /run/secrets/github-token) ./build-static.sh && \ 103 | rm -Rf dist/static-php-cli/source/* 104 | -------------------------------------------------------------------------------- /testdata/Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | debug 3 | frankenphp { 4 | #worker ./index.php 5 | } 6 | } 7 | 8 | http:// { 9 | log 10 | route { 11 | root . 12 | # Add trailing slash for directory requests 13 | @canonicalPath { 14 | file {path}/index.php 15 | not path */ 16 | } 17 | redir @canonicalPath {path}/ 308 18 | 19 | # If the requested file does not exist, try index files 20 | @indexFiles file { 21 | try_files {path} {path}/index.php index.php 22 | split_path .php 23 | } 24 | rewrite @indexFiles {http.matchers.file.relative} 25 | 26 | encode zstd br gzip 27 | 28 | # FrankenPHP! 29 | @phpFiles path *.php 30 | php @phpFiles 31 | file_server 32 | 33 | respond 404 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /testdata/_executor.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | $fn = require $_SERVER['SCRIPT_FILENAME']; 4 | if ('1' !== ($_SERVER['FRANKENPHP_WORKER'] ?? null)) { 5 | $fn(); 6 | exit(0); 7 | } 8 | 9 | while (frankenphp_handle_request($fn)) {} 10 | 11 | exit(0); 12 | -------------------------------------------------------------------------------- /testdata/autoloader-require.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | function my_autoloader(string $class) { 4 | } 5 | spl_autoload_register('my_autoloader'); 6 | -------------------------------------------------------------------------------- /testdata/autoloader.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | require_once __DIR__.'/autoloader-require.php'; 5 | 6 | return function () { 7 | echo "request {$_GET['i']}\n"; 8 | echo implode(',', spl_autoload_functions()); 9 | }; 10 | -------------------------------------------------------------------------------- /testdata/benchmark.Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | frankenphp 3 | } 4 | 5 | http:// { 6 | route { 7 | root . 8 | 9 | encode zstd br gzip 10 | 11 | php { 12 | file_server off 13 | resolve_root_symlink false 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/command.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | var_dump($argv, $_SERVER); 4 | echo "From the CLI\n"; 5 | 6 | exit(3); 7 | -------------------------------------------------------------------------------- /testdata/connectionStatusLog.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | ignore_user_abort(true); 4 | 5 | require_once __DIR__.'/_executor.php'; 6 | 7 | return function () { 8 | if($_GET['finish'] ?? false) { 9 | frankenphp_finish_request(); 10 | } 11 | 12 | echo 'hi'; 13 | flush(); 14 | $status = (string) connection_status(); 15 | error_log("request {$_GET['i']}: " . $status); 16 | }; 17 | -------------------------------------------------------------------------------- /testdata/cookies.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | echo var_export($_COOKIE); 7 | }; 8 | -------------------------------------------------------------------------------- /testdata/die.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | do { 4 | $ok = frankenphp_handle_request(function (): void { 5 | echo 'Hello, world'; 6 | }); 7 | 8 | die(); 9 | } while ($ok); 10 | -------------------------------------------------------------------------------- /testdata/dirindex/index.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | echo "Hello from directory index.php"; 4 | -------------------------------------------------------------------------------- /testdata/early-hints.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | header('Link: </style.css>; rel=preload; as=style'); 7 | header("Request: {$_GET['i']}"); 8 | headers_send(103); 9 | 10 | header_remove('Link'); 11 | 12 | echo 'Hello'; 13 | }; 14 | -------------------------------------------------------------------------------- /testdata/echo.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | header('Content-Type: text/plain'); 4 | 5 | echo file_get_contents('php://input'); 6 | -------------------------------------------------------------------------------- /testdata/env/env.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__ . '/../_executor.php'; 4 | 5 | return function () use (&$rememberedKey) { 6 | $keys = $_GET['keys']; 7 | 8 | // echoes ENV1=value1,ENV2=value2 9 | echo join(',', array_map(fn($key) => "$key=" . $_ENV[$key], $keys)); 10 | }; 11 | -------------------------------------------------------------------------------- /testdata/env/import-env.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | return $_ENV['custom_key']; 4 | -------------------------------------------------------------------------------- /testdata/env/overwrite-env.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/../_executor.php'; 4 | 5 | // modify $_ENV in the global symbol table 6 | // the modification should persist through the worker's lifetime 7 | $_ENV['custom_key'] = 'custom_value'; 8 | 9 | return function () use (&$rememberedIndex) { 10 | $custom_key = require __DIR__.'/import-env.php'; 11 | echo $custom_key; 12 | }; 13 | -------------------------------------------------------------------------------- /testdata/env/putenv.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/../_executor.php'; 4 | 5 | return function () use (&$rememberedKey) { 6 | $key = $_GET['key']; 7 | $put = $_GET['put'] ?? null; 8 | 9 | if(isset($put)){ 10 | putenv("$key=$put"); 11 | } 12 | 13 | $get = getenv($key); 14 | $asStr = $get === false ? '' : $get; 15 | 16 | echo "$key=$asStr"; 17 | }; 18 | -------------------------------------------------------------------------------- /testdata/env/remember-env.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/../_executor.php'; 4 | 5 | $rememberedIndex = 0; 6 | 7 | return function () use (&$rememberedIndex) { 8 | $indexFromRequest = $_GET['index'] ?? null; 9 | if(isset($indexFromRequest)){ 10 | $rememberedIndex = (int)$indexFromRequest; 11 | putenv("index=$rememberedIndex"); 12 | } 13 | 14 | $indexInEnv = (int)getenv('index'); 15 | if($indexInEnv === $rememberedIndex){ 16 | echo 'success'; 17 | return; 18 | } 19 | echo "failure: '$indexInEnv' is not '$rememberedIndex'"; 20 | }; 21 | -------------------------------------------------------------------------------- /testdata/env/test-env.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/../_executor.php'; 4 | 5 | return function() { 6 | $var = 'MY_VAR_' . ($_GET['var'] ?? ''); 7 | // Setting an environment variable 8 | $result = putenv("$var=HelloWorld"); 9 | if ($result) { 10 | echo "Set MY_VAR successfully.\n"; 11 | echo "MY_VAR = " . getenv($var) . "\n"; 12 | } else { 13 | echo "Failed to set MY_VAR.\n"; 14 | } 15 | 16 | // Unsetting the environment variable 17 | $result = putenv($var); 18 | if ($result) { 19 | echo "Unset MY_VAR successfully.\n"; 20 | $value = getenv($var); 21 | if ($value === false) { 22 | echo "MY_VAR is unset.\n"; 23 | } else { 24 | echo "MY_VAR = " . $value . "\n"; 25 | } 26 | } else { 27 | echo "Failed to unset MY_VAR.\n"; 28 | } 29 | 30 | $result = putenv("$var="); 31 | if ($result) { 32 | echo "MY_VAR set to empty successfully.\n"; 33 | $value = getenv($var); 34 | if ($value === false) { 35 | echo "MY_VAR is unset.\n"; 36 | } else { 37 | echo "MY_VAR = " . $value . "\n"; 38 | } 39 | } else { 40 | echo "Failed to set MY_VAR.\n"; 41 | } 42 | 43 | // Attempt to unset a non-existing variable 44 | $result = putenv('NON_EXISTING_VAR' . ($_GET['var'] ?? '')); 45 | if ($result) { 46 | echo "Unset NON_EXISTING_VAR successfully.\n"; 47 | } else { 48 | echo "Failed to unset NON_EXISTING_VAR.\n"; 49 | } 50 | 51 | getenv(); 52 | }; 53 | -------------------------------------------------------------------------------- /testdata/exception.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | echo 'hello'; 7 | throw new Exception("request {$_GET['i']}"); 8 | }; 9 | -------------------------------------------------------------------------------- /testdata/failing-worker.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | $fail = random_int(1, 100) < 10; 4 | $wait = random_int(1000 * 100, 1000 * 500); // wait 100ms - 500ms 5 | 6 | usleep($wait); 7 | if ($fail) { 8 | exit(1); 9 | } 10 | 11 | while (frankenphp_handle_request(function () { 12 | echo "ok"; 13 | })) { 14 | $fail = random_int(1, 100) < 10; 15 | if ($fail) { 16 | exit(1); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /testdata/fiber-basic.php: -------------------------------------------------------------------------------- 1 | <?php 2 | require_once __DIR__.'/_executor.php'; 3 | 4 | return function() { 5 | $fiber = new Fiber(function() { 6 | echo 'Fiber '.($_GET['i'] ?? ''); 7 | }); 8 | $fiber->start(); 9 | }; 10 | -------------------------------------------------------------------------------- /testdata/fiber-no-cgo.php: -------------------------------------------------------------------------------- 1 | <?php 2 | require_once __DIR__.'/_executor.php'; 3 | 4 | return function() { 5 | $fiber = new Fiber(function() { 6 | Fiber::suspend('Fiber '.($_GET['i'] ?? '')); 7 | }); 8 | echo $fiber->start(); 9 | 10 | $fiber->resume(); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /testdata/file-stream.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | $fileStream = fopen(__DIR__ . '/file-stream.txt', 'r'); 4 | $input = fopen('php://input', 'r'); 5 | 6 | while (frankenphp_handle_request(function () use ($fileStream, $input) { 7 | echo fread($fileStream, 5); 8 | 9 | // this line will lead to a zend_mm_heap corrupted error if the input stream was destroyed 10 | stream_is_local($input); 11 | })) ; 12 | 13 | fclose($fileStream); 14 | -------------------------------------------------------------------------------- /testdata/file-stream.txt: -------------------------------------------------------------------------------- 1 | word1word2word3 2 | -------------------------------------------------------------------------------- /testdata/file-upload.php: -------------------------------------------------------------------------------- 1 | <?php 2 | require_once __DIR__.'/_executor.php'; 3 | 4 | return function() 5 | { 6 | $uploaded = ($_FILES['file']['tmp_name'] ?? null) ? file_get_contents($_FILES['file']['tmp_name']) : null; 7 | if ($uploaded) { 8 | echo 'Upload OK'; 9 | return; 10 | } 11 | 12 | echo <<<'HTML' 13 | <form method="POST" enctype="multipart/form-data"> 14 | <input type="file" name="file"> 15 | <input type="submit"> 16 | </form> 17 | HTML; 18 | }; 19 | -------------------------------------------------------------------------------- /testdata/files/.gitignore: -------------------------------------------------------------------------------- 1 | test.txt 2 | -------------------------------------------------------------------------------- /testdata/files/static.txt: -------------------------------------------------------------------------------- 1 | Hello from file 2 | -------------------------------------------------------------------------------- /testdata/finish-request.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | echo 'This is output '.($_GET['i'] ?? '')."\n"; 7 | 8 | frankenphp_finish_request(); 9 | 10 | echo 'This is not'; 11 | }; 12 | -------------------------------------------------------------------------------- /testdata/flush.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | 4 | require_once __DIR__.'/_executor.php'; 5 | 6 | return function () { 7 | if (ini_get("output_buffering") !== "0") { 8 | // Disable output buffering if not already done 9 | while (@ob_end_flush()); 10 | } 11 | 12 | echo 'He'; 13 | 14 | flush(); 15 | 16 | echo 'llo '.($_GET['i'] ?? ''); 17 | }; 18 | -------------------------------------------------------------------------------- /testdata/headers.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | header('Foo: bar'); 7 | header('Foo2: bar2'); 8 | header('Foo3:bar3'); // no space after colon (also valid, not recommended) 9 | header('Invalid'); 10 | header('I: ' . ($_GET['i'] ?? 'i not set')); 11 | http_response_code(201); 12 | 13 | echo 'Hello'; 14 | }; 15 | -------------------------------------------------------------------------------- /testdata/hello.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | echo "Hello from PHP"; 4 | -------------------------------------------------------------------------------- /testdata/hello.txt: -------------------------------------------------------------------------------- 1 | Hello -------------------------------------------------------------------------------- /testdata/index.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | echo sprintf("I am by birth a Genevese (%s)", $_GET['i'] ?? 'i not set'); 7 | }; 8 | -------------------------------------------------------------------------------- /testdata/ini.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | echo $_GET['key'] . ':' . ini_get($_GET['key']); 7 | }; 8 | -------------------------------------------------------------------------------- /testdata/input.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | header('Foo: bar'); 7 | 8 | echo file_get_contents('php://input'); 9 | }; 10 | -------------------------------------------------------------------------------- /testdata/large-request.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | printf( 7 | 'Request body size: %d (%s)', 8 | strlen(file_get_contents('php://input')), 9 | $_GET['i'] ?? 'unknown', 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /testdata/large-response.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | echo str_repeat("Hey\n", 1024); 7 | }; 8 | -------------------------------------------------------------------------------- /testdata/load-test.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http' 2 | import { check } from 'k6' 3 | 4 | export const options = { 5 | // A number specifying the number of VUs to run concurrently. 6 | vus: 100, 7 | // A string specifying the total duration of the test run. 8 | duration: '30s' 9 | 10 | // The following section contains configuration options for execution of this 11 | // test script in Grafana Cloud. 12 | // 13 | // See https://grafana.com/docs/grafana-cloud/k6/get-started/run-cloud-tests-from-the-cli/ 14 | // to learn about authoring and running k6 test scripts in Grafana k6 Cloud. 15 | // 16 | // ext: { 17 | // loadimpact: { 18 | // // The ID of the project to which the test is assigned in the k6 Cloud UI. 19 | // // By default tests are executed in default project. 20 | // projectID: "", 21 | // // The name of the test in the k6 Cloud UI. 22 | // // Test runs with the same name will be grouped. 23 | // name: "script.js" 24 | // } 25 | // }, 26 | 27 | // Uncomment this section to enable the use of Browser API in your tests. 28 | // 29 | // See https://grafana.com/docs/k6/latest/using-k6-browser/running-browser-tests/ to learn more 30 | // about using Browser API in your test scripts. 31 | // 32 | // scenarios: { 33 | // // The scenario name appears in the result summary, tags, and so on. 34 | // // You can give the scenario any name, as long as each name in the script is unique. 35 | // ui: { 36 | // // Executor is a mandatory parameter for browser-based tests. 37 | // // Shared iterations in this case tells k6 to reuse VUs to execute iterations. 38 | // // 39 | // // See https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/ for other executor types. 40 | // executor: 'shared-iterations', 41 | // options: { 42 | // browser: { 43 | // // This is a mandatory parameter that instructs k6 to launch and 44 | // // connect to a Chromium-based browser, and use it to run UI-based 45 | // // tests. 46 | // type: 'chromium', 47 | // }, 48 | // }, 49 | // }, 50 | // } 51 | } 52 | 53 | const payload = 'foo\n'.repeat(1000) 54 | 55 | // The function that defines VU logic. 56 | // 57 | // See https://grafana.com/docs/k6/latest/examples/get-started-with-k6/ to learn more 58 | // about authoring k6 scripts. 59 | // 60 | export default function () { 61 | const params = { 62 | headers: { 63 | Accept: 64 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 65 | // 'Accept-Encoding': 'br', 66 | 'Accept-Language': 'fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3', 67 | 'Cache-Control': 'no-cache', 68 | Connection: 'keep-alive', 69 | Cookie: 70 | 'user_session=myrandomuuid; __Host-user_session_same_site=myotherrandomuuid; dotcom_user=dunglas; logged_in=yes; _foo=barbarbarbarbarbar; _device_id=anotherrandomuuid; color_mode=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar; preferred_color_mode=light; tz=Europe%2FParis; has_recent_activity=1', 71 | DNT: '1', 72 | Host: 'example.com', 73 | Pragma: 'no-cache', 74 | 'Sec-Fetch-Dest': 'document', 75 | 'Sec-Fetch-Mode': 'navigate', 76 | 'Sec-Fetch-Site': 'cross-site', 77 | 'Sec-GPC': '1', 78 | 'Upgrade-Insecure-Requests': '1', 79 | 'User-Agent': 80 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0' 81 | } 82 | } 83 | 84 | const res = http.post('http://localhost/echo.php', payload, params) 85 | check(res, { 86 | 'is status 200': (r) => r.status === 200, 87 | 'is echoed': (r) => r.body === payload 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /testdata/log.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | error_log("request {$_GET['i']}"); 7 | }; 8 | -------------------------------------------------------------------------------- /testdata/non-worker.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | frankenphp_handle_request(function () {}); 4 | -------------------------------------------------------------------------------- /testdata/only-headers.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | header('Content-Type: application/json'); 7 | header('HTTP/1.1 204 No Content', true, 204); 8 | 9 | echo '{"status": "test"}'; 10 | flush(); 11 | }; 12 | -------------------------------------------------------------------------------- /testdata/performance/api.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http' 2 | 3 | /** 4 | * Many applications communicate with external APIs or microservices. 5 | * Latencies tend to be much higher than with databases in these cases. 6 | * We'll consider 10ms-150ms 7 | */ 8 | export const options = { 9 | stages: [ 10 | { duration: '20s', target: 150 }, 11 | { duration: '20s', target: 1000 }, 12 | { duration: '10s', target: 0 } 13 | ], 14 | thresholds: { 15 | http_req_failed: ['rate<0.01'] 16 | } 17 | } 18 | 19 | /* global __ENV */ 20 | export default function () { 21 | // 10-150ms latency 22 | const latency = Math.floor(Math.random() * 141) + 10 23 | // 1-30000 work units 24 | const work = Math.ceil(Math.random() * 30000) 25 | // 1-40 output units 26 | const output = Math.ceil(Math.random() * 40) 27 | 28 | http.get(http.url`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=${latency}&work=${work}&output=${output}`) 29 | } 30 | -------------------------------------------------------------------------------- /testdata/performance/computation.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http' 2 | 3 | /** 4 | * Simulate an application that does very little IO, but a lot of computation 5 | */ 6 | export const options = { 7 | stages: [ 8 | { duration: '20s', target: 80 }, 9 | { duration: '20s', target: 150 }, 10 | { duration: '5s', target: 0 } 11 | ], 12 | thresholds: { 13 | http_req_failed: ['rate<0.01'] 14 | } 15 | } 16 | 17 | /* global __ENV */ 18 | export default function () { 19 | // do 1-1,000,000 work units 20 | const work = Math.ceil(Math.random() * 1_000_000) 21 | // output 1-500 units 22 | const output = Math.ceil(Math.random() * 500) 23 | // simulate 0-2ms latency 24 | const latency = Math.floor(Math.random() * 3) 25 | 26 | http.get(http.url`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=${latency}&work=${work}&output=${output}`) 27 | } 28 | -------------------------------------------------------------------------------- /testdata/performance/database.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http' 2 | 3 | /** 4 | * Modern databases tend to have latencies in the single-digit milliseconds. 5 | * We'll simulate 1-10ms latencies and 1-2 queries per request. 6 | */ 7 | export const options = { 8 | stages: [ 9 | { duration: '20s', target: 100 }, 10 | { duration: '30s', target: 200 }, 11 | { duration: '10s', target: 0 } 12 | ], 13 | thresholds: { 14 | http_req_failed: ['rate<0.01'] 15 | } 16 | } 17 | 18 | /* global __ENV */ 19 | export default function () { 20 | // 1-10ms latency 21 | const latency = Math.floor(Math.random() * 10) + 1 22 | // 1-2 iterations per request 23 | const iterations = Math.floor(Math.random() * 2) + 1 24 | // 1-30000 work units per iteration 25 | const work = Math.ceil(Math.random() * 30000) 26 | // 1-40 output units 27 | const output = Math.ceil(Math.random() * 40) 28 | 29 | http.get(http.url`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=${latency}&work=${work}&output=${output}&iterations=${iterations}`) 30 | } 31 | -------------------------------------------------------------------------------- /testdata/performance/flamegraph.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install brendangregg's FlameGraph 4 | if [ ! -d "/usr/local/src/flamegraph" ]; then 5 | mkdir /usr/local/src/flamegraph && 6 | cd /usr/local/src/flamegraph && 7 | git clone https://github.com/brendangregg/FlameGraph.git 8 | fi 9 | 10 | # let the test warm up 11 | sleep 10 12 | 13 | # run a 30 second profile on the Caddy admin port 14 | cd /usr/local/src/flamegraph/FlameGraph && 15 | go tool pprof -raw -output=cpu.txt 'http://localhost:2019/debug/pprof/profile?seconds=30' && 16 | ./stackcollapse-go.pl cpu.txt | ./flamegraph.pl >/go/src/app/testdata/performance/flamegraph.svg 17 | -------------------------------------------------------------------------------- /testdata/performance/hanging-requests.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http' 2 | 3 | /** 4 | * It is not uncommon for external services to hang for a long time. 5 | * Make sure the server is resilient in such cases and doesn't hang as well. 6 | */ 7 | export const options = { 8 | stages: [ 9 | { duration: '20s', target: 100 }, 10 | { duration: '20s', target: 500 }, 11 | { duration: '20s', target: 0 } 12 | ], 13 | thresholds: { 14 | http_req_failed: ['rate<0.01'] 15 | } 16 | } 17 | 18 | /* global __ENV */ 19 | export default function () { 20 | // 2% chance for a request that hangs for 15s 21 | if (Math.random() < 0.02) { 22 | http.get(`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=15000&work=10000&output=100`) 23 | return 24 | } 25 | 26 | // a regular request 27 | http.get(`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=5&work=10000&output=100`) 28 | } 29 | -------------------------------------------------------------------------------- /testdata/performance/hello-world.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http' 2 | 3 | /** 4 | * 'Hello world' tests the raw server performance. 5 | */ 6 | export const options = { 7 | stages: [ 8 | { duration: '5s', target: 100 }, 9 | { duration: '20s', target: 400 }, 10 | { duration: '5s', target: 0 } 11 | ], 12 | thresholds: { 13 | http_req_failed: ['rate<0.01'] 14 | } 15 | } 16 | 17 | /* global __ENV */ 18 | export default function () { 19 | http.get(`${__ENV.CADDY_HOSTNAME}/sleep.php`) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/performance/k6.Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | frankenphp { 3 | max_threads {$MAX_THREADS} 4 | num_threads {$NUM_THREADS} 5 | worker { 6 | file /go/src/app/testdata/{$WORKER_FILE:sleep.php} 7 | num {$WORKER_THREADS} 8 | } 9 | } 10 | } 11 | 12 | :80 { 13 | route { 14 | root /go/src/app/testdata 15 | php { 16 | root /go/src/app/testdata 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/performance/perf-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install the dev.Dockerfile, build the app and run k6 tests 4 | 5 | docker build -t frankenphp-dev -f dev.Dockerfile . 6 | 7 | export "CADDY_HOSTNAME=http://host.docker.internal" 8 | 9 | select filename in ./testdata/performance/*.js; do 10 | read -r -p "How many worker threads? " workerThreads 11 | read -r -p "How many max threads? " maxThreads 12 | 13 | numThreads=$((workerThreads + 1)) 14 | 15 | docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ 16 | -p 8125:80 \ 17 | -v "$PWD:/go/src/app" \ 18 | --name load-test-container \ 19 | -e "MAX_THREADS=$maxThreads" \ 20 | -e "WORKER_THREADS=$workerThreads" \ 21 | -e "NUM_THREADS=$numThreads" \ 22 | -itd \ 23 | frankenphp-dev \ 24 | sh /go/src/app/testdata/performance/start-server.sh 25 | 26 | docker exec -d load-test-container sh /go/src/app/testdata/performance/flamegraph.sh 27 | 28 | sleep 10 29 | 30 | docker run --entrypoint "" -it --rm -v .:/app -w /app \ 31 | --add-host "host.docker.internal:host-gateway" \ 32 | grafana/k6:latest \ 33 | k6 run -e "CADDY_HOSTNAME=$CADDY_HOSTNAME:8125" "./$filename" 34 | 35 | docker exec load-test-container curl "http://localhost:2019/frankenphp/threads" 36 | 37 | docker stop load-test-container 38 | docker rm load-test-container 39 | done 40 | -------------------------------------------------------------------------------- /testdata/performance/performance-testing.md: -------------------------------------------------------------------------------- 1 | # Running Load tests 2 | 3 | To run load tests with k6 you need to have Docker and Bash installed. 4 | Go the root of this repository and run: 5 | 6 | ```sh 7 | bash testdata/performance/perf-test.sh 8 | ``` 9 | 10 | This will build the `frankenphp-dev` docker image and run it under the name 'load-test-container' 11 | in the background. Additionally, it will run the `grafana/k6` container, and you'll be able to choose 12 | the load test you want to run. A `flamegraph.svg` will be created in the `testdata/performance` directory. 13 | 14 | If the load test has stopped prematurely, you might have to remove the container manually: 15 | 16 | ```sh 17 | docker stop load-test-container 18 | docker rm load-test-container 19 | ``` 20 | -------------------------------------------------------------------------------- /testdata/performance/start-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # build and run FrankenPHP with the k6.Caddyfile 4 | cd /go/src/app/caddy/frankenphp && 5 | go build --buildvcs=false && 6 | cd ../../testdata/performance && 7 | /go/src/app/caddy/frankenphp/frankenphp run -c k6.Caddyfile 8 | -------------------------------------------------------------------------------- /testdata/performance/timeouts.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http' 2 | 3 | /** 4 | * Databases or external resources can sometimes become unavailable for short periods of time. 5 | * Make sure the server can recover quickly from periods of unavailability. 6 | * This simulation swaps between a hanging and a working server every 10 seconds. 7 | */ 8 | export const options = { 9 | stages: [ 10 | { duration: '20s', target: 100 }, 11 | { duration: '20s', target: 500 }, 12 | { duration: '20s', target: 0 } 13 | ], 14 | thresholds: { 15 | http_req_failed: ['rate<0.01'] 16 | } 17 | } 18 | 19 | /* global __ENV */ 20 | export default function () { 21 | const tenSecondInterval = Math.floor(new Date().getSeconds() / 10) 22 | const shouldHang = tenSecondInterval % 2 === 0 23 | 24 | // every 10 seconds requests lead to a max_execution-timeout 25 | if (shouldHang) { 26 | http.get(`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=50000`) 27 | return 28 | } 29 | 30 | // every other 10 seconds the resource is back 31 | http.get(`${__ENV.CADDY_HOSTNAME}/sleep.php?sleep=5&work=30000&output=100`) 32 | } 33 | -------------------------------------------------------------------------------- /testdata/persistent-object-require.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | class MyObject 4 | { 5 | public function __construct(public string $id) {} 6 | } 7 | -------------------------------------------------------------------------------- /testdata/persistent-object.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | require_once __DIR__.'/persistent-object-require.php'; 5 | 6 | $foo = new MyObject('obj1'); 7 | 8 | return function () use ($foo) { 9 | echo 'request: ' . $_GET['i'] . "\n"; 10 | echo 'class exists: ' . class_exists(MyObject::class) . "\n"; 11 | echo 'id: ' . $foo->id . "\n"; 12 | echo 'object id: '. spl_object_id($foo); 13 | }; 14 | -------------------------------------------------------------------------------- /testdata/phpinfo.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | phpinfo(); 7 | }; 8 | -------------------------------------------------------------------------------- /testdata/request-headers.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | apache_request_headers(); 4 | 5 | require_once __DIR__.'/_executor.php'; 6 | 7 | return function() { 8 | print_r(apache_request_headers()); 9 | }; 10 | -------------------------------------------------------------------------------- /testdata/response-headers.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | header('Foo: bar'); 7 | header('Foo2: bar2'); 8 | header('Invalid'); 9 | header('I: ' . ($_GET['i'] ?? 'i not set')); 10 | if ($_GET['i'] % 3) { 11 | http_response_code($_GET['i'] + 100); 12 | } 13 | 14 | var_export(apache_response_headers()); 15 | }; 16 | -------------------------------------------------------------------------------- /testdata/server-all-vars-ordered.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | echo "<pre>\n"; 4 | foreach ([ 5 | 'CONTENT_LENGTH', 6 | 'HTTP_CONTENT_LENGTH', 7 | 'CONTENT_TYPE', 8 | 'HTTP_CONTENT_TYPE', 9 | 'HTTP_SPECIAL_CHARS', 10 | 'DOCUMENT_ROOT', 11 | 'DOCUMENT_URI', 12 | 'GATEWAY_INTERFACE', 13 | 'HTTP_HOST', 14 | 'HTTPS', 15 | 'PATH_INFO', 16 | 'DOCUMENT_ROOT', 17 | 'REMOTE_ADDR', 18 | 'PHP_SELF', 19 | 'REMOTE_HOST', 20 | 'REQUEST_SCHEME', 21 | 'SCRIPT_FILENAME', 22 | 'SCRIPT_NAME', 23 | 'SERVER_NAME', 24 | 'SERVER_PORT', 25 | 'SERVER_PROTOCOL', 26 | 'SERVER_SOFTWARE', 27 | 'SSL_PROTOCOL', 28 | 'AUTH_TYPE', 29 | 'REMOTE_IDENT', 30 | 'PATH_TRANSLATED', 31 | 'QUERY_STRING', 32 | 'REMOTE_USER', 33 | 'REQUEST_METHOD', 34 | 'REQUEST_URI', 35 | 'HTTP_X_EMPTY_HEADER', 36 | ] as $name) { 37 | echo "$name:" . $_SERVER[$name] . "\n"; 38 | } 39 | echo "</pre>"; -------------------------------------------------------------------------------- /testdata/server-all-vars-ordered.txt: -------------------------------------------------------------------------------- 1 | <pre> 2 | CONTENT_LENGTH:7 3 | HTTP_CONTENT_LENGTH:7 4 | CONTENT_TYPE:application/x-www-form-urlencoded 5 | HTTP_CONTENT_TYPE:application/x-www-form-urlencoded 6 | HTTP_SPECIAL_CHARS:<%00> 7 | DOCUMENT_ROOT:{documentRoot} 8 | DOCUMENT_URI:/server-all-vars-ordered.php 9 | GATEWAY_INTERFACE:CGI/1.1 10 | HTTP_HOST:localhost:{testPort} 11 | HTTPS: 12 | PATH_INFO:/path 13 | DOCUMENT_ROOT:{documentRoot} 14 | REMOTE_ADDR:127.0.0.1 15 | PHP_SELF:/server-all-vars-ordered.php/path 16 | REMOTE_HOST:127.0.0.1 17 | REQUEST_SCHEME:http 18 | SCRIPT_FILENAME:{documentRoot}/server-all-vars-ordered.php 19 | SCRIPT_NAME:/server-all-vars-ordered.php 20 | SERVER_NAME:localhost 21 | SERVER_PORT:{testPort} 22 | SERVER_PROTOCOL:HTTP/1.1 23 | SERVER_SOFTWARE:FrankenPHP 24 | SSL_PROTOCOL: 25 | AUTH_TYPE: 26 | REMOTE_IDENT: 27 | PATH_TRANSLATED:{documentRoot}/path 28 | QUERY_STRING:specialChars=%3E\x00%00</> 29 | REMOTE_USER:user 30 | REQUEST_METHOD:POST 31 | REQUEST_URI:/original-path?specialChars=%3E\x00%00</> 32 | HTTP_X_EMPTY_HEADER: 33 | </pre> -------------------------------------------------------------------------------- /testdata/server-variable.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | echo print_r($_SERVER); 7 | }; 8 | -------------------------------------------------------------------------------- /testdata/session.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | session_start(); 7 | 8 | if (isset($_SESSION['count'])) { 9 | $_SESSION['count']++; 10 | } else { 11 | $_SESSION['count'] = 0; 12 | } 13 | 14 | echo 'Count: '.$_SESSION['count'].PHP_EOL; 15 | }; 16 | -------------------------------------------------------------------------------- /testdata/sleep.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__ . '/_executor.php'; 4 | 5 | return function () { 6 | $sleep = (int)($_GET['sleep'] ?? 0); 7 | $work = (int)($_GET['work'] ?? 0); 8 | $output = (int)($_GET['output'] ?? 1); 9 | $iterations = (int)($_GET['iterations'] ?? 1); 10 | 11 | for ($i = 0; $i < $iterations; $i++) { 12 | // simulate work 13 | // with 30_000 iterations we're in the range of a simple Laravel request 14 | // (without JIT and with debug symbols enabled) 15 | for ($j = 0; $j < $work; $j++) { 16 | $a = +$j; 17 | } 18 | 19 | // simulate IO, sleep x milliseconds 20 | if ($sleep > 0) { 21 | usleep($sleep * 1000); 22 | } 23 | 24 | // simulate output 25 | for ($k = 0; $k < $output; $k++) { 26 | echo "slept for $sleep ms and worked for $work iterations"; 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /testdata/super-globals.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | var_export($_GET); 7 | var_export($_POST); 8 | var_export($_SERVER); 9 | }; 10 | -------------------------------------------------------------------------------- /testdata/timeout.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/_executor.php'; 4 | 5 | return function () { 6 | printf("request: %d\n", $_GET['i'] ?? 'unknown'); 7 | set_time_limit(1); 8 | 9 | $x = true; 10 | $y = 0; 11 | while ($x) { 12 | $y++; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /testdata/transition-regular.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | echo "Hello from regular thread"; 4 | -------------------------------------------------------------------------------- /testdata/transition-worker-1.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | while (frankenphp_handle_request(function () { 4 | echo "Hello from worker 1"; 5 | })) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /testdata/transition-worker-2.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | while (frankenphp_handle_request(function () { 4 | echo "Hello from worker 2"; 5 | // Simulate work to force potential race conditions (phpmainthread_test.go) 6 | usleep(1000); 7 | })) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /testdata/worker-env.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | $workerServer = $_SERVER; 4 | 5 | require_once __DIR__.'/_executor.php'; 6 | 7 | return function () use ($workerServer) { 8 | echo $_SERVER['FOO'] ?? ''; 9 | echo $workerServer['FOO'] ?? ''; 10 | echo $_GET['i'] ?? ''; 11 | }; 12 | -------------------------------------------------------------------------------- /testdata/worker-getopt.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | do { 4 | $ok = frankenphp_handle_request(function (): void { 5 | print_r($_SERVER); 6 | }); 7 | 8 | getopt('abc'); 9 | 10 | if (!isset($_SERVER['HTTP_REQUEST'])) { 11 | exit(1); 12 | } 13 | if (isset($_SERVER['FRANKENPHP_WORKER'])) { 14 | exit(2); 15 | } 16 | if (isset($_SERVER['FOO'])) { 17 | exit(3); 18 | } 19 | } while ($ok); 20 | -------------------------------------------------------------------------------- /testdata/worker-restart.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | $fn = static function () { 4 | echo sprintf("Counter (%s)", $_GET['i'] ?? 'i not set'); 5 | }; 6 | 7 | $loopMax = $_SERVER['EVERY'] ?? 10; 8 | $loops = 0; 9 | do { 10 | $ret = \frankenphp_handle_request($fn); 11 | } while ($ret && (-1 === $loopMax || ++$loops < $loopMax)); 12 | 13 | exit(0); 14 | -------------------------------------------------------------------------------- /testdata/worker-with-counter.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | $numberOfRequests = 0; 4 | $printNumberOfRequests = function () use (&$numberOfRequests) { 5 | $numberOfRequests++; 6 | echo "requests:$numberOfRequests"; 7 | }; 8 | 9 | while (frankenphp_handle_request($printNumberOfRequests)) { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /testdata/worker-with-env.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | $env = $_SERVER['APP_ENV'] ?? ''; 4 | do { 5 | $ok = frankenphp_handle_request(function () use ($env): void { 6 | echo "Worker has APP_ENV=$env"; 7 | }); 8 | } while ($ok); 9 | -------------------------------------------------------------------------------- /testdata/worker.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | $i = 0; 4 | do { 5 | $ok = frankenphp_handle_request(function () use ($i): void { 6 | echo sprintf("Requests handled: %d (request time: %s)\n", $i, $_SERVER['REQUEST_TIME_FLOAT']); 7 | 8 | var_export($_GET); 9 | var_export($_POST); 10 | var_export($_SERVER); 11 | }); 12 | 13 | $i++; 14 | } while ($ok); 15 | -------------------------------------------------------------------------------- /threadinactive.go: -------------------------------------------------------------------------------- 1 | package frankenphp 2 | 3 | // representation of a thread with no work assigned to it 4 | // implements the threadHandler interface 5 | // each inactive thread weighs around ~350KB 6 | // keeping threads at 'inactive' will consume more memory, but allow a faster transition 7 | type inactiveThread struct { 8 | thread *phpThread 9 | } 10 | 11 | func convertToInactiveThread(thread *phpThread) { 12 | thread.setHandler(&inactiveThread{thread: thread}) 13 | } 14 | 15 | func (handler *inactiveThread) beforeScriptExecution() string { 16 | thread := handler.thread 17 | 18 | switch thread.state.get() { 19 | case stateTransitionRequested: 20 | return thread.transitionToNewHandler() 21 | case stateBooting, stateTransitionComplete: 22 | thread.state.set(stateInactive) 23 | 24 | // wait for external signal to start or shut down 25 | thread.state.markAsWaiting(true) 26 | thread.state.waitFor(stateTransitionRequested, stateShuttingDown) 27 | thread.state.markAsWaiting(false) 28 | return handler.beforeScriptExecution() 29 | case stateShuttingDown: 30 | // signal to stop 31 | return "" 32 | } 33 | panic("unexpected state: " + thread.state.name()) 34 | } 35 | 36 | func (handler *inactiveThread) afterScriptExecution(int) { 37 | panic("inactive threads should not execute scripts") 38 | } 39 | 40 | func (handler *inactiveThread) getRequestContext() *frankenPHPContext { 41 | return nil 42 | } 43 | 44 | func (handler *inactiveThread) name() string { 45 | return "Inactive PHP Thread" 46 | } 47 | -------------------------------------------------------------------------------- /types.c: -------------------------------------------------------------------------------- 1 | #include "types.h" 2 | 3 | zval *get_ht_packed_data(HashTable *ht, uint32_t index) { 4 | if (ht->u.flags & HASH_FLAG_PACKED) { 5 | return &ht->arPacked[index]; 6 | } 7 | return NULL; 8 | } 9 | 10 | Bucket *get_ht_bucket_data(HashTable *ht, uint32_t index) { 11 | if (!(ht->u.flags & HASH_FLAG_PACKED)) { 12 | return &ht->arData[index]; 13 | } 14 | return NULL; 15 | } 16 | 17 | void *__emalloc__(size_t size) { return emalloc(size); } 18 | 19 | void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, 20 | bool persistent) { 21 | zend_hash_init(ht, nSize, null, pDestructor, persistent); 22 | } 23 | -------------------------------------------------------------------------------- /types.h: -------------------------------------------------------------------------------- 1 | #ifndef TYPES_H 2 | #define TYPES_H 3 | 4 | #include <zend.h> 5 | #include <zend_API.h> 6 | #include <zend_alloc.h> 7 | #include <zend_hash.h> 8 | #include <zend_types.h> 9 | 10 | zval *get_ht_packed_data(HashTable *, uint32_t index); 11 | Bucket *get_ht_bucket_data(HashTable *, uint32_t index); 12 | 13 | void *__emalloc__(size_t size); 14 | void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, 15 | bool persistent); 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /types_test.go: -------------------------------------------------------------------------------- 1 | package frankenphp 2 | 3 | import "testing" 4 | 5 | func TestGoString(t *testing.T) { 6 | testGoString(t) 7 | } 8 | -------------------------------------------------------------------------------- /typestest.go: -------------------------------------------------------------------------------- 1 | package frankenphp 2 | 3 | //#include <Zend/zend.h> 4 | // 5 | //zend_string *hello_string() { 6 | // return zend_string_init("Hello", 5, 1); 7 | //} 8 | import "C" 9 | import ( 10 | "github.com/stretchr/testify/assert" 11 | "testing" 12 | "unsafe" 13 | ) 14 | 15 | func testGoString(t *testing.T) { 16 | assert.Equal(t, "", GoString(nil)) 17 | assert.Equal(t, "Hello", GoString(unsafe.Pointer(C.hello_string()))) 18 | } 19 | -------------------------------------------------------------------------------- /watcher_test.go: -------------------------------------------------------------------------------- 1 | //go:build !nowatcher 2 | 3 | package frankenphp_test 4 | 5 | import ( 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | // we have to wait a few milliseconds for the watcher debounce to take effect 17 | const pollingTime = 250 18 | 19 | // in tests checking for no reload: we will poll 3x250ms = 0.75s 20 | const minTimesToPollForChanges = 3 21 | 22 | // in tests checking for a reload: we will poll a maximum of 60x250ms = 15s 23 | const maxTimesToPollForChanges = 60 24 | 25 | func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { 26 | watch := []string{"./testdata/**/*.txt"} 27 | 28 | runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { 29 | requestBodyHasReset := pollForWorkerReset(t, handler, maxTimesToPollForChanges) 30 | assert.True(t, requestBodyHasReset) 31 | }, &testOptions{nbParallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-counter.php", watch: watch}) 32 | } 33 | 34 | func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { 35 | watch := []string{"./testdata/**/*.php"} 36 | 37 | runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { 38 | requestBodyHasReset := pollForWorkerReset(t, handler, minTimesToPollForChanges) 39 | assert.False(t, requestBodyHasReset) 40 | }, &testOptions{nbParallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-counter.php", watch: watch}) 41 | } 42 | 43 | func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Request), limit int) bool { 44 | // first we make an initial request to start the request counter 45 | body := fetchBody("GET", "http://example.com/worker-with-counter.php", handler) 46 | assert.Equal(t, "requests:1", body) 47 | 48 | // now we spam file updates and check if the request counter resets 49 | for i := 0; i < limit; i++ { 50 | updateTestFile("./testdata/files/test.txt", "updated", t) 51 | time.Sleep(pollingTime * time.Millisecond) 52 | body := fetchBody("GET", "http://example.com/worker-with-counter.php", handler) 53 | if body == "requests:1" { 54 | return true 55 | } 56 | } 57 | return false 58 | } 59 | 60 | func updateTestFile(fileName string, content string, t *testing.T) { 61 | absFileName, err := filepath.Abs(fileName) 62 | assert.NoError(t, err) 63 | dirName := filepath.Dir(absFileName) 64 | if _, err := os.Stat(dirName); os.IsNotExist(err) { 65 | err = os.MkdirAll(dirName, 0700) 66 | assert.NoError(t, err) 67 | } 68 | bytes := []byte(content) 69 | err = os.WriteFile(absFileName, bytes, 0644) 70 | assert.NoError(t, err) 71 | } 72 | --------------------------------------------------------------------------------