干了好几年PHP,我经常被新同事问一个看起来很基础的问题:你懂执行流程吗?php -v能跑,phpinfo()也正常,但你问他“从请求进来到页面返回,中间到底走过哪几步”,他就开始含糊了。其实这不是基础不基础的问题,这是对PHP构成没有完整认知。这次我想聊的东西归结起来就三块:PHP由哪些部分组成、一次请求到底怎么执行、以及核心语法在Zend引擎里是怎么被处理的。适合两类人看:一类是刚把语法学完、想往底层走一步的同学,另一类是常年写业务、被各种玄学BUG和反序列化漏洞坑过、想补点内功的开发者。
1. 先把“PHP”拆开:它不只是一门语言
1.1 四个组成层:SAPI、核心函数库、Zend引擎、扩展
很多人一提到PHP就觉得是一套解释器,实际上PHP运行时是一个分层的系统。从宏观上看,整个运行环境可以拆成四层:
- SAPI层:Server API,也就是“服务器应用编程接口”。CLI、FPM、Apache的mod_php、CGI、甚至嵌入式
embed都算SAPI。它是PHP与外部世界的桥,决定了PHP以什么身份运行。 - 核心函数库层:包括字符串处理、数组函数、文件操作、流、输出控制、网络socket等基础能力,这层是绝大多数业务代码直接调用的东西。
- Zend引擎层:也就是通常说的
Zend/目录下的那一大坨C代码。负责词法分析、语法分析、编译生成opcode、执行opcode、内存管理、垃圾回收。后面重点聊的就是它。 - 扩展层:
pdo_mysql、redis、mbstring、gd、curl、swoole,以及商业加密扩展SG11这类都在这层。扩展通过PHP提供的API向脚本层暴露函数和类。
这四个层的关系可以理解为:SAPI把请求送进来,核心函数库负责提供“干活的工具”,Zend引擎负责“翻译并执行你写的代码”,扩展负责“接入外部世界”。平时用php -m能看到当前加载了哪些扩展,用php --ini能确认加载的是哪个配置文件。排查“函数不存在”这类问题,第一步通常就是看扩展有没有加载或者版本吻不吻合。
1.2 为什么说“PHP源码部署”和“Python源码部署”不是一回事
网上经常有人问php源码和py源码的区别,这个问题其实暴露了一个误解:以为PHP是纯解释执行,Python是半编译。两者都不是纯解释。PHP脚本要经过词法分析、语法分析、编译成opcode,再由Zend虚拟机执行;Python则是编译成字节码,再由Python虚拟机执行。区别在于中间产物怎么处理:Python的.pyc会落盘,Java的.class会落盘,PHP的opcode默认只存在内存里,请求结束就没了,这就是为什么OPcache对PHP这么重要。
还有一点很关键:常规PHP模式是“请求驱动”的。每个请求结束后,这个请求创建的变量、对象、资源都会销毁,下个请求从头开始。Python脚本如果用常驻进程跑,内存里的对象是跨请求保留的。这个差异直接影响你对“变量生命周期”和“内存泄漏”的理解。后面讲Zend引擎的垃圾回收时,这个差异会很自然地浮出来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一行代码背后:PHP的解析与执行流程
2.1 从字符流到token:词法分析做了什么
假设你有个非常简单的文件test.php,里面写$a = 1 + 2; echo $a;。你要理解执行流程,第一步就是看它如何被拆成token。词法分析器(PHP里是zend_language_scanner)把整个文件当成一个字符流,按照规则切成一个个有意义的单元,这些单元叫token。
我经常用token_get_all()让人直观感受这件事:
php复制var_dump(token_get_all('<?php $a = 1 + 2; echo $a;'));
输出里能看到T_VARIABLE表示变量$a,T_LNUMBER表示整数1和2,运算符=、+、;就是它本身,T_ECHO表示echo关键字。词法分析阶段不关心语法对不对,比如你写$a = = 1,词法阶段照样能拆出一堆token,直到语法分析阶段才报错。
这也是为什么很多IDE的报错会有“延迟感”:编辑器底层先走一遍词法再走一遍语法,两层都通过才会标绿。你写的PHP源码最终会被处理成token流,交给下一步。
2.2 parser与AST:语法分析怎么把token变成树
语法分析器(PHP里由Bison生成)拿到token流后,会根据PHP的语法规则把它们组装成一棵抽象语法树,也就是AST。PHP 7之前,语法分析结果是直接生成opcode的;PHP 7开始引入了AST层,先由parser生成AST,再通过compiler把AST编译成opcodes。
AST为什么重要?因为树形结构比线性token流更适合做优化。拿$a = 1 + 2 * 3来说,*的优先级高于+,在AST里1 + (2 * 3)的树形关系一目了然。引入AST之后,PHP编译器可以在这层做更多分析和优化,也方便开发者通过ext-ast扩展看到自己代码的语法树。
这一层也是绝大多数“语法错误”的爆发地。比如少写分号、括号不匹配、函数调用参数对不上,都是parser阶段直接抛致命错误的。线上看到PHP Parse error: syntax error, unexpected ...,基本可以确定是这里出问题。
2.3 opcode与Zend虚拟机:真正干活的是谁
AST编译之后变成一组有序的opline,每条opline对应一个opcode和若干操作数,所有opline合起来叫op_array。你可以把opcode理解成PHP的中间汇编,Zend引擎执行的就是这玩意。
想亲眼看到opcode,有两个办法:装PECL的vld扩展,或者用PHP自带的phpdbg。
bash复制php -d vld.active=1 -d vld.execute=0 test.php
输出大概长这样(不同PHP版本指令细节会有差异):
code复制filename: /tmp/test.php
compi
