diff --git a/docs/cn/CONTRIBUTING.md b/docs/cn/CONTRIBUTING.md index 28c1672d..ec895a8c 100644 --- a/docs/cn/CONTRIBUTING.md +++ b/docs/cn/CONTRIBUTING.md @@ -42,7 +42,7 @@ go test -tags watcher -race -v ./... ```console cd caddy/frankenphp/ -go build +go build -tags watcher,brotli,nobadger,nomysql,nopgx cd ../../ ``` @@ -53,10 +53,13 @@ cd testdata/ ../caddy/frankenphp/frankenphp run ``` -服务器正在监听 `127.0.0.1:8080`: +服务器正在监听 `127.0.0.1:80`: + +> [!NOTE] +> 如果您正在使用 Docker,您必须绑定容器的 80 端口或者在容器内部执行命令。 ```console -curl -vk https://localhost/phpinfo.php +curl -vk http://127.0.0.1/phpinfo.php ``` ## 最小测试服务器 @@ -149,18 +152,15 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push 3. 启用 `tmate` 以连接到容器 - ```patch - - - name: Set CGO flags - run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV" - + - - + run: | - + sudo apt install gdb - + mkdir -p /home/runner/.config/gdb/ - + printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit - + - - + uses: mxschmitt/action-tmate@v3 - ``` + ```patch + - name: Set CGO flags + run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV" + + - run: | + + sudo apt install gdb + + mkdir -p /home/runner/.config/gdb/ + + printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit + + - uses: mxschmitt/action-tmate@v3 + ``` 4. 连接到容器 5. 打开 `frankenphp.go` @@ -190,7 +190,6 @@ docker buildx bake -f docker-bake.hcl --pull --no-cache --push - [PHP 嵌入 C++](https://gist.github.com/paresy/3cbd4c6a469511ac7479aa0e7c42fea7) - [扩展和嵌入 PHP 作者:Sara Golemon](https://books.google.fr/books?id=zMbGvK17_tYC&pg=PA254&lpg=PA254#v=onepage&q&f=false) - [TSRMLS_CC到底是什么?](http://blog.golemon.com/2006/06/what-heck-is-tsrmlscc-anyway.html) -- [Mac 上的 PHP 嵌入](https://gist.github.com/jonnywang/61427ffc0e8dde74fff40f479d147db4) - [SDL 绑定](https://pkg.go.dev/github.com/veandco/go-sdl2@v0.4.21/sdl#Main) ## Docker 相关资源 diff --git a/docs/cn/README.md b/docs/cn/README.md index ac8f9878..0b4685fa 100644 --- a/docs/cn/README.md +++ b/docs/cn/README.md @@ -8,7 +8,7 @@ FrankenPHP 凭借其令人惊叹的功能为你的 PHP 应用程序提供了超 FrankenPHP 可与任何 PHP 应用程序一起使用,并且由于提供了与 worker 模式的集成,使你的 Symfony 和 Laravel 项目比以往任何时候都更快。 -FrankenPHP 也可以用作独立的 Go 库,将 PHP 嵌入到任何使用 net/http 的应用程序中。 +FrankenPHP 也可以用作独立的 Go 库,将 PHP 嵌入到任何使用 `net/http` 的应用程序中。 [**了解更多** _frankenphp.dev_](https://frankenphp.dev/cn/) 以及查看此演示文稿: @@ -56,7 +56,7 @@ docker run -v .:/app/public \ > [!TIP] > > 不要尝试使用 `https://127.0.0.1`。使用 `https://localhost` 并接受自签名证书。 -> 使用 [`SERVER_NAME` 环境变量](config.md#环境变量) 更改要使用的域。 +> 使用 [`SERVER_NAME` 环境变量](config.md#environment-variables) 更改要使用的域。 ### Homebrew @@ -76,12 +76,16 @@ frankenphp php-server ## 文档 +- [Classic 模式](classic.md) - [worker 模式](worker.md) - [早期提示支持(103 HTTP status code)](early-hints.md) - [实时功能](mercure.md) +- [高效地服务大型静态文件](x-sendfile.md) - [配置](config.md) +- [用 Go 编写 PHP 扩展](extensions.md) - [Docker 镜像](docker.md) - [在生产环境中部署](production.md) +- [性能优化](performance.md) - [创建独立、可自行执行的 PHP 应用程序](embed.md) - [创建静态二进制文件](static.md) - [从源代码编译](compile.md) diff --git a/docs/cn/classic.md b/docs/cn/classic.md new file mode 100644 index 00000000..5fbe7970 --- /dev/null +++ b/docs/cn/classic.md @@ -0,0 +1,11 @@ +# 使用经典模式 + +在没有任何额外配置的情况下,FrankenPHP 以经典模式运行。在此模式下,FrankenPHP 的功能类似于传统的 PHP 服务器,直接提供 PHP 文件服务。这使其成为 PHP-FPM 或 Apache with mod_php 的无缝替代品。 + +与 Caddy 类似,FrankenPHP 接受无限数量的连接,并使用[固定数量的线程](config.md#caddyfile-配置)来为它们提供服务。接受和排队的连接数量仅受可用系统资源的限制。 +PHP 线程池使用在启动时初始化的固定数量的线程运行,类似于 PHP-FPM 的静态模式。也可以让线程在[运行时自动扩展](performance.md#max_threads),类似于 PHP-FPM 的动态模式。 + +排队的连接将无限期等待,直到有 PHP 线程可以为它们提供服务。为了避免这种情况,你可以在 FrankenPHP 的全局配置中使用 max_wait_time [配置](config.md#caddyfile-配置)来限制请求可以等待空闲的 PHP 线程的时间,超时后将被拒绝。 +此外,你还可以在 Caddy 中设置合理的[写超时](https://caddyserver.com/docs/caddyfile/options#timeouts)。 + +每个 Caddy 实例只会启动一个 FrankenPHP 线程池,该线程池将在所有 `php_server` 块之间共享。 diff --git a/docs/cn/compile.md b/docs/cn/compile.md index a02ee0e5..1db99eaa 100644 --- a/docs/cn/compile.md +++ b/docs/cn/compile.md @@ -9,6 +9,23 @@ FrankenPHP 支持 PHP 8.2 及更高版本。 +### 使用 Homebrew (Linux 和 Mac) + +安装与 FrankenPHP 兼容的 libphp 版本的最简单方法是使用 [Homebrew PHP](https://github.com/shivammathur/homebrew-php) 提供的 ZTS 包。 + +首先,如果尚未安装,请安装 [Homebrew](https://brew.sh)。 + +然后,安装 PHP 的 ZTS 变体、Brotli(可选,用于压缩支持)和 watcher(可选,用于文件更改检测): + +```console +brew install shivammathur/php/php-zts brotli watcher +brew link --overwrite --force shivammathur/php/php-zts +``` + +### 通过编译 PHP + +或者,你可以按照以下步骤,使用 FrankenPHP 所需的选项从源代码编译 PHP。 + 首先,[获取 PHP 源代码](https://www.php.net/downloads.php) 并提取它们: ```console @@ -16,11 +33,10 @@ tar xf php-* cd php-*/ ``` -然后,为你的平台配置 PHP. +然后,运行适用于你平台的 `configure` 脚本。 +以下 `./configure` 标志是必需的,但你可以添加其他标志,例如编译扩展或附加功能。 -这些参数是必需的,但你也可以添加其他编译参数(例如额外的扩展)。 - -### Linux +#### Linux ```console ./configure \ @@ -30,12 +46,12 @@ cd php-*/ --enable-zend-max-execution-timers ``` -### Mac +#### Mac -使用 [Homebrew](https://brew.sh/) 包管理器安装 `libiconv`、`bison`、`re2c` 和 `pkg-config`: +使用 [Homebrew](https://brew.sh/) 包管理器安装所需的和可选的依赖项: ```console -brew install libiconv bison re2c pkg-config +brew install libiconv bison brotli re2c pkg-config watcher echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc ``` @@ -43,16 +59,13 @@ echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc ```console ./configure \ - --enable-embed=static \ + --enable-embed \ --enable-zts \ --disable-zend-signals \ - --disable-opcache-jit \ - --enable-static \ - --enable-shared=no \ --with-iconv=/opt/homebrew/opt/libiconv/ ``` -## 编译并安装 PHP +#### 编译 PHP 最后,编译并安装 PHP: @@ -61,30 +74,36 @@ make -j"$(getconf _NPROCESSORS_ONLN)" sudo make install ``` +## 安装可选依赖项 + +某些 FrankenPHP 功能依赖于必须安装的可选系统依赖项。 +或者,可以通过向 Go 编译器传递构建标签来禁用这些功能。 + +| 功能 | 依赖项 | 用于禁用的构建标签 | +|--------------------------|------------------------------------------------------------------------|-------------------| +| Brotli 压缩 | [Brotli](https://github.com/google/brotli) | nobrotli | +| 文件更改时重启 worker | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher | + ## 编译 Go 应用 -你现在可以使用 Go 库并编译我们的 Caddy 构建: - -```console -curl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz -cd frankenphp-main/caddy/frankenphp -CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build -``` +你现在可以构建最终的二进制文件。 ### 使用 xcaddy -你可以使用 [xcaddy](https://github.com/caddyserver/xcaddy) 来编译 [自定义 Caddy 模块](https://caddyserver.com/docs/modules/) 的 FrankenPHP: +推荐的方法是使用 [xcaddy](https://github.com/caddyserver/xcaddy) 来编译 FrankenPHP。 +`xcaddy` 还允许轻松添加 [自定义 Caddy 模块](https://caddyserver.com/docs/modules/) 和 FrankenPHP 扩展: ```console CGO_ENABLED=1 \ -XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'" \ +XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \ +CGO_CFLAGS=$(php-config --includes) \ +CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \ xcaddy build \ --output frankenphp \ --with github.com/dunglas/frankenphp/caddy \ - --with github.com/dunglas/caddy-cbrotli \ --with github.com/dunglas/mercure/caddy \ --with github.com/dunglas/vulcain/caddy - # Add extra Caddy modules here + # 在这里添加额外的 Caddy 模块和 FrankenPHP 扩展 ``` > [!TIP] @@ -96,3 +115,13 @@ xcaddy build \ > 请将 `XCADDY_GO_BUILD_FLAGS` 环境变量更改为如下类似的值 > `XCADDY_GO_BUILD_FLAGS=$'-ldflags "-w -s -extldflags \'-Wl,-z,stack-size=0x80000\'"'` > (根据你的应用需求更改堆栈大小)。 + +### 不使用 xcaddy + +或者,可以通过直接使用 `go` 命令来编译 FrankenPHP 而不使用 `xcaddy`: + +```console +curl -L https://github.com/php/frankenphp/archive/refs/heads/main.tar.gz | tar xz +cd frankenphp-main/caddy/frankenphp +CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go build -tags=nobadger,nomysql,nopgx +``` diff --git a/docs/cn/config.md b/docs/cn/config.md index 8c24680f..3b468112 100644 --- a/docs/cn/config.md +++ b/docs/cn/config.md @@ -1,41 +1,44 @@ -# 配置 +# 配置 -FrankenPHP,Caddy 以及 Mercure 和 Vulcain 模块可以使用 [Caddy 支持的格式](https://caddyserver.com/docs/getting-started#your-first-config) 进行配置。 +FrankenPHP、Caddy 以及 Mercure 和 Vulcain 模块可以使用 [Caddy 支持的格式](https://caddyserver.com/docs/getting-started#your-first-config) 进行配置。 + +在 [Docker 镜像](docker.md) 中,`Caddyfile` 位于 `/etc/frankenphp/Caddyfile`。 +静态二进制文件也会在执行 `frankenphp run` 命令的目录中查找 `Caddyfile`。 +你可以使用 `-c` 或 `--config` 选项指定自定义路径。 -在[Docker 映像](docker.md) 中,`Caddyfile` 位于 `/etc/frankenphp/Caddyfile`。 -静态二进制文件会在启动时所在的目录中查找 `Caddyfile`。 PHP 本身可以[使用 `php.ini` 文件](https://www.php.net/manual/zh/configuration.file.php)进行配置。 -PHP 解释器将在以下位置查找: -Docker: +根据你的安装方法,PHP 解释器将在上述位置查找配置文件。 -- php.ini: `/usr/local/etc/php/php.ini` 默认情况下不提供 php.ini。 +## Docker + +- `php.ini`: `/usr/local/etc/php/php.ini`(默认情况下不提供 `php.ini`) - 附加配置文件: `/usr/local/etc/php/conf.d/*.ini` -- php 扩展: `/usr/local/lib/php/extensions/no-debug-zts-/` +- PHP 扩展: `/usr/local/lib/php/extensions/no-debug-zts-/` - 你应该复制 PHP 项目提供的官方模板: ```dockerfile FROM dunglas/frankenphp -# 生产: +# 生产环境: RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini -# 开发: +# 或开发环境: RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini ``` -FrankenPHP 安装 (.rpm 或 .deb): +## RPM 和 Debian 包 -- php.ini: `/etc/frankenphp/php.ini` 默认情况下提供带有生产预设的 php.ini 文件。 +- `php.ini`: `/etc/frankenphp/php.ini`(默认情况下提供带有生产预设的 `php.ini` 文件) - 附加配置文件: `/etc/frankenphp/php.d/*.ini` -- php 扩展: `/usr/lib/frankenphp/modules/` +- PHP 扩展: `/usr/lib/frankenphp/modules/` -静态二进制: +## 静态二进制文件 -- php.ini: 执行 `frankenphp run` 或 `frankenphp php-server` 的目录,然后是 `/etc/frankenphp/php.ini` +- `php.ini`: 执行 `frankenphp run` 或 `frankenphp php-server` 的目录,然后是 `/etc/frankenphp/php.ini` - 附加配置文件: `/etc/frankenphp/php.d/*.ini` -- php 扩展: 无法加载 -- 复制[PHP 源代码](https://github.com/php/php-src/)中提供的`php.ini-production`或`php.ini-development`中的一个。 +- PHP 扩展: 无法加载,将它们打包在二进制文件本身中 +- 复制 [PHP 源代码](https://github.com/php/php-src/) 中提供的 `php.ini-production` 或 `php.ini-development` 中的一个。 ## Caddyfile 配置 @@ -47,22 +50,28 @@ FrankenPHP 安装 (.rpm 或 .deb): localhost { # 启用压缩(可选) encode zstd br gzip - # 执行当前目录中的 PHP 文件并提供资产 + # 在当前目录中执行 PHP 文件并提供资源服务 php_server } ``` -你也可以使用全局选项显式配置 FrankenPHP: +你还可以使用全局选项显式配置 FrankenPHP: `frankenphp` [全局选项](https://caddyserver.com/docs/caddyfile/concepts#global-options) 可用于配置 FrankenPHP。 ```caddyfile { frankenphp { - num_threads # 设置要启动的 PHP 线程数。默认值:可用 CPU 数量的 2 倍。 + num_threads # 设置要启动的 PHP 线程数量。默认:可用 CPU 数量的 2 倍。 + max_threads # 限制可以在运行时启动的额外 PHP 线程的数量。默认值:num_threads。可以设置为 'auto'。 + max_wait_time # 设置请求在超时之前可以等待的最大时间,直到找到一个空闲的 PHP 线程。 默认:禁用。 + php_ini # 设置一个 php.ini 指令。可以多次使用以设置多个指令。 worker { - file # 设置 worker 脚本的路径。 - num # 设置要启动的 PHP 线程数,默认为可用 CPU 数的 2 倍。 - env # 将额外的环境变量设置为给定值。可以为多个环境变量多次指定。 + file # 设置工作脚本的路径。 + num # 设置要启动的 PHP 线程数量,默认为可用 CPU 数量的 2 倍。 + env # 设置一个额外的环境变量为给定的值。可以多次指定以设置多个环境变量。 + watch # 设置要监视文件更改的路径。可以为多个路径多次指定。 + name # 设置worker的名称,用于日志和指标。默认值:worker文件的绝对路径。 + max_consecutive_failures # 设置在工人被视为不健康之前的最大连续失败次数,-1意味着工人将始终重新启动。默认值:6。 } } } @@ -70,7 +79,7 @@ localhost { # ... ``` -或者,你可以使用 `worker` 选项的一行缩写形式: +或者,您可以使用 `worker` 选项的一行简短形式。 ```caddyfile { @@ -82,11 +91,11 @@ localhost { # ... ``` -如果在同一服务器上运行多个应用,还可以定义多个 worker: +如果您在同一服务器上服务多个应用程序,您还可以定义多个工作线程: ```caddyfile app.example.com { - root /path/to/app/public + root /path/to/app/public php_server { root /path/to/app/public # 允许更好的缓存 worker index.php @@ -94,23 +103,26 @@ app.example.com { } other.example.com { - root /path/to/other/public + root /path/to/other/public php_server { root /path/to/other/public worker index.php } } + # ... ``` -通常你只需要 `php_server` 指令, -但如果要完全控制,则可以使用较低级别的 `php` 指令: +使用 `php_server` 指令通常是您需要的。 +但是如果你需要完全控制,你可以使用更低级的 `php` 指令。 +`php` 指令将所有输入传递给 PHP,而不是先检查是否 +是一个PHP文件。在[性能页面](performance.md#try_files)中了解更多关于它的信息。 -使用 `php_server` 指令等效于以下配置: +使用 `php_server` 指令等同于以下配置: ```caddyfile route { - # 为目录请求添加尾部斜杠 + # 为目录请求添加尾斜杠 @canonicalPath { file {path}/index.php not path */ @@ -129,43 +141,161 @@ route { } ``` -`php_server` 和 `php` 指令具有以下选项: +`php_server` 和 `php` 指令有以下选项: ```caddyfile php_server [] { - root # 设置站点的根目录。默认值:`root` 指令。 - split_path # 设置用于将 URI 拆分为两部分的子字符串。第一个匹配的子字符串将用于从路径中拆分"路径信息"。第一个部分以匹配的子字符串为后缀,并将假定为实际资源(CGI 脚本)名称。第二部分将设置为PATH_INFO,供脚本使用。默认值:`.php` - resolve_root_symlink false # 禁用将 `root` 目录在符号链接时将其解析为实际值(默认启用)。 - env # 设置额外的环境变量,可以设置多个环境变量。 + root # 将根文件夹设置为站点。默认值:`root` 指令。 + split_path # 设置用于将 URI 分割成两部分的子字符串。第一个匹配的子字符串将用来将 "路径信息" 与路径分开。第一部分后缀为匹配的子字符串,并将被视为实际资源(CGI 脚本)名称。第二部分将被设置为脚本使用的 PATH_INFO。默认值:`.php`。 + resolve_root_symlink false # 禁用通过评估符号链接(如果存在)将 `root` 目录解析为其实际值(默认启用)。 + env # 设置一个额外的环境变量为给定的值。可以多次指定以设置多个环境变量。 file_server off # 禁用内置的 file_server 指令。 - worker { # 创建特定于此服务器的 worker。可以为多个 worker 多次指定。 - file # 设置 worker 脚本的路径,可以相对于 php_server 根目录 - num # 设置要启动的 PHP 线程数,默认为可用 CPU 数的 2 倍 - name # 为 worker 设置名称,用于日志和指标。默认值:worker 文件的绝对路径。在 php_server 块中定义时始终以 m# 开头。 + worker { # 为此服务器创建特定的worker。可以多次指定以创建多个workers。 + file # 设置工作脚本的路径,可以相对于 php_server 根目录 + num # 设置要启动的 PHP 线程数,默认为可用数量的 2 倍 + name # 为worker设置名称,用于日志和指标。默认值:worker文件的绝对路径。定义在 php_server 块中时,始终以 m# 开头。 watch # 设置要监视文件更改的路径。可以为多个路径多次指定。 - env # 将额外的环境变量设置为给定值。可以为多个环境变量多次指定。此 worker 的环境变量也从 php_server 父级继承,但可以在此处覆盖。 + env # 设置一个额外的环境变量为给定值。可以多次指定以设置多个环境变量。此工作进程的环境变量也从 php_server 父进程继承,但可以在此处覆盖。 + match # 将worker匹配到路径模式。覆盖 try_files,并且只能在 php_server 指令中使用。 } - worker # 也可以像在全局 frankenphp 块中一样使用简短形式。 + worker # 也可以像在全局 frankenphp 块中那样使用简短形式。 } ``` +### 监控文件变化 + +由于 workers 只会启动您的应用程序一次并将其保留在内存中, +因此对您的 PHP 文件的任何更改不会立即反映出来。 + +Wworkers 可以通过 `watch` 指令在文件更改时重新启动。 +这对开发环境很有用。 + +```caddyfile +{ + frankenphp { + worker { + file /path/to/app/public/worker.php + watch + } + } +} +``` + +如果没有指定 `watch` 目录,它将回退到 `./**/*.{php,yaml,yml,twig,env}`, +这将监视启动 FrankenPHP 进程的目录及其子目录中的所有 `.php`、`.yaml`、`.yml`、`.twig` 和 `.env` 文件。 +你也可以通过 [shell 文件名模式](https://pkg.go.dev/path/filepath#Match) 指定一个或多个目录: + +```caddyfile +{ + frankenphp { + worker { + file /path/to/app/public/worker.php + watch /path/to/app # 监视 /path/to/app 所有子目录中的所有文件 + watch /path/to/app/*.php # 监视位于/path/to/app中的以.php结尾的文件 + watch /path/to/app/**/*.php # 监视 /path/to/app 及子目录中的 PHP 文件 + watch /path/to/app/**/*.{php,twig} # 在/path/to/app及其子目录中监视PHP和Twig文件 + } + } +} +``` + +- `**` 模式表示递归监视 +- 目录也可以是相对的(相对于FrankenPHP进程启动的位置) +- 如果您定义了多个workers,当文件发生更改时,将重新启动所有workers。 +- 小心查看在运行时创建的文件(如日志),因为它们可能导致不必要的工作进程重启。 + +文件监视器基于[e-dant/watcher](https://github.com/e-dant/watcher)。 + +## 将 worker 匹配到一条路径 + +在传统的PHP应用程序中,脚本总是放在公共目录中。 +这对于工作脚本也是如此,这些脚本被视为任何其他PHP脚本。 +如果您想将工作脚本放在公共目录外,可以通过 `match` 指令来实现。 + +`match` 指令是 `try_files` 的一种优化替代方案,仅在 `php_server` 和 `php` 内部可用。 +以下示例将在公共目录中提供文件(如果存在) +并将请求转发给与路径模式匹配的 worker。 + +```caddyfile +{ + frankenphp { + php_server { + worker { + file /path/to/worker.php # 文件可以在公共路径之外 + match /api/* # 所有以 /api/ 开头的请求将由此 worker 处理 + } + } + } +} +``` + +### 全双工 (HTTP/1) + +在使用HTTP/1.x时,可能希望启用全双工模式,以便在完整主体之前写入响应。 +已被阅读。(例如:WebSocket、服务器发送事件等。) + +这是一个可选配置,需要添加到 `Caddyfile` 中的全局选项中: + +```caddyfile +{ + servers { + enable_full_duplex + } +} +``` + +> [!CAUTION] +> +> 启用此选项可能导致不支持全双工的旧HTTP/1.x客户端死锁。 +> 这也可以通过配置 `CADDY_GLOBAL_OPTIONS` 环境配置来实现: + +```sh +CADDY_GLOBAL_OPTIONS="servers { + enable_full_duplex +}" +``` + +您可以在[Caddy文档](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex)中找到有关此设置的更多信息。 + ## 环境变量 -以下环境变量可用于在 `Caddyfile` 中注入 Caddy 指令,而无需对其进行修改: +可以使用以下环境变量在不修改 `Caddyfile` 的情况下注入 Caddy 指令: -- `SERVER_NAME`: 更改 [要监听的地址](https://caddyserver.com/docs/caddyfile/concepts#addresses),提供的主机名也将用于生成的 TLS 证书 -- `CADDY_GLOBAL_OPTIONS`: 注入 [全局选项](https://caddyserver.com/docs/caddyfile/options) +- `SERVER_NAME`: 更改[监听的地址](https://caddyserver.com/docs/caddyfile/concepts#addresses),提供的宿主名也将用于生成的TLS证书。 +- `SERVER_ROOT`: 更改网站的根目录,默认为 `public/` +- `CADDY_GLOBAL_OPTIONS`: 注入[全局选项](https://caddyserver.com/docs/caddyfile/options) - `FRANKENPHP_CONFIG`: 在 `frankenphp` 指令下注入配置 +至于 FPM 和 CLI SAPIs,环境变量默认在 `$_SERVER` 超全局中暴露。 + +[the `variables_order` PHP 指令](https://www.php.net/manual/en/ini.core.php#ini.variables-order) 的 `S` 值始终等于 `ES`,无论 `E` 在该指令中的其他位置如何。 + ## PHP 配置 -要加载 [其他 PHP INI 配置文件](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan), -可以使用 `PHP_INI_SCAN_DIR` 环境变量。 -设置后,PHP 将加载给定目录中存在 `.ini` 扩展名的所有文件。 +加载[附加的 PHP 配置文件](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan), +`PHP_INI_SCAN_DIR`环境变量可以被使用。 +设置后,PHP 将加载给定目录中所有带有 `.ini` 扩展名的文件。 + +您还可以通过在 `Caddyfile` 中使用 `php_ini` 指令来更改 PHP 配置: + +```caddyfile +{ + frankenphp { + php_ini memory_limit 256M + + # 或者 + + php_ini { + memory_limit 256M + max_execution_time 15 + } + } +} +``` ## 启用调试模式 -使用 Docker 镜像时,将 `CADDY_GLOBAL_OPTIONS` 环境变量设置为 `debug` 以启用调试模式: +使用Docker镜像时,将`CADDY_GLOBAL_OPTIONS`环境变量设置为`debug`以启用调试模式: ```console docker run -v $PWD:/app/public \ diff --git a/docs/cn/docker.md b/docs/cn/docker.md index 3f28641f..4b7d191f 100644 --- a/docs/cn/docker.md +++ b/docs/cn/docker.md @@ -1,7 +1,15 @@ # 构建自定义 Docker 镜像 -[FrankenPHP Docker 镜像](https://hub.docker.com/r/dunglas/frankenphp) 基于 [官方 PHP 镜像](https://hub.docker.com/_/php/)。 -Alpine Linux 和 Debian 衍生版适用于常见的处理器架构,支持 PHP 8.2、8.3 和 8.4。。[查看 Tags](https://hub.docker.com/r/dunglas/frankenphp/tags)。 +[FrankenPHP Docker 镜像](https://hub.docker.com/r/dunglas/frankenphp) 基于 [官方 PHP 镜像](https://hub.docker.com/_/php/)。提供适用于流行架构的 Debian 和 Alpine Linux 变体。推荐使用 Debian 变体。 + +提供 PHP 8.2、8.3 和 8.4 的变体。 + +标签遵循此模式:`dunglas/frankenphp:-php-` + +- `` 和 `` 分别是 FrankenPHP 和 PHP 的版本号,范围从主版本(例如 `1`)、次版本(例如 `1.2`)到补丁版本(例如 `1.2.3`)。 +- `` 要么是 `bookworm`(用于 Debian Bookworm)要么是 `alpine`(用于 Alpine 的最新稳定版本)。 + +[浏览标签](https://hub.docker.com/r/dunglas/frankenphp/tags)。 ## 如何使用镜像 @@ -71,13 +79,13 @@ FROM dunglas/frankenphp AS runner COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp ``` -FrankenPHP 提供的 `builder` 镜像包含 libphp 的编译版本。 +FrankenPHP 提供的 `builder` 镜像包含 `libphp` 的编译版本。 [用于构建的镜像](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder) 适用于所有版本的 FrankenPHP 和 PHP,包括 Alpine 和 Debian。 > [!TIP] > > 如果你的系统基于 musl libc(Alpine Linux 上默认使用)并搭配 Symfony 使用, -> 你可能需要 [增加默认堆栈大小](compile.md#使用-xcaddy)。 +> 你可能需要 [增加默认堆栈大小](compile.md#using-xcaddy)。 ## 默认启用 worker 模式 @@ -136,7 +144,7 @@ volumes: FrankenPHP 可以在 Docker 中以非 root 用户身份运行。 -下面是一个示例 Dockerfile: +下面是一个示例 `Dockerfile`: ```dockerfile FROM dunglas/frankenphp @@ -148,18 +156,45 @@ RUN \ useradd ${USER}; \ # 需要开放80和443端口的权限 setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \ - # 需要 /data/caddy 和 /config/caddy 目录的写入权限 - chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy; + # 需要 /config/caddy 和 /data/caddy 目录的写入权限 + chown -R ${USER}:${USER} /config/caddy /data/caddy USER ${USER} ``` +### 无权限运行 + +即使在无根运行时,FrankenPHP 也需要 `CAP_NET_BIND_SERVICE` 权限来将 +Web 服务器绑定到特权端口(80 和 443)。 + +如果你在非特权端口(1024 及以上)上公开 FrankenPHP,则可以以非 root 用户身份运行 +Web 服务器,并且不需要任何权限: + +```dockerfile +FROM dunglas/frankenphp + +ARG USER=appuser + +RUN \ + # 在基于 alpine 的发行版使用 "adduser -D ${USER}" + useradd ${USER}; \ + # 移除默认权限 + setcap -r /usr/local/bin/frankenphp; \ + # 给予 /config/caddy 和 /data/caddy 写入权限 + chown -R ${USER}:${USER} /config/caddy /data/caddy + +USER ${USER} +``` + +接下来,设置 `SERVER_NAME` 环境变量以使用非特权端口。 +示例:`:8000` + ## 更新 Docker 镜像会按照以下条件更新: -* 发布新的版本后 -* 每日 4:00(UTC 时间)检查新的 PHP 镜像 +- 发布新的版本后 +- 每日 4:00(UTC 时间)检查新的 PHP 镜像 ## 开发版本 diff --git a/docs/cn/embed.md b/docs/cn/embed.md index 3f27c72c..3897ad39 100644 --- a/docs/cn/embed.md +++ b/docs/cn/embed.md @@ -6,6 +6,8 @@ FrankenPHP 能够将 PHP 应用程序的源代码和资源文件嵌入到静态 了解有关此功能的更多信息 [Kévin 在 SymfonyCon 上的演讲](https://dunglas.dev/2023/12/php-and-symfony-apps-as-standalone-binaries/)。 +有关嵌入 Laravel 应用程序,请[阅读此特定文档条目](laravel.md#laravel-apps-as-standalone-binaries)。 + ## 准备你的应用 在创建独立二进制文件之前,请确保应用已准备好进行打包。 @@ -29,7 +31,8 @@ cd $TMPDIR/my-prepared-app echo APP_ENV=prod > .env.local echo APP_DEBUG=0 >> .env.local -# 删除测试文件 +# 删除测试和其他不需要的文件以节省空间 +# 或者,将这些文件添加到您的 .gitattributes 文件中,并设置 export-ignore 属性 rm -Rf tests/ # 安装依赖项 @@ -39,6 +42,11 @@ composer install --ignore-platform-reqs --no-dev -a composer dump-env prod ``` +### 自定义配置 + +要自定义[配置](config.md),您可以放置一个 `Caddyfile` 以及一个 `php.ini` 文件 +在应用程序的主目录中嵌入(在之前的示例中是`$TMPDIR/my-prepared-app`)。 + ## 创建 Linux 二进制文件 创建 Linux 二进制文件的最简单方法是使用我们提供的基于 Docker 的构建器。 @@ -52,17 +60,15 @@ composer dump-env prod WORKDIR /go/src/app/dist/app COPY . . - # 构建静态二进制文件,只选择你需要的 PHP 扩展 - WORKDIR /go/src/app/ - RUN EMBED=dist/app/ \ - PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \ - ./build-static.sh - ``` + # 构建静态二进制文件 + WORKDIR /go/src/app/ + RUN EMBED=dist/app/ ./build-static.sh + ``` > [!CAUTION] > - > 某些 .dockerignore 文件(例如默认的 [symfony-docker .dockerignore](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore))会忽略 vendor - > 文件夹和环境文件。在构建之前,请务必调整或删除 .dockerignore 文件。 + > 某些 `.dockerignore` 文件(例如默认的 [Symfony Docker `.dockerignore`](https://github.com/dunglas/symfony-docker/blob/main/.dockerignore)) + > 会忽略 `vendor/` 文件夹和 `.env` 文件。在构建之前,请务必调整或删除 `.dockerignore` 文件。 2. 构建: @@ -85,9 +91,7 @@ composer dump-env prod ```console git clone https://github.com/php/frankenphp cd frankenphp -EMBED=/path/to/your/app \ - PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \ - ./build-static.sh +EMBED=/path/to/your/app ./build-static.sh ``` 在 `dist/` 目录中生成的二进制文件名称为 `frankenphp--`。 @@ -120,13 +124,20 @@ EMBED=/path/to/your/app \ ./my-app php-cli bin/console ``` +## PHP Extensions + +默认情况下,脚本将构建您项目的 `composer.json` 文件中所需的扩展(如果有的话)。 +如果 `composer.json` 文件不存在,将构建默认扩展,如 [静态构建条目](static.md) 中所述。 + +要自定义扩展,请使用 `PHP_EXTENSIONS` 环境变量。 + ## 自定义构建 [阅读静态构建文档](static.md) 查看如何自定义二进制文件(扩展、PHP 版本等)。 ## 分发二进制文件 -创建的二进制文件不会被压缩。 -若要在发送文件之前减小文件的大小,可以对其进行压缩。 +在Linux上,创建的二进制文件使用[UPX](https://upx.github.io)进行压缩。 +在Mac上,您可以在发送文件之前压缩它以减小文件大小。 我们推荐使用 `xz`。 diff --git a/docs/cn/extensions.md b/docs/cn/extensions.md new file mode 100644 index 00000000..76cf731a --- /dev/null +++ b/docs/cn/extensions.md @@ -0,0 +1,746 @@ +# 使用 Go 编写 PHP 扩展 + +使用 FrankenPHP,你可以**使用 Go 编写 PHP 扩展**,这允许你创建**高性能的原生函数**,可以直接从 PHP 调用。你的应用程序可以利用任何现有或新的 Go 库,以及直接从你的 PHP 代码中使用**协程(goroutines)的并发模型**。 + +编写 PHP 扩展通常使用 C 语言完成,但通过一些额外的工作,也可以使用其他语言编写。PHP 扩展允许你利用底层语言的强大功能来扩展 PHP 的功能,例如,通过添加原生函数或优化特定操作。 + +借助 Caddy 模块,你可以使用 Go 编写 PHP 扩展,并将其快速集成到 FrankenPHP 中。 + +## 两种方法 + +FrankenPHP 提供两种方式来创建 Go 语言的 PHP 扩展: + +1. **使用扩展生成器** - 推荐的方法,为大多数用例生成所有必要的样板代码,让你专注于编写 Go 代码 +2. **手动实现** - 对于高级用例,完全控制扩展结构 + +我们将从生成器方法开始,因为这是最简单的入门方式,然后为那些需要完全控制的人展示手动实现。 + +## 使用扩展生成器 + +FrankenPHP 捆绑了一个工具,允许你**仅使用 Go 创建 PHP 扩展**。**无需编写 C 代码**或直接使用 CGO:FrankenPHP 还包含一个**公共类型 API**,帮助你在 Go 中编写扩展,而无需担心**PHP/C 和 Go 之间的类型转换**。 + +> [!TIP] +> 如果你想了解如何从头开始在 Go 中编写扩展,可以阅读下面的手动实现部分,该部分演示了如何在不使用生成器的情况下在 Go 中编写 PHP 扩展。 + +请记住,此工具**不是功能齐全的扩展生成器**。它旨在帮助你在 Go 中编写简单的扩展,但它不提供 PHP 扩展的最高级功能。如果你需要编写更**复杂和优化**的扩展,你可能需要编写一些 C 代码或直接使用 CGO。 + +### 先决条件 + +正如下面的手动实现部分所涵盖的,你需要[获取 PHP 源代码](https://www.php.net/downloads.php)并创建一个新的 Go 模块。 + +#### 创建新模块并获取 PHP 源代码 + +在 Go 中编写 PHP 扩展的第一步是创建一个新的 Go 模块。你可以使用以下命令: + +```console +go mod init github.com/my-account/my-module +``` + +第二步是为后续步骤[获取 PHP 源代码](https://www.php.net/downloads.php)。获取后,将它们解压到你选择的目录中,不要放在你的 Go 模块内: + +```console +tar xf php-* +``` + +### 编写扩展 + +现在一切都设置好了,可以在 Go 中编写你的原生函数。创建一个名为 `stringext.go` 的新文件。我们的第一个函数将接受一个字符串作为参数,重复次数,一个布尔值来指示是否反转字符串,并返回结果字符串。这应该看起来像这样: + +```go +import ( + "C" + "github.com/dunglas/frankenphp" + "strings" +) + +//export_php:function repeat_this(string $str, int $count, bool $reverse): string +func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer { + str := frankenphp.GoString(unsafe.Pointer(s)) + + result := strings.Repeat(str, int(count)) + if reverse { + runes := []rune(result) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + result = string(runes) + } + + return frankenphp.PHPString(result, false) +} +``` + +这里有两个重要的事情要注意: + +* 指令注释 `//export_php:function` 定义了 PHP 中的函数签名。这是生成器知道如何使用正确的参数和返回类型生成 PHP 函数的方式; +* 函数必须返回 `unsafe.Pointer`。FrankenPHP 提供了一个 API 来帮助你在 C 和 Go 之间进行类型转换。 + +虽然第一点不言自明,但第二点可能更难理解。让我们在下一节中深入了解类型转换。 + +### 类型转换 + +虽然一些变量类型在 C/PHP 和 Go 之间具有相同的内存表示,但某些类型需要更多逻辑才能直接使用。这可能是编写扩展时最困难的部分,因为它需要了解 Zend 引擎的内部结构以及变量在 PHP 中的内部存储方式。此表总结了你需要知道的内容: + +| PHP 类型 | Go 类型 | 直接转换 | C 到 Go 助手 | Go 到 C 助手 | 类方法支持 | +|--------------------|---------------------|----------|----------------------|----------------------|------------| +| `int` | `int64` | ✅ | - | - | ✅ | +| `?int` | `*int64` | ✅ | - | - | ✅ | +| `float` | `float64` | ✅ | - | - | ✅ | +| `?float` | `*float64` | ✅ | - | - | ✅ | +| `bool` | `bool` | ✅ | - | - | ✅ | +| `?bool` | `*bool` | ✅ | - | - | ✅ | +| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ | +| `array` | `*frankenphp.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | ✅ | +| `object` | `struct` | ❌ | _尚未实现_ | _尚未实现_ | ❌ | + +> [!NOTE] +> 此表尚不详尽,将随着 FrankenPHP 类型 API 变得更加完整而完善。 +> +> 特别是对于类方法,目前支持原始类型和数组。对象尚不能用作方法参数或返回类型。 + +如果你参考上一节的代码片段,你可以看到助手用于转换第一个参数和返回值。我们的 `repeat_this()` 函数的第二和第三个参数不需要转换,因为底层类型的内存表示对于 C 和 Go 都是相同的。 + +#### 处理数组 + +FrankenPHP 通过 `frankenphp.Array` 类型为 PHP 数组提供原生支持。此类型表示 PHP 索引数组(列表)和关联数组(哈希映射),具有有序的键值对。 + +**在 Go 中创建和操作数组:** + +```go +//export_php:function process_data(array $input): array +func process_data(arr *C.zval) unsafe.Pointer { + // 将 PHP 数组转换为 Go + goArray := frankenphp.GoArray(unsafe.Pointer(arr)) + + result := &frankenphp.Array{} + + result.SetInt(0, "first") + result.SetInt(1, "second") + result.Append("third") // 自动分配下一个整数键 + + result.SetString("name", "John") + result.SetString("age", int64(30)) + + for i := uint32(0); i < goArray.Len(); i++ { + key, value := goArray.At(i) + if key.Type == frankenphp.PHPStringKey { + result.SetString("processed_"+key.Str, value) + } else { + result.SetInt(key.Int+100, value) + } + } + + // 转换回 PHP 数组 + return frankenphp.PHPArray(result) +} +``` + +**`frankenphp.Array` 的关键特性:** + +* **有序键值对** - 像 PHP 数组一样维护插入顺序 +* **混合键类型** - 在同一数组中支持整数和字符串键 +* **类型安全** - `PHPKey` 类型确保正确的键处理 +* **自动列表检测** - 转换为 PHP 时,自动检测数组应该是打包列表还是哈希映射 +* **不支持对象** - 目前,只有标量类型和数组可以用作值。提供对象将导致 PHP 数组中的 `null` 值。 + +**可用方法:** + +* `SetInt(key int64, value interface{})` - 使用整数键设置值 +* `SetString(key string, value interface{})` - 使用字符串键设置值 +* `Append(value interface{})` - 使用下一个可用整数键添加值 +* `Len() uint32` - 获取元素数量 +* `At(index uint32) (PHPKey, interface{})` - 获取索引处的键值对 +* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - 转换为 PHP 数组 + +### 声明原生 PHP 类 + +生成器支持将 Go 结构体声明为**不透明类**,可用于创建 PHP 对象。你可以使用 `//export_php:class` 指令注释来定义 PHP 类。例如: + +```go +//export_php:class User +type UserStruct struct { + Name string + Age int +} +``` + +#### 什么是不透明类? + +**不透明类**是内部结构(属性)对 PHP 代码隐藏的类。这意味着: + +* **无直接属性访问**:你不能直接从 PHP 读取或写入属性(`$user->name` 不起作用) +* **仅方法接口** - 所有交互必须通过你定义的方法进行 +* **更好的封装** - 内部数据结构完全由 Go 代码控制 +* **类型安全** - 没有 PHP 代码使用错误类型破坏内部状态的风险 +* **更清晰的 API** - 强制设计适当的公共接口 + +这种方法提供了更好的封装,并防止 PHP 代码意外破坏 Go 对象的内部状态。与对象的所有交互都必须通过你明确定义的方法进行。 + +#### 为类添加方法 + +由于属性不能直接访问,你**必须定义方法**来与不透明类交互。使用 `//export_php:method` 指令来定义行为: + +```go +//export_php:class User +type UserStruct struct { + Name string + Age int +} + +//export_php:method User::getName(): string +func (us *UserStruct) GetUserName() unsafe.Pointer { + return frankenphp.PHPString(us.Name, false) +} + +//export_php:method User::setAge(int $age): void +func (us *UserStruct) SetUserAge(age int64) { + us.Age = int(age) +} + +//export_php:method User::getAge(): int +func (us *UserStruct) GetUserAge() int64 { + return int64(us.Age) +} + +//export_php:method User::setNamePrefix(string $prefix = "User"): void +func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) { + us.Name = frankenphp.GoString(unsafe.Pointer(prefix)) + ": " + us.Name +} +``` + +#### 可空参数 + +生成器支持在 PHP 签名中使用 `?` 前缀的可空参数。当参数可空时,它在你的 Go 函数中变成指针,允许你检查值在 PHP 中是否为 `null`: + +```go +//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void +func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) { + // 检查是否提供了 name(不为 null) + if name != nil { + us.Name = frankenphp.GoString(unsafe.Pointer(name)) + } + + // 检查是否提供了 age(不为 null) + if age != nil { + us.Age = int(*age) + } + + // 检查是否提供了 active(不为 null) + if active != nil { + us.Active = *active + } +} +``` + +**关于可空参数的要点:** + +* **可空原始类型**(`?int`、`?float`、`?bool`)在 Go 中变成指针(`*int64`、`*float64`、`*bool`) +* **可空字符串**(`?string`)仍然是 `*C.zend_string`,但可以是 `nil` +* **在解引用指针值之前检查 `nil`** +* **PHP `null` 变成 Go `nil`** - 当 PHP 传递 `null` 时,你的 Go 函数接收 `nil` 指针 + +> [!WARNING] +> 目前,类方法有以下限制。**不支持对象**作为参数类型或返回类型。**完全支持数组**作为参数和返回类型。支持的类型:`string`、`int`、`float`、`bool`、`array` 和 `void`(用于返回类型)。**完全支持可空参数类型**,适用于所有标量类型(`?string`、`?int`、`?float`、`?bool`)。 + +生成扩展后,你将被允许在 PHP 中使用类及其方法。请注意,你**不能直接访问属性**: + +```php +setAge(25); +echo $user->getName(); // 输出:(空,默认值) +echo $user->getAge(); // 输出:25 +$user->setNamePrefix("Employee"); + +// ✅ 这也可以工作 - 可空参数 +$user->updateInfo("John", 30, true); // 提供所有参数 +$user->updateInfo("Jane", null, false); // Age 为 null +$user->updateInfo(null, 25, null); // Name 和 active 为 null + +// ❌ 这不会工作 - 直接属性访问 +// echo $user->name; // 错误:无法访问私有属性 +// $user->age = 30; // 错误:无法访问私有属性 +``` + +这种设计确保你的 Go 代码完全控制如何访问和修改对象的状态,提供更好的封装和类型安全。 + +### 声明常量 + +生成器支持使用两个指令将 Go 常量导出到 PHP:`//export_php:const` 用于全局常量,`//export_php:classconstant` 用于类常量。这允许你在 Go 和 PHP 代码之间共享配置值、状态代码和其他常量。 + +#### 全局常量 + +使用 `//export_php:const` 指令创建全局 PHP 常量: + +```go +//export_php:const +const MAX_CONNECTIONS = 100 + +//export_php:const +const API_VERSION = "1.2.3" + +//export_php:const +const STATUS_OK = iota + +//export_php:const +const STATUS_ERROR = iota +``` + +#### 类常量 + +使用 `//export_php:classconstant ClassName` 指令创建属于特定 PHP 类的常量: + +```go +//export_php:classconstant User +const STATUS_ACTIVE = 1 + +//export_php:classconstant User +const STATUS_INACTIVE = 0 + +//export_php:classconstant User +const ROLE_ADMIN = "admin" + +//export_php:classconstant Order +const STATE_PENDING = iota + +//export_php:classconstant Order +const STATE_PROCESSING = iota + +//export_php:classconstant Order +const STATE_COMPLETED = iota +``` + +类常量在 PHP 中使用类名作用域访问: + +```php +getName(); // "John Doe" + +echo My\Extension\STATUS_ACTIVE; // 1 +``` + +#### 重要说明 + +* 每个文件只允许**一个**命名空间指令。如果找到多个命名空间指令,生成器将返回错误。 +* 命名空间适用于文件中的**所有**导出符号:函数、类、方法和常量。 +* 命名空间名称遵循 PHP 命名空间约定,使用反斜杠(`\`)作为分隔符。 +* 如果没有声明命名空间,符号将照常导出到全局命名空间。 + +### 生成扩展 + +这就是魔法发生的地方,现在可以生成你的扩展。你可以使用以下命令运行生成器: + +```console +GEN_STUB_FILE=php-src/build/gen_stub.php frankenphp extension-init my_extension.go +``` + +> [!NOTE] +> 不要忘记将 `GEN_STUB_FILE` 环境变量设置为你之前下载的 PHP 源代码中 `gen_stub.php` 文件的路径。这是在手动实现部分中提到的同一个 `gen_stub.php` 脚本。 + +如果一切顺利,应该创建了一个名为 `build` 的新目录。此目录包含扩展的生成文件,包括带有生成的 PHP 函数存根的 `my_extension.go` 文件。 + +### 将生成的扩展集成到 FrankenPHP 中 + +我们的扩展现在已准备好编译并集成到 FrankenPHP 中。为此,请参阅 FrankenPHP [编译文档](compile.md)以了解如何编译 FrankenPHP。使用 `--with` 标志添加模块,指向你的模块路径: + +```console +CGO_ENABLED=1 \ +XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \ +CGO_CFLAGS=$(php-config --includes) \ +CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \ +xcaddy build \ + --output frankenphp \ + --with github.com/my-account/my-module/build +``` + +请注意,你指向在生成步骤中创建的 `/build` 子目录。但是,这不是强制性的:你也可以将生成的文件复制到你的模块目录并直接指向它。 + +### 测试你的生成扩展 + +你可以创建一个 PHP 文件来测试你创建的函数和类。例如,创建一个包含以下内容的 `index.php` 文件: + +```php +process('Hello World', StringProcessor::MODE_LOWERCASE); // "hello world" +echo $processor->process('Hello World', StringProcessor::MODE_UPPERCASE); // "HELLO WORLD" +``` + +一旦你按照上一节所示将扩展集成到 FrankenPHP 中,你就可以使用 `./frankenphp php-server` 运行此测试文件,你应该看到你的扩展正在工作。 + +## 手动实现 + +如果你想了解扩展的工作原理或需要完全控制你的扩展,你可以手动编写它们。这种方法给你完全的控制,但需要更多的样板代码。 + +### 基本函数 + +我们将看到如何在 Go 中编写一个简单的 PHP 扩展,定义一个新的原生函数。此函数将从 PHP 调用,并将触发一个在 Caddy 日志中记录消息的协程。此函数不接受任何参数并且不返回任何内容。 + +#### 定义 Go 函数 + +在你的模块中,你需要定义一个新的原生函数,该函数将从 PHP 调用。为此,创建一个你想要的名称的文件,例如 `extension.go`,并添加以下代码: + +```go +package ext_go + +//#include "extension.h" +import "C" +import ( + "unsafe" + "github.com/caddyserver/caddy/v2" + "github.com/dunglas/frankenphp" +) + +func init() { + frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry)) +} + +//export go_print_something +func go_print_something() { + go func() { + caddy.Log().Info("Hello from a goroutine!") + }() +} +``` + +`frankenphp.RegisterExtension()` 函数通过处理内部 PHP 注册逻辑简化了扩展注册过程。`go_print_something` 函数使用 `//export` 指令表示它将在我们将编写的 C 代码中可访问,这要归功于 CGO。 + +在此示例中,我们的新函数将触发一个在 Caddy 日志中记录消息的协程。 + +#### 定义 PHP 函数 + +为了允许 PHP 调用我们的函数,我们需要定义相应的 PHP 函数。为此,我们将创建一个存根文件,例如 `extension.stub.php`,其中包含以下代码: + +```php + + +extern zend_module_entry ext_module_entry; + +#endif +``` + +接下来,创建一个名为 `extension.c` 的文件,该文件将执行以下步骤: + +* 包含 PHP 头文件; +* 声明我们的新原生 PHP 函数 `go_print()`; +* 声明扩展元数据。 + +让我们首先包含所需的头文件: + +```c +#include +#include "extension.h" +#include "extension_arginfo.h" + +// 包含 Go 导出的符号 +#include "_cgo_export.h" +``` + +然后我们将 PHP 函数定义为原生语言函数: + +```c +PHP_FUNCTION(go_print) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + go_print_something(); +} + +zend_module_entry ext_module_entry = { + STANDARD_MODULE_HEADER, + "ext_go", + ext_functions, /* Functions */ + NULL, /* MINIT */ + NULL, /* MSHUTDOWN */ + NULL, /* RINIT */ + NULL, /* RSHUTDOWN */ + NULL, /* MINFO */ + "0.1.1", + STANDARD_MODULE_PROPERTIES +}; +``` + +在这种情况下,我们的函数不接受参数并且不返回任何内容。它只是调用我们之前定义的 Go 函数,使用 `//export` 指令导出。 + +最后,我们在 `zend_module_entry` 结构中定义扩展的元数据,例如其名称、版本和属性。这些信息对于 PHP 识别和加载我们的扩展是必需的。请注意,`ext_functions` 是指向我们定义的 PHP 函数的指针数组,它由 `gen_stub.php` 脚本在 `extension_arginfo.h` 文件中自动生成。 + +扩展注册由我们在 Go 代码中调用的 FrankenPHP 的 `RegisterExtension()` 函数自动处理。 + +### 高级用法 + +现在我们知道了如何在 Go 中创建基本的 PHP 扩展,让我们复杂化我们的示例。我们现在将创建一个 PHP 函数,该函数接受一个字符串作为参数并返回其大写版本。 + +#### 定义 PHP 函数存根 + +为了定义新的 PHP 函数,我们将修改我们的 `extension.stub.php` 文件以包含新的函数签名: + +```php + [!TIP] +> 不要忽视函数的文档!你可能会与其他开发人员共享扩展存根,以记录如何使用你的扩展以及哪些功能可用。 + +通过使用 `gen_stub.php` 脚本重新生成存根文件,`extension_arginfo.h` 文件应该如下所示: + +```c +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_go_upper, 0, 1, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_FUNCTION(go_upper); + +static const zend_function_entry ext_functions[] = { + ZEND_FE(go_upper, arginfo_go_upper) + ZEND_FE_END +}; +``` + +我们可以看到 `go_upper` 函数定义了一个 `string` 类型的参数和一个 `string` 的返回类型。 + +#### Go 和 PHP/C 之间的类型转换 + +你的 Go 函数不能直接接受 PHP 字符串作为参数。你需要将其转换为 Go 字符串。幸运的是,FrankenPHP 提供了助手函数来处理 PHP 字符串和 Go 字符串之间的转换,类似于我们在生成器方法中看到的。 + +头文件保持简单: + +```c +#ifndef _EXTENSION_H +#define _EXTENSION_H + +#include + +extern zend_module_entry ext_module_entry; + +#endif +``` + +我们现在可以在我们的 `extension.c` 文件中编写 Go 和 C 之间的桥梁。我们将 PHP 字符串直接传递给我们的 Go 函数: + +```c +PHP_FUNCTION(go_upper) +{ + zend_string *str; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(str) + ZEND_PARSE_PARAMETERS_END(); + + zend_string *result = go_upper(str); + RETVAL_STR(result); +} +``` + +你可以在 [PHP 内部手册](https://www.phpinternalsbook.com/php7/extensions_design/php_functions.html#parsing-parameters-zend-parse-parameters) 的专门页面中了解更多关于 `ZEND_PARSE_PARAMETERS_START` 和参数解析的信息。在这里,我们告诉 PHP 我们的函数接受一个 `string` 类型的强制参数作为 `zend_string`。然后我们将此字符串直接传递给我们的 Go 函数,并使用 `RETVAL_STR` 返回结果。 + +只剩下一件事要做:在 Go 中实现 `go_upper` 函数。 + +#### 实现 Go 函数 + +我们的 Go 函数将接受 `*C.zend_string` 作为参数,使用 FrankenPHP 的助手函数将其转换为 Go 字符串,处理它,并将结果作为新的 `*C.zend_string` 返回。助手函数为我们处理所有内存管理和转换复杂性。 + +```go +import "strings" + +//export go_upper +func go_upper(s *C.zend_string) *C.zend_string { + str := frankenphp.GoString(unsafe.Pointer(s)) + + upper := strings.ToUpper(str) + + return (*C.zend_string)(frankenphp.PHPString(upper, false)) +} +``` + +这种方法比手动内存管理更清洁、更安全。FrankenPHP 的助手函数自动处理 PHP 的 `zend_string` 格式和 Go 字符串之间的转换。`PHPString()` 中的 `false` 参数表示我们想要创建一个新的非持久字符串(在请求结束时释放)。 + +> [!TIP] +> 在此示例中,我们不执行任何错误处理,但你应该始终检查指针不是 `nil` 并且数据在 Go 函数中使用之前是有效的。 + +### 将扩展集成到 FrankenPHP 中 + +我们的扩展现在已准备好编译并集成到 FrankenPHP 中。为此,请参阅 FrankenPHP [编译文档](compile.md)以了解如何编译 FrankenPHP。使用 `--with` 标志添加模块,指向你的模块路径: + +```console +CGO_ENABLED=1 \ +XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \ +CGO_CFLAGS=$(php-config --includes) \ +CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \ +xcaddy build \ + --output frankenphp \ + --with github.com/my-account/my-module +``` + +就是这样!你的扩展现在集成到 FrankenPHP 中,可以在你的 PHP 代码中使用。 + +### 测试你的扩展 + +将扩展集成到 FrankenPHP 后,你可以为你实现的函数创建一个包含示例的 `index.php` 文件: + +```php + [!WARNING] +> +> Web 和命令行界面可能有不同的设置。 +> 确保在适当的上下文中运行 `openssl_get_cert_locations()`。 + +[从Mozilla提取的CA证书可以在curl网站上下载](https://curl.se/docs/caextract.html)。 + +或者,许多发行版,包括 Debian、Ubuntu 和 Alpine,提供名为 `ca-certificates` 的软件包,其中包含这些证书。 + +还可以使用 `SSL_CERT_FILE` 和 `SSL_CERT_DIR` 来提示 OpenSSL 在哪里查找 CA 证书: + +```console +# Set TLS certificates environment variables +export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +export SSL_CERT_DIR=/etc/ssl/certs +``` diff --git a/docs/cn/laravel.md b/docs/cn/laravel.md index 39f65baf..d9434e1d 100644 --- a/docs/cn/laravel.md +++ b/docs/cn/laravel.md @@ -30,8 +30,10 @@ docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp root public/ # 启用压缩(可选) encode zstd br gzip - # 执行当前目录中的 PHP 文件并提供资产 - php_server + # 执行当前目录中的 PHP 文件并提供资源 + php_server { + try_files {path} index.php + } } ``` @@ -64,11 +66,119 @@ php artisan octane:frankenphp * `--admin-port`: 管理服务器应可用的端口(默认值: `2019`) * `--workers`: 应可用于处理请求的 worker 数(默认值: `auto`) * `--max-requests`: 在 worker 重启之前要处理的请求数(默认值: `500`) -* `--caddyfile`: FrankenPHP `Caddyfile` 文件的路径 +* `--caddyfile`:FrankenPHP `Caddyfile` 文件的路径(默认: [Laravel Octane 中的存根 `Caddyfile`](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile)) * `--https`: 开启 HTTPS、HTTP/2 和 HTTP/3,自动生成和延长证书 * `--http-redirect`: 启用 HTTP 到 HTTPS 重定向(仅在使用 `--https` 时启用) * `--watch`: 修改应用程序时自动重新加载服务器 * `--poll`: 在监视时使用文件系统轮询,以便通过网络监视文件 * `--log-level`: 在指定日志级别或高于指定日志级别的日志消息 +> [!TIP] +> 要获取结构化的 JSON 日志(在使用日志分析解决方案时非常有用),请明确传递 `--log-level` 选项。 + 你可以了解更多关于 [Laravel Octane 官方文档](https://laravel.com/docs/octane)。 + +## Laravel 应用程序作为独立的可执行文件 + +使用[FrankenPHP 的应用嵌入功能](embed.md),可以将 Laravel 应用程序作为 +独立的二进制文件分发。 + +按照以下步骤将您的Laravel应用程序打包为Linux的独立二进制文件: + +1. 在您的应用程序的存储库中创建一个名为 `static-build.Dockerfile` 的文件: + + ```dockerfile + FROM --platform=linux/amd64 dunglas/frankenphp:static-builder + + # 复制你的应用 + WORKDIR /go/src/app/dist/app + COPY . . + + # 删除测试和其他不必要的文件以节省空间 + # 或者,将这些文件添加到 .dockerignore 文件中 + RUN rm -Rf tests/ + + # 复制 .env 文件 + RUN cp .env.example .env + # 将 APP_ENV 和 APP_DEBUG 更改为适合生产环境 + RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env + + # 根据需要对您的 .env 文件进行其他更改 + + # 安装依赖项 + RUN composer install --ignore-platform-reqs --no-dev -a + + # 构建静态二进制文件 + WORKDIR /go/src/app/ + RUN EMBED=dist/app/ ./build-static.sh + ``` + + > [!CAUTION] + > + > 一些 `.dockerignore` 文件 + > 将忽略 `vendor/` 目录和 `.env` 文件。在构建之前,请确保调整或删除 `.dockerignore` 文件。 + +2. 构建: + + ```console + docker build -t static-laravel-app -f static-build.Dockerfile . + ``` + +3. 提取二进制: + + ```console + docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp + ``` + +4. 填充缓存: + + ```console + frankenphp php-cli artisan optimize + ``` + +5. 运行数据库迁移(如果有的话): + + ```console + frankenphp php-cli artisan migrate + ``` + +6. 生成应用程序的密钥: + + ```console + frankenphp php-cli artisan key:generate + ``` + +7. 启动服务器: + + ```console + frankenphp php-server + ``` + +您的应用程序现在准备好了! + +了解有关可用选项的更多信息,以及如何为其他操作系统构建二进制文件,请参见 [应用程序嵌入](embed.md) +文档。 + +### 更改存储路径 + +默认情况下,Laravel 将上传的文件、缓存、日志等存储在应用程序的 `storage/` 目录中。 +这不适合嵌入式应用,因为每个新版本将被提取到不同的临时目录中。 + +设置 `LARAVEL_STORAGE_PATH` 环境变量(例如,在 `.env` 文件中)或调用 `Illuminate\Foundation\Application::useStoragePath()` 方法以使用临时目录之外的目录。 + +### 使用独立二进制文件运行 Octane + +甚至可以将 Laravel Octane 应用打包为独立的二进制文件! + +为此,[正确安装 Octane](#laravel-octane) 并遵循 [前一部分](#laravel-应用程序作为独立的可执行文件) 中描述的步骤。 + +然后,通过 Octane 在工作模式下启动 FrankenPHP,运行: + +```console +PATH="$PWD:$PATH" frankenphp php-cli artisan octane:frankenphp +``` + +> [!CAUTION] +> +> 为了使命令有效,独立二进制文件**必须**命名为 `frankenphp` +> 因为 Octane 需要一个名为 `frankenphp` 的程序在路径中可用。 diff --git a/docs/cn/mercure.md b/docs/cn/mercure.md index 6efd6091..d51c9eb8 100644 --- a/docs/cn/mercure.md +++ b/docs/cn/mercure.md @@ -1,6 +1,6 @@ # 实时 -FrankenPHP 带有一个内置的 Mercure Hub! +FrankenPHP 配备了内置的 [Mercure](https://mercure.rocks) 中心! Mercure 允许将事件实时推送到所有连接的设备:它们将立即收到 JavaScript 事件。 无需 JS 库或 SDK! @@ -9,4 +9,7 @@ Mercure 允许将事件实时推送到所有连接的设备:它们将立即收 要启用 Mercure Hub,请按照 [Mercure 网站](https://mercure.rocks/docs/hub/config) 中的说明更新 `Caddyfile`。 +Mercure hub 的路径是`/.well-known/mercure`. +在 Docker 中运行 FrankenPHP 时,完整的发送 URL 将类似于 `http://php/.well-known/mercure` (其中 `php` 是运行 FrankenPHP 的容器名称)。 + 要从你的代码中推送 Mercure 更新,我们推荐 [Symfony Mercure Component](https://symfony.com/components/Mercure)(不需要 Symfony 框架来使用)。 diff --git a/docs/cn/metrics.md b/docs/cn/metrics.md new file mode 100644 index 00000000..5207d20e --- /dev/null +++ b/docs/cn/metrics.md @@ -0,0 +1,17 @@ +# 指标 + +当启用 [Caddy 指标](https://caddyserver.com/docs/metrics) 时,FrankenPHP 公开以下指标: + +- `frankenphp_total_threads`:PHP 线程的总数。 +- `frankenphp_busy_threads`:当前正在处理请求的 PHP 线程数(运行中的 worker 始终占用一个线程)。 +- `frankenphp_queue_depth`:常规排队请求的数量 +- `frankenphp_total_workers{worker="[worker_name]"}`:worker 的总数。 +- `frankenphp_busy_workers{worker="[worker_name]"}`:当前正在处理请求的 worker 数量。 +- `frankenphp_worker_request_time{worker="[worker_name]"}`:所有 worker 处理请求所花费的时间。 +- `frankenphp_worker_request_count{worker="[worker_name]"}`:所有 worker 处理的请求数量。 +- `frankenphp_ready_workers{worker="[worker_name]"}`:至少调用过一次 `frankenphp_handle_request` 的 worker 数量。 +- `frankenphp_worker_crashes{worker="[worker_name]"}`:worker 意外终止的次数。 +- `frankenphp_worker_restarts{worker="[worker_name]"}`:worker 被故意重启的次数。 +- `frankenphp_worker_queue_depth{worker="[worker_name]"}`:排队请求的数量。 + +对于 worker 指标,`[worker_name]` 占位符被 Caddyfile 中的 worker 名称替换,否则将使用 worker 文件的绝对路径。 diff --git a/docs/cn/performance.md b/docs/cn/performance.md new file mode 100644 index 00000000..ee11bb07 --- /dev/null +++ b/docs/cn/performance.md @@ -0,0 +1,157 @@ +# 性能 + +默认情况下,FrankenPHP 尝试在性能和易用性之间提供良好的折衷。 +但是,通过使用适当的配置,可以大幅提高性能。 + +## 线程和 Worker 数量 + +默认情况下,FrankenPHP 启动的线程和 worker(在 worker 模式下)数量是可用 CPU 数量的 2 倍。 + +适当的值很大程度上取决于你的应用程序是如何编写的、它做什么以及你的硬件。 +我们强烈建议更改这些值。为了获得最佳的系统稳定性,建议 `num_threads` x `memory_limit` < `available_memory`。 + +要找到正确的值,最好运行模拟真实流量的负载测试。 +[k6](https://k6.io) 和 [Gatling](https://gatling.io) 是很好的工具。 + +要配置线程数,请使用 `php_server` 和 `php` 指令的 `num_threads` 选项。 +要更改 worker 数量,请使用 `frankenphp` 指令的 `worker` 部分的 `num` 选项。 + +### `max_threads` + +虽然准确了解你的流量情况总是更好,但现实应用往往更加 +不可预测。`max_threads` [配置](config.md#caddyfile-config) 允许 FrankenPHP 在运行时自动生成额外线程,直到指定的限制。 +`max_threads` 可以帮助你确定需要多少线程来处理你的流量,并可以使服务器对延迟峰值更具弹性。 +如果设置为 `auto`,限制将基于你的 `php.ini` 中的 `memory_limit` 进行估算。如果无法这样做, +`auto` 将默认为 2x `num_threads`。请记住,`auto` 可能会严重低估所需的线程数。 +`max_threads` 类似于 PHP FPM 的 [pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children)。主要区别是 FrankenPHP 使用线程而不是 +进程,并根据需要自动在不同的 worker 脚本和"经典模式"之间委派它们。 + +## Worker 模式 + +启用 [worker 模式](worker.md) 大大提高了性能, +但你的应用必须适配以兼容此模式: +你需要创建一个 worker 脚本并确保应用不会泄漏内存。 + +## 不要使用 musl + +官方 Docker 镜像的 Alpine Linux 变体和我们提供的默认二进制文件使用 [musl libc](https://musl.libc.org)。 + +众所周知,当使用这个替代 C 库而不是传统的 GNU 库时,PHP [更慢](https://gitlab.alpinelinux.org/alpine/aports/-/issues/14381), +特别是在以 ZTS 模式(线程安全)编译时,这是 FrankenPHP 所必需的。在大量线程环境中,差异可能很显著。 + +另外,[一些错误只在使用 musl 时发生](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl)。 + +在生产环境中,我们建议使用链接到 glibc 的 FrankenPHP。 + +这可以通过使用 Debian Docker 镜像(默认)、从我们的 [Releases](https://github.com/php/frankenphp/releases) 下载 -gnu 后缀二进制文件,或通过[从源代码编译 FrankenPHP](compile.md) 来实现。 + +或者,我们提供使用 [mimalloc 分配器](https://github.com/microsoft/mimalloc) 编译的静态 musl 二进制文件,这缓解了线程场景中的问题。 + +## Go 运行时配置 + +FrankenPHP 是用 Go 编写的。 + +一般来说,Go 运行时不需要任何特殊配置,但在某些情况下, +特定的配置可以提高性能。 + +你可能想要将 `GODEBUG` 环境变量设置为 `cgocheck=0`(FrankenPHP Docker 镜像中的默认值)。 + +如果你在容器(Docker、Kubernetes、LXC...)中运行 FrankenPHP 并限制容器的可用内存, +请将 `GOMEMLIMIT` 环境变量设置为可用内存量。 + +有关更多详细信息,[专门针对此主题的 Go 文档页面](https://pkg.go.dev/runtime#hdr-Environment_Variables) 是充分利用运行时的必读内容。 + +## `file_server` + +默认情况下,`php_server` 指令自动设置文件服务器来 +提供存储在根目录中的静态文件(资产)。 + +此功能很方便,但有成本。 +要禁用它,请使用以下配置: + +```caddyfile +php_server { + file_server off +} +``` + +## `try_files` + +除了静态文件和 PHP 文件外,`php_server` 还会尝试提供你应用程序的索引 +和目录索引文件(`/path/` -> `/path/index.php`)。如果你不需要目录索引, +你可以通过明确定义 `try_files` 来禁用它们,如下所示: + +```caddyfile +php_server { + try_files {path} index.php + root /root/to/your/app # 在这里明确添加根目录允许更好的缓存 +} +``` + +这可以显著减少不必要的文件操作数量。 + +另一种具有 0 个不必要文件系统操作的方法是改用 `php` 指令并按路径将 +文件与 PHP 分开。如果你的整个应用程序由一个入口文件提供服务,这种方法效果很好。 +一个在 `/assets` 文件夹后面提供静态文件的示例[配置](config.md#caddyfile-config)可能如下所示: + +```caddyfile +route { + @assets { + path /assets/* + } + + # /assets 后面的所有内容都由文件服务器处理 + file_server @assets { + root /root/to/your/app + } + + # 不在 /assets 中的所有内容都由你的索引或 worker PHP 文件处理 + rewrite index.php + php { + root /root/to/your/app # 在这里明确添加根目录允许更好的缓存 + } +} +``` + +## 占位符 + +你可以在 `root` 和 `env` 指令中使用[占位符](https://caddyserver.com/docs/conventions#placeholders)。 +但是,这会阻止缓存这些值,并带来显著的性能成本。 + +如果可能,请避免在这些指令中使用占位符。 + +## `resolve_root_symlink` + +默认情况下,如果文档根目录是符号链接,FrankenPHP 会自动解析它(这对于 PHP 正常工作是必要的)。 +如果文档根目录不是符号链接,你可以禁用此功能。 + +```caddyfile +php_server { + resolve_root_symlink false +} +``` + +如果 `root` 指令包含[占位符](https://caddyserver.com/docs/conventions#placeholders),这将提高性能。 +在其他情况下,收益将可以忽略不计。 + +## 日志 + +日志显然非常有用,但根据定义, +它需要 I/O 操作和内存分配,这会大大降低性能。 +确保你[正确设置日志级别](https://caddyserver.com/docs/caddyfile/options#log), +并且只记录必要的内容。 + +## PHP 性能 + +FrankenPHP 使用官方 PHP 解释器。 +所有常见的 PHP 相关性能优化都适用于 FrankenPHP。 + +特别是: + +- 检查 [OPcache](https://www.php.net/manual/zh/book.opcache.php) 是否已安装、启用并正确配置 +- 启用 [Composer 自动加载器优化](https://getcomposer.org/doc/articles/autoloader-optimization.md) +- 确保 `realpath` 缓存对于你的应用程序需求足够大 +- 使用[预加载](https://www.php.net/manual/zh/opcache.preloading.php) + +有关更多详细信息,请阅读[专门的 Symfony 文档条目](https://symfony.com/doc/current/performance.html) +(即使你不使用 Symfony,大多数提示也很有用)。 diff --git a/docs/cn/production.md b/docs/cn/production.md index 17297f1b..01777f93 100644 --- a/docs/cn/production.md +++ b/docs/cn/production.md @@ -18,6 +18,9 @@ ENV SERVER_NAME=your-domain-name.example.com # 如果要禁用 HTTPS,请改用以下值: #ENV SERVER_NAME=:80 +# 如果你的项目不使用 "public" 目录作为 web 根目录,你可以在这里设置: +# ENV SERVER_ROOT=web/ + # 启用 PHP 生产配置 RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" @@ -124,7 +127,7 @@ git clone git@github.com:/.git 进入包含项目 (``) 的目录,并在生产模式下启动应用: ```console -docker compose up -d --wait +docker compose up --wait ``` 你的服务器已启动并运行,并且已自动为你生成 HTTPS 证书。 @@ -132,7 +135,7 @@ docker compose up -d --wait > [!CAUTION] > -> Docker 有一个缓存层,请确保每个部署都有正确的构建,或者使用 --no-cache 选项重新构建项目以避免缓存问题。 +> Docker 有一个缓存层,请确保每个部署都有正确的构建,或者使用 `--no-cache` 选项重新构建项目以避免缓存问题。 ## 在多个节点上部署 diff --git a/docs/cn/static.md b/docs/cn/static.md index 5b8417be..d1a19827 100644 --- a/docs/cn/static.md +++ b/docs/cn/static.md @@ -1,22 +1,50 @@ # 创建静态构建 -基于 [static-php-cli](https://github.com/crazywhalecc/static-php-cli) 项目(这个项目支持所有 SAPI,不仅仅是 `cli`), -FrankenPHP 已支持创建静态二进制,无需安装本地 PHP。 +与其使用本地安装的PHP库, +由于伟大的 [static-php-cli 项目](https://github.com/crazywhalecc/static-php-cli),创建一个静态或基本静态的 FrankenPHP 构建是可能的(尽管它的名字,这个项目支持所有的 SAPI,而不仅仅是 CLI)。 使用这种方法,我们可构建一个包含 PHP 解释器、Caddy Web 服务器和 FrankenPHP 的可移植二进制文件! +完全静态的本地可执行文件不需要任何依赖,并且可以在 [`scratch` Docker 镜像](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch) 上运行。 +然而,它们无法加载动态 PHP 扩展(例如 Xdebug),并且由于使用了 musl libc,有一些限制。 + +大多数静态二进制文件只需要 `glibc` 并且可以加载动态扩展。 + +在可能的情况下,我们建议使用基于glibc的、主要是静态构建的版本。 + FrankenPHP 还支持 [将 PHP 应用程序嵌入到静态二进制文件中](embed.md)。 ## Linux 我们提供了一个 Docker 镜像来构建 Linux 静态二进制文件: +### 基于musl的完全静态构建 + +对于一个在任何Linux发行版上运行且不需要依赖项的完全静态二进制文件,但不支持动态加载扩展: + ```console -docker buildx bake --load static-builder -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 +docker buildx bake --load static-builder-musl +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-musl ``` -生成的静态二进制文件名为 `frankenphp`,可在当前目录中找到。 +为了在高度并发的场景中获得更好的性能,请考虑使用 [mimalloc](https://github.com/microsoft/mimalloc) 分配器。 + +```console +docker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl +``` + +### 基于glibc的,主要静态构建(支持动态扩展) + +对于一个支持动态加载 PHP 扩展的二进制文件,同时又将所选扩展静态编译: + +```console +docker buildx bake --load static-builder-gnu +docker cp $(docker create --name static-builder-gnu dunglas/frankenphp:static-builder-gnu):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphp ; docker rm static-builder-gnu +``` + +该二进制文件支持所有glibc版本2.17及以上,但不支持基于musl的系统(如Alpine Linux)。 + +生成的主要是静态的(除了 `glibc`)二进制文件名为 `frankenphp`,并且可以在当前目录中找到。 如果你想在没有 Docker 的情况下构建静态二进制文件,请查看 macOS 说明,它也适用于 Linux。 @@ -24,12 +52,12 @@ docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-b 默认情况下,大多数流行的 PHP 扩展都会被编译。 -若要减小二进制文件的大小并减少攻击面,可以选择使用 `PHP_EXTENSIONS` Docker 参数来自定义构建的扩展。 +为了减少二进制文件的大小和减少攻击面,您可以选择使用 `PHP_EXTENSIONS` Docker ARG 构建的扩展列表。 -例如,运行以下命令以生成仅包含 `opcache,pdo_sqlite` 扩展的二进制: +例如,运行以下命令仅构建 `opcache` 扩展: ```console -docker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder +docker buildx bake --load --set static-builder-musl.args.PHP_EXTENSIONS=opcache,pdo_sqlite static-builder-musl # ... ``` @@ -38,9 +66,9 @@ docker buildx bake --load --set static-builder.args.PHP_EXTENSIONS=opcache,pdo_s ```console docker buildx bake \ --load \ - --set static-builder.args.PHP_EXTENSIONS=gd \ - --set static-builder.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \ - static-builder + --set static-builder-musl.args.PHP_EXTENSIONS=gd \ + --set static-builder-musl.args.PHP_EXTENSION_LIBS=libjpeg,libwebp \ + static-builder-musl ``` ### 额外的 Caddy 模块 @@ -50,8 +78,8 @@ docker buildx bake \ ```console docker buildx bake \ --load \ - --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" \ - static-builder + --set static-builder-musl.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" \ + static-builder-musl ``` 在本例中,我们为 Caddy 添加了 [Souin](https://souin.io) HTTP 缓存模块,以及 [cbrotli](https://github.com/dunglas/caddy-cbrotli)、[Mercure](https://mercure.rocks) 和 [Vulcain](https://vulcain.rocks) 模块。 @@ -68,7 +96,7 @@ docker buildx bake \ 如果遇到了 GitHub API 速率限制,请在 `GITHUB_TOKEN` 的环境变量中设置 GitHub Personal Access Token: ```console -GITHUB_TOKEN="xxx" docker --load buildx bake static-builder +GITHUB_TOKEN="xxx" docker --load buildx bake static-builder-musl # ... ``` @@ -96,5 +124,38 @@ cd frankenphp * `XCADDY_ARGS`:传递给 [xcaddy](https://github.com/caddyserver/xcaddy) 的参数,例如用于添加额外的 Caddy 模块 * `EMBED`: 要嵌入二进制文件的 PHP 应用程序的路径 * `CLEAN`: 设置后,libphp 及其所有依赖项都是重新构建的(不使用缓存) +* `NO_COMPRESS`: 不要使用UPX压缩生成的二进制文件 * `DEBUG_SYMBOLS`: 设置后,调试符号将被保留在二进制文件内 +* `MIMALLOC`: (实验性,仅限Linux) 用[mimalloc](https://github.com/microsoft/mimalloc)替换musl的mallocng,以提高性能。我们仅建议在musl目标构建中使用此选项,对于glibc,建议禁用此选项,并在运行二进制文件时使用[`LD_PRELOAD`](https://microsoft.github.io/mimalloc/overrides.html)。 * `RELEASE`: (仅限维护者)设置后,生成的二进制文件将上传到 GitHub 上 + +## 扩展 + +使用glibc或基于macOS的二进制文件,您可以动态加载PHP扩展。然而,这些扩展必须使用ZTS支持进行编译。 +由于大多数软件包管理器目前不提供其扩展的 ZTS 版本,因此您必须自己编译它们。 + +为此,您可以构建并运行 `static-builder-gnu` Docker 容器,远程进入它,并使用 `./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config` 编译扩展。 + +关于 [Xdebug 扩展](https://xdebug.org) 的示例步骤: + +```console +docker build -t gnu-ext -f static-builder-gnu.Dockerfile --build-arg FRANKENPHP_VERSION=1.0 . +docker create --name static-builder-gnu -it gnu-ext /bin/sh +docker start static-builder-gnu +docker exec -it static-builder-gnu /bin/sh +cd /go/src/app/dist/static-php-cli/buildroot/bin +git clone https://github.com/xdebug/xdebug.git && cd xdebug +source scl_source enable devtoolset-10 +../phpize +./configure --with-php-config=/go/src/app/dist/static-php-cli/buildroot/bin/php-config +make +exit +docker cp static-builder-gnu:/go/src/app/dist/static-php-cli/buildroot/bin/xdebug/modules/xdebug.so xdebug-zts.so +docker cp static-builder-gnu:/go/src/app/dist/frankenphp-linux-$(uname -m) ./frankenphp +docker stop static-builder-gnu +docker rm static-builder-gnu +docker rmi gnu-ext +``` + +这将在当前目录中创建 `frankenphp` 和 `xdebug-zts.so`。 +如果你将 `xdebug-zts.so` 移动到你的扩展目录中,添加 `zend_extension=xdebug-zts.so` 到你的 php.ini 并运行 FrankenPHP,它将加载 Xdebug。 diff --git a/docs/cn/worker.md b/docs/cn/worker.md index 2d3424fa..cc0a7be1 100644 --- a/docs/cn/worker.md +++ b/docs/cn/worker.md @@ -1,7 +1,7 @@ # 使用 FrankenPHP Workers -启动应用程序一次并将其保存在内存中。 -FrankenPHP 将在几毫秒内处理传入的请求。 +启动一次应用程序并将其保存在内存中。 +FrankenPHP 将在几毫秒内处理传入请求。 ## 启动 Worker 脚本 @@ -17,24 +17,34 @@ docker run \ dunglas/frankenphp ``` -### 独立二进制 +### 独立二进制文件 -使用 `php-server` 命令的 `--worker` 选项, 执行命令使当前目录的内容使用 worker: +使用 `php-server` 命令的 `--worker` 选项通过 worker 为当前目录的内容提供服务: ```console frankenphp php-server --worker /path/to/your/worker/script.php ``` +如果你的 PHP 应用程序已[嵌入到二进制文件中](embed.md),你可以在应用程序的根目录中添加自定义的 `Caddyfile`。 +它将被自动使用。 + +还可以使用 `--watch` 选项在[文件更改时重启 worker](config.md#watching-for-file-changes)。 +如果 `/path/to/your/app/` 目录或子目录中任何以 `.php` 结尾的文件被修改,以下命令将触发重启: + +```console +frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to/your/app/**/*.php" +``` + ## Symfony Runtime -FrankenPHP 的 worker 模式由 [Symfony Runtime 组件](https://symfony.com/doc/current/components/runtime.html) 支持。 -要在 worker 中启动任何 Symfony 应用程序,请安装 [PHP Runtime](https://github.com/php-runtime/runtime) 的 FrankenPHP 软件包: +FrankenPHP 的 worker 模式由 [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html) 支持。 +要在 worker 中启动任何 Symfony 应用程序,请安装 [PHP Runtime](https://github.com/php-runtime/runtime) 的 FrankenPHP 包: ```console composer require runtime/frankenphp-symfony ``` -通过定义 `APP_RUNTIME` 环境变量来启动你的应用服务器,以使用 FrankenPHP Symfony Runtime: +通过定义 `APP_RUNTIME` 环境变量来使用 FrankenPHP Symfony Runtime 启动你的应用服务器: ```console docker run \ @@ -47,45 +57,50 @@ docker run \ ## Laravel Octane -请参阅 [文档](laravel.md#laravel-octane)。 +请参阅[专门的文档](laravel.md#laravel-octane)。 ## 自定义应用程序 -以下示例演示如何在不依赖第三方库的情况下创建自己的 worker 脚本: +以下示例展示了如何创建自己的 worker 脚本而不依赖第三方库: ```php boot(); -// 循环外的处理程序以获得更好的性能(减少工作量) +// 在循环外的处理器以获得更好的性能(减少工作量) $handler = static function () use ($myApp) { - // 收到请求时调用 - // 超全局变量 php://input + // 当收到请求时调用, + // 超全局变量、php://input 等都会被重置 echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER); }; -for ($nbRequests = 0, $running = true; isset($_SERVER['MAX_REQUESTS']) && ($nbRequests < ((int)$_SERVER['MAX_REQUESTS'])) && $running; ++$nbRequests) { - $running = \frankenphp_handle_request($handler); - // 发送 HTTP 响应后执行某些操作 +$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0); +for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) { + $keepRunning = \frankenphp_handle_request($handler); + + // 在发送 HTTP 响应后做一些事情 $myApp->terminate(); - // 调用垃圾回收器以减少在页面生成过程中触发垃圾回收器的几率 + // 调用垃圾收集器以减少在页面生成过程中触发垃圾收集的可能性 gc_collect_cycles(); + + if (!$keepRunning) break; } -// 结束清理 + +// 清理 $myApp->shutdown(); ``` -然后,启动应用并使用 `FRANKENPHP_CONFIG` 环境变量来配置你的 worker: +然后,启动你的应用程序并使用 `FRANKENPHP_CONFIG` 环境变量配置你的 worker: ```console docker run \ @@ -95,8 +110,8 @@ docker run \ dunglas/frankenphp ``` -默认情况下,每个 CPU 启动一个 worker。 -你还可以配置要启动的 worker 数: +默认情况下,每个 CPU 启动 2 个 worker。 +你也可以配置要启动的 worker 数量: ```console docker run \ @@ -106,9 +121,59 @@ docker run \ dunglas/frankenphp ``` -### 在一定数量的请求后重新启动 Worker +### 在处理一定数量的请求后重启 Worker -由于 PHP 最初不是为长时间运行的进程而设计的,因此仍然有许多库和遗留代码会发生内存泄露。 -在 worker 模式下使用此类代码的解决方法是在处理一定数量的请求后重新启动 worker 程序脚本: +由于 PHP 最初不是为长时间运行的进程而设计的,仍有许多库和传统代码会泄漏内存。 +在 worker 模式下使用此类代码的一个解决方法是在处理一定数量的请求后重启 worker 脚本: -前面的 worker 代码段允许通过设置名为 `MAX_REQUESTS` 的环境变量来配置要处理的最大请求数。 +前面的 worker 代码片段允许通过设置名为 `MAX_REQUESTS` 的环境变量来配置要处理的最大请求数。 + +### 手动重启 Workers + +虽然可以在[文件更改时重启 workers](config.md#watching-for-file-changes),但也可以通过 [Caddy admin API](https://caddyserver.com/docs/api) 优雅地重启所有 workers。如果在你的 [Caddyfile](config.md#caddyfile-config) 中启用了 admin,你可以通过简单的 POST 请求 ping 重启端点,如下所示: + +```console +curl -X POST http://localhost:2019/frankenphp/workers/restart +``` + +### Worker 故障 + +如果 worker 脚本因非零退出代码而崩溃,FrankenPHP 将使用指数退避策略重启它。 +如果 worker 脚本保持运行的时间超过上次退避 × 2, +它将不会惩罚 worker 脚本并再次重启它。 +但是,如果 worker 脚本在短时间内继续以非零退出代码失败 +(例如,脚本中有拼写错误),FrankenPHP 将崩溃并出现错误:`too many consecutive failures`。 + +可以在你的 [Caddyfile](config.md#caddyfile-配置) 中使用 `max_consecutive_failures` 选项配置连续失败的次数: + +```caddyfile +frankenphp { + worker { + # ... + max_consecutive_failures 10 + } +} +``` + +## 超全局变量行为 + +[PHP 超全局变量](https://www.php.net/manual/zh/language.variables.superglobals.php)(`$_SERVER`、`$_ENV`、`$_GET`...) +行为如下: + +- 在第一次调用 `frankenphp_handle_request()` 之前,超全局变量包含绑定到 worker 脚本本身的值 +- 在调用 `frankenphp_handle_request()` 期间和之后,超全局变量包含从处理的 HTTP 请求生成的值,每次调用 `frankenphp_handle_request()` 都会更改超全局变量的值 + +要在回调内访问 worker 脚本的超全局变量,必须复制它们并将副本导入到回调的作用域中: + +```php +