最近在用PHP的Phalcon框架做一个网站。遇到一些问题,干脆研究个透彻,免得心里面不踏实 – 我是喜欢刨根问底,凡事都要知道个为什么的人。

安装

作为多年C++开发出身,对脚本性能的追求刻在了骨子里。PHP当然要装PHP7,但Phalcon的安装文档说明的并不准确,没有针对PHP7说明,一些步骤并不需要。

从源码安装Phalcon是很方便的,也是最通用的办法。

1、安装PHP7,要选择PHP 7.0.x,不要用PHP 7.1.x。因为Phalcon紧密的跟PHP内核结合,对版本的依赖性很强,没有针对测试过的内核安装,大概率得到:Segmentation fault。这是使用这种框架必然要付出的代价,对Phalcon不满的声音,很多来自于此。目前可以选PHP 7.0.14,建议从源码安装,一键安装包很多。

2、 选择phalcon 3.0.x分支:git clone -b 3.0.x https://github.com/phalcon/cphalcon.git

这个版本是正式版,针对PHP 7.0.x测试过的。不需要安装其它PHP5的头文件,的比如php5-dev。php5-dev php5-mysql 这类,都不要安装,版本也和PHP7对不上,从源码安装了PHP7,这些头文件都有了。

3、进入phalcon代码目录:cphalcon/build,执行./install。这个install是个shell脚本,它会自动的运行phpize,设置一些编译参数,查看你的PHP的版本,选择合适的代码编译。要从这里开始。提前安装编译工具包是必须的,但前面用源码安装PHP7,GCC这些已经有了。编译成功后,要设置php.ini,最后面增加:

[phalcon]
extension=phalcon.so

4、使用php -m测试一下,看看phalcon模块是否安装成功,如果有错误,基本都是Segmentation fault.

Nginx的配置

官方文档有错误。按照它那个设置,运行得到的是:Access denied.

问题在哪里?谷歌了一番,大概明白了,不过心里不踏实,干脆直接查源码,一探究竟。这是开源的巨大优点:代码前面藏不住秘密,只要你肯去看,能看明白。对于熟悉C/C++开发的人来说,这点更是不成问题,老本行,代码风格熟悉的很。

理解这个,要首先阐述一下Nginx+PHP的运行模式。Nginx是Web服务器,它不处理脚本的解析,可以说一无所知。那如何运行PHP脚本?当然要PHP自己的解析器。这就需要一个纽带,把它们连接起来 – CGI协议。这个其实符合UNIX程序精神:程序只做一件事,要做好;做专业的程序,不做大而全的程序。程序之间用各种机制组合起来。FastCGI是对传统CGI模式的改进,程序可以常驻内存,使用进程池,避免根据请求反复创建进程。PHP-FPM就实现了这个模式,已经整合进入核心,编译后就有这个程序了。Nginx的配置文件有相关的指令可以把两者连接起来,PHP脚本就能顺利解析运行了。

其实对于其它脚本道理也是一样,比如Python。Nginx把外部请求转换成了CGI协议,用规定协议跟脚本通信,然后返回脚本运行结果给外部请求,一般是浏览器。相当于一个网关管道,有进有出。

具体说是这样:Nginx收到请求,比如 index.php。它发现不是能处理的静态文件,就转给配置里面写好的处理器,是这条指令:fastcgi_pass unix:/dev/shm/php-cgi.sock;。

光有这个还不行,还需要一些相关的信息,比如脚本具体位置,参数等等。这个是fastcgi_param参数干的事情。一般Nginx安装包,配置文件目录都有一个fastcgi_params文件,还有fastcgi.conf,两者只差了一行代码。里面包装的就是Nginx和PHP通信的参数信息。

路由模式:

可是因为一些框架的设计需求,还需要其它的配置指令。所有的web框架,都有个路由设计问题,大体分成几种:

  • 原生模式:index.php?q=index&a=run… Url有点丑陋
  • rewrite模式:index/run/?id=1 需要开启rewrite模块
  • pathinfo模式:index/run/id/1 用path传递操作的模块、参数信息
  • html模式:index-run.htm?uid=xxx 需要开启rewrite

如果你把网站想象成一个App,那么路由其实就是一个入口操作问题。它把网站内所有的请求,都集中导入到一个地方判断、分配处理器。这个模式被无数框架使用,优点多多。但怎么选择路由模式,各个框架不一样。Phalcon/yaf等框架,选择了pathinfo模式。这个模式的Url比较优雅,但是需要额外的配置,框架需要知道传进来的路径信息,分解后才能处理它。

讨论了半天,那个“Access denied.”咋处理?哪里错了?有技术文章说,解决办法是:打开PHP的配置参数:php.ini -> cgi.fix_pathinfo=1,就解决了。试验了一下,确实立刻OK。但是此法绝不可取,因为它留下了巨大的安全漏洞。

For instance, if a request is made for /forum/avatar/1232.jpg/file.php which does not exist but if /forum/avatar/1232.jpgdoes, the PHP interpreter will process /forum/avatar/1232.jpg instead. If this contains embedded PHP code, this code will be executed accordingly.

PHP的官方配置文档说明里面也提到:

cgi.fix_pathinfo boolean
Provides real PATH_INFO/ PATH_TRANSLATED support for CGI. PHP’s previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok what PATH_INFO is. For more information on PATH_INFO, see the CGI specs. Setting this to 1 will cause PHP CGI to fix its paths to conform to the spec. A setting of zero causes PHP to behave as before. It is turned on by default. You should fix your scripts to use SCRIPT_FILENAME rather than PATH_TRANSLATED.

更简单的办法:是删除这行配置:

fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;

就解决了。它是不需要的,可以看源码:

源码探究:
php -> fpm_main.c -> main()
/* library is already initialized, now init our request */
request = fpm_init_request(fcgi_fd);
zend_first_try {
while (EXPECTED(fcgi_accept_request(request) >= 0)) {
char *primary_script = NULL;
request_body_fd = -1;
SG(server_context) = (void *) request;
//call init
init_request_info();
fpm_request_info();
……………………….
static void init_request_info(void)
{
fcgi_request *request = (fcgi_request*) SG(server_context);
char *env_script_filename = FCGI_GETENV(request, “SCRIPT_FILENAME”);
char *env_path_translated = FCGI_GETENV(request, “PATH_TRANSLATED”);
//这里写的很明白:默认要用SCRIPT_FILENAME的值,而不是PATH_TRANSLATED
//所以PATH_TRANSLATED可以废弃了,保留应该只是兼容性需求。如果设置了
//PATH_TRANSLATED,后面因为用这个值计算,反而导致了路径错误,变成
//Access denied.
char *script_path_translated = env_script_filename;
char *ini;
int apache_was_here = 0;
/* some broken servers do not have script_filename or argv0
* an example, IIS configured in some ways. then they do more
* broken stuff and set path_translated to the cgi script location */
if (!script_path_translated && env_path_translated) {
script_path_translated = env_path_translated;
}
……………………….

一个Nginx配置:


server {
listen 80;
listen 443 ssl http2;
server_name phpdev.com www.phpdev.com;
access_log /data/wwwlogs/phpdev.com_nginx.log combined;
index index.html index.htm index.php;
root /data/wwwroot/phpdev.com;
location / {
try_files $uri $uri/ /index.php?_url=$uri&$args;
}
location ~ \.php$ {
fastcgi_pass unix:/dev/shm/php-cgi.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ {
expires 30d;
access_log off;
}
location ~ .*\.(js|css)?$ {
expires 7d;
access_log off;
}
location ~ /\.ht {
deny all;
}
}

fastcgi_split_path_info ^(.+\.php)(/.+)$;

这行是关键,把path_info信息分解。

一些参考资料:

这篇文章,对fastcgi参数阐述的非常清楚。
https://www.digitalocean.com/community/tutorials/understanding-and-implementing-fastcgi-proxying-in-nginx
Nginx官方PHP配置wiki:
https://www.nginx.com/resources/wiki/start/topics/examples/phpfcgi/
不错的nginx配置参考:
https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/