如何配置 Nginx 使其支持对 PHP 程序的请求

Nginx 可以用作 Web 服务器(或称 HTTP 服务器),不过其自身并不支持对 PHP 代码的解释,要想让客户端正常访问 PHP 程序(包括简单脚本和复杂应用),需要利用 Nginx 的“FastCGI 模块”将这些请求发送给 PHP 的 FastCGI 服务器,即 FPM(FastCGI 进程管理器)进行处理,Nginx 仅负责将处理结果响应给客户端。

本文会详细介绍如何通过修改 Nginx 的配置文件,使其能够与 FPM 进行协作,实现客户端对 PHP 程序的请求。

一、安装 Nginx 和 PHP

如果你所使用的操作系统已经安装了 Nginx 和开启了 FPM 功能的 PHP,可略过此步骤。

Nginx 和 PHP 均提供了两种安装方式:通过包管理器(如 CentOS 或 RHEL 的 Yum/DNF、Ubuntu 的 APT 等)安装或通过编译源代码安装。前者非常方便快捷,但是无法自定义功能,且版本可能会滞后;后者比较麻烦,但是能用到最新版本,且有很大的功能定制空间。具体选择哪种方式,可以根据自己的需求选择。

关于如何编译安装最新版 Nginx 可以参考以下文章:

关于如何编译安装最新版 PHP 可以参考以下文章:

Nginx 配置文件的存放位置因不同的操作系统及不同的安装方式而有所不同,本文假设 Nginx 的配置文件存放在如下所示位置:

/usr/local/nginx/conf/nginx.conf

为了让 Nginx 能够与 FPM 进行通信,需要用到 FPM 生成的 “Unix domain socket”(如 /tmp/php-fpm.sock),或“Network socket”(如 127.0.0.1:9000) 。这两种方式可任选其一,具体是什么可查看 FPM 的配置文件 www.conf 中为 listen 指令设定的值。

在开始下面的步骤之前,请确保 Nginx 和 FPM 都已正常运行,且 Nginx 的 nginx 命令也是可用的。

二、接收处理 PHP 请求

假设当前的 Nginx 配置中存在如下所示由 server 块指令设置的一个“虚拟服务器”(virtual server),当请求由 server_name 指令设定的服务器名时(一般是域名,本文以本地主机名 localhost 为例),可以正常访问 Web 文件目录中的静态文件(即 root 指令设定的目录,本文以 /home/www-data/www 为例)。

user www-data;
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include mime.types;
    default_type application/octet-stream;

    server {
        listen 80;
        server_name localhost;

        location / {
            root /home/www-data/www;
            index index.html index.htm;
        }
    }
}

在这样的配置状态下,Nginx 仅支持对静态文件的请求,如果请求的是 PHP 文件,在未经 FPM 解析的情况下,也会被当做普通文件发送给客户端(响应的表现会取决于 Nginx 配置中 default_type 指令设定的值,比如,如果是 application/octet-stream 就会导致 PHP 文件被下载,如果是 text/plain 就会以纯文本显示)。

为了避免 PHP 文件被当做普通文件对待,我们需修改此配置,向 server 块指令中新添加一个如下所示的 location 块指令,把对 PHP 文件的请求过滤出来,并传递给 FPM 进行处理。

location ~ \.php$ {
    root /home/www-data/www;
    fastcgi_pass unix:/tmp/php-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

这里使用了 location 块指令的除“前缀字符串”(prefix string)外的另一种语法,即通过大小写敏感的前置修饰符 ~(波浪号)指定一个“规则表达式”(Regular expression),意思是如果“请求 URI”(Request-URI)[1]除“查询字符串”(query string)的部分[2]与该规则相匹配,就使用相应的块指令处理该请求。

这里设定的规则是 \.php$,即当请求 URI 是以 .php 结尾的,就用新添加的块指令进行处理。注意,在 Nginx 中,带规则表达式的 location 比带前缀字符串的 location 优先级高,因此 Nginx 会先尝试匹配新添加的 location,失败后在尝试匹配之前的 location。

新添加的 location 块指令还包含了四个指令,其中 fastcgi_pass 和 fastcgi_param 属于 Nginx 的“FastCGI 模块”,也是实现本文目的核心指令。下面会按照从上到下的顺序依次解释这些指令的作用。

第 1 个是 root 指令,用来设定存放 PHP 文件的目录,本文示例中, PHP 文件与其它 Web 文件存放在同一目录中,所以其值与之前 location 中的 root 指令指定的目录路径相同。如果你嫌重复,也可以将 root 指令放入 server 块指令,并删掉两个 location 中的 root 指令,让它们继承 server 块指令中的 root 指令。

第 2 个是 fastcgi_pass 指令,用来设定 FastCGI 服务器(即 FPM)的地址,可以是 Unix socket 也可以是 Network socket,也就是本文开头所提到的让 Nginx 与 FPM 通信的两种方式。本文示例使用的是前者,指令的值设定形式为 unix:/tmp/php-fpm.sock,你也可以选用后者。

第 3 个是 fastcgi_param 指令,用来设定要传递给 FastCGI 服务器(即 FPM)的参数(parameter)。本例仅设定了一个名为 SCRIPT_FILENAME 的参数,用来确定所请求 PHP 文件的名称,它的值由两个 Nginx 模块中的两个内置变量拼接而成,其中 $document_root 指的是 root 指令设定的目录路径,$fastcgi_script_name 是请求 URI。比如,当客户端访问 http://localhost/test.php 时,Nginx 就可以可通过路径 /home/www-data/www/test.php 找到该 PHP 文件(如果该文件存在的话)。

第 4 个是 include 指令,它的作用是将 Nginx 预置的名为 fastcgi_params 的文件包含到当前上下文中,该文件包含很多用 fastcgi_param 指令设定的参数,如 QUERY_STRINGREQUEST_METHOD 等,尽可能涵盖了处理请求时可能用到信息。注意,预置文件中也含有之前设定的 SCRIPT_FILENAME 的参数,不过该参数会被将其包含进来的上下文中所设定的同名参数所覆盖。

新的 location 块指令添加完成后,Nginx 配置文件中的 server 块指令应如下所示:

server {
    listen 80;
    server_name localhost;

    location / {
        root /home/www-data/www;
        index index.html index.htm;
    }

    location ~ \.php$ {
        root /home/www-data/www;
        fastcgi_pass unix:/tmp/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

现在就可以创建一个简单的 PHP 脚本来测试新修改的配置是否可用。首先,向存放 Web 文件的目录 /home/www-data/www 中添加一个名为 index.php 的 PHP 文件,其内容如下所示:

<?php echo "Hello,{$_GET['name']}!\n";

然后运行以下命令测试 Nginx 配置文件,确保所做的修改没有错误:

sudo nginx -t

如果没有错误,运行以下命令让 Nginx 重新载入修改后的配置文件:

sudo nginx -s reload

最后,运行如下所示命令向 Nginx 发送一个请求:

curl http://localhost/index.php?name=osfere

如果一切正常,应该可以在终端看到“Hello,osfere!”的字样,这就表示 Nginx 已成功将请求传递给 FPM,也成功收到了 PHP 的处理结果。

三、支持自动索引主页

到目前为止,虽然 Nginx 能够正确处理客户端对 PHP 文件的请求,但是还存在一个问题,那就是请求 URI 必须以 .php 结尾,才能匹配到用来处理 PHP 的 location 块指令,否则就会被当做静态文件或目录进行处理。

比如像下面这样,把之前那个请求地址中的 index.php 去掉:

curl http://localhost/?name=osfere

就会匹配到如下所示的处理静态文件的 location 块指令:

location / {
    root /home/www-data/www;
    index index.html index.htm;
}

由于请求 URI 没有指定具体文件,只有一个表示请求 Web 服务器根目录的斜杠 /,因此 Nginx 会尝试在 root 指令设定的目录中查找 index 指令设定的两个文件 index.html 和 index.htm,如果不存在就会返回 403 响应,表示没有可用的索引文件。

为了让 Nginx 能够索引 index.php,需要将其添加到 index 指令:

index index.php index.html index.htm;

这样,当请求 URI 是一个路径时,Nginx 就会自动查找该路径下的 index.php 文件,一旦找到 index 指令就会产生一个“内部重定向”(internal redirect),进而将对 index.php 的请求交由处理 PHP 的 location 块指令进行处理。

经过以上修改,现在 Nginx 的配置文件内容应如下所示:

user www-data;
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include mime.types;
    default_type application/octet-stream;

    server {
        listen 80;
        server_name localhost;

        location / {
            root /home/www-data/www;
            index index.php index.html index.htm;
        }

        location ~ \.php$ {
            root /home/www-data/www;
            fastcgi_pass unix:/tmp/php-fpm.sock;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
    }
}

在该配置状态下,如果请求是以 .php 结尾的,会交给第二个 location 块指令处理,否则会交给第一个 location 块指令处理,如果请求文件存在就直接发送,否则会尝试依次查找名为 index.php、index.html 和 index.htm 的文件,如果能找到 index.php 则内部重定向给第二个 location 块指令进行处理,否则继续查找 index.html 或 index.htm,有则内部重定向给当前的 location 块指令处理,否则返回 403 或 404 响应(如果请求 URI 以斜杠 / 结尾则为前者,否则为后者)。

保存修改后的 Nginx 配置文件,运行以下命令让 Nginx 重新载入配置:

sudo nginx -s reload

现在再次向 Nginx 发送同样的请求,Nginx 就能正常响应了:

curl http://localhost/?name=osfere

在一个 PHP 文件对应一个页面的情况下,当前的配置文件已完全能够满足需求,但是如果你所使用 PHP 程序采用了单点入口策略,则还需要继续对配置做相应的修改才能让其正常运行。

四、支持单点入口应用

许多现代 PHP 框架(如 Symfony、Laravel 等)都采用了“单点入口”(single point of entry)策略,即客户端对应用的请求必须通过单一入口(如 index.php)到达应用,框架会通过“路由”(routing)机制匹配客户端的请求,并运行应用的相应功能。

为方便说明,这里将 index.php 的内容替换成如下所示代码:

<?php

$route = '/user';
$request_uri = $_SERVER['REQUEST_URI'];

if (str_starts_with($request_uri, $route)) {
    if (isset($_GET['name'])) {
        $name = $_GET['name'] ?: 'Stranger';
        echo "Hello, {$name}!"; exit;
    }
}

http_response_code(404);
echo '404 Not Found';

这段简单的 PHP 脚本模拟了采用单点入口策略的 PHP 应用,它只定义了一条路由 /user,如果请求能够到达此应用,并且请求 URI 与该路由相匹配,就会显示一个带有用户名的欢迎语,否则就显示 404 页面。

但是,在当前 Nginx 配置状态下,如果做出如下所示的请求:

curl http://localhost/user?name=osfere

是无法到达此应用的,因为 Nginx 不知道应该如何处理这个请求,它只知道请求 URI 既不是以 .php 结尾,root 指令设定的目录中也不存在名为 user 的文件,最终会返回 404 响应。

要解决此问题,应确保客户端的请求在所有条件均不满足的情况下,也能够通过应用的单一入口 index.php 到达应用,以便应用判断当前请求是否与设定的路由相匹配,进而做出正确的响应。

这就需要用 try_files 的指令,该指令属于 Nginx 的“HTTP 核心模块”,可设定用空格分隔的多个文件或目录作为参数,最后一个参数是备用的请求 URI。Nginx 会按先后顺序依次检查这些文件或目录是否存在,如果存在就在当前上下文中对其做相应处理,如果都不存在则会内部重定向到最后一个参数(注意,最后一个参数必须是一个有效的请求 URI,否则会产生循环内部重定向到当前的 location,最终导致 500 响应)。

要想正常访问应用,就需要向第一个 location 块指令中添加如下所示的 try_files 指令:

try_files $uri $uri/ /index.php?$args;

在该指令中,还用到了 Nginx 的“HTTP 核心模块”中的两个内置变量:$uri$args。前者指的是当前请求 URI 中不带参数的部分,即 /user,后者指的是请求 URI 中仅参数部分,即 name=osfere

下面是将 try_files 指令放入 location 块指令中的样子:

location / {
    root /home/www-data/www;
    index index.php index.html index.htm;
    try_files $uri $uri/ /index.php?$args;
}

现在 location 块指令的含义是,先在 root 指令设定的目录中查找名为 $uri 的文件,如果存在响应。如果没找到就在 $uri 后添加一个斜杠,当做目录进行索引,依次查找 index 指令设定的文件,如果存在就内部重定向到当前 location 进行响应。如果没有索引到任何文件,则内部重定向到最后一个参数设定的请求 URI,即 /index.php?args。这样,当请求的静态文件不存在,或请求目录索引不到任何文件时,请求就会自动进入应用的单点入口 index.php。

下面是最终的 Nginx 配置文件内容,该配置同时适用于单页面 PHP 文件,以及采用了单点入口策略的 PHP 应用,如 WordPress、Symfony、Laravel 等。

user www-data;
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include mime.types;
    default_type application/octet-stream;

    server {
        listen 80;
        server_name localhost;

        location / {
            root /home/www-data/www;
            index index.php index.html index.htm;
            try_files $uri $uri/ /index.php?$args;
        }

        location ~ \.php$ {
            root /home/www-data/www;
            fastcgi_pass unix:/tmp/php-fpm.sock;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
    }
}

保存修改后的配置文件,运行以下命令让 Nginx 重新载入配置:

sudo nginx -s reload

现在,再次请求之前那个 URL,应用就能正常工作了:

curl http://localhost/user?name=osfere

由于 Nginx 找不到名为 user 的文件或目录,因此会内部重定向到最后一个请求 URI,也就是 /index.php?name=osfere。内部重定向后的请求会匹配到处理 PHP 程序的 location 块指令,这样就通过单一入口进入了应用,应用会用路由 /user 与原始请求 URI,即 /user?name=osfere 进行对比,如果后者是以 /user 开头的,则取出查询字符串中的 name 的值 osfere,最终组合成欢迎语“Hello, osfere!”。

注意,查询字符串以及原始请求 URI 是 Nginx 的 fastcgi_param 指令通过 QUERY_STRING 和 REQUEST_URI 这两个参数传递给应用的。


[1] 注意这里术语 Request-URI 是由 HTTP 规范(RFC 2616,§5.1.2)定义的。
[2] Nginx 官方文档显示,location 指令仅尝试匹配请求 URI 除查询字符串的部分。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注