├── .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 | 
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 | 
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 | 
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 | 
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 | 
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
)
13 | var classConstRegex = regexp.MustCompile(`//\s*export_php:classconst\s+(\w+)
)
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, ®ularThread{}, 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 |
--------------------------------------------------------------------------------