PHP进化史:PHP5 一个全新的时代
2004年7月,PHP5正式版本的发布,标志着一个全新的PHP时代的到来。它的核心是第二代Zend引擎,并引入了对全新的PECL模块的支持。
2004年7月,PHP5正式版本的发布,标志着一个全新的PHP时代的到来。它的核心是第二代Zend引擎,并引入了对全新的PECL模块的支持。
PHP4.0整个脚本程序的核心大幅更动,让程序的执行速度,满足更快的要求。在最佳化之后的效率,已较传统CGI或者ASP等程序有更好的表现。而且还有更强的新功能、更丰富的函数库。无论您接不接受,PHP 都将在 Web CGI 的领域上,掀起巅覆性的革命。对于一位专业的Web Master 而言,它将也是必修课程之一。
PHP4.0这些优异的性能是PHP 脚本引擎重新设计产生的结果:引擎由 AndiGutmans 和 Zeev Suraski从底层全面重写。PHP4.0 脚本引擎 ——Zend 引擎,使用了一种更有效的编译——执行方式,而不是PHP 3.0 采用的执行 ——当解析时模型。
在 PHP4.0 版本,用 cloc 统计代码如下。

这个时候的代码量已经接近 20W 行。
上面也提到了PHP4.0 最大的变化 Zend 引擎。
PHP执行这段代码会经过如下4个步骤(确切的来说,应该是PHP的语言引擎Zend)
1997年中PHP的发展也已经有了一些变化,他已经从Rasmus的宠物项目变成了更加有组织的团体项目。语法分析引擎也由Zeev Suraski 和 Andi Gutmans进行了重新改写,这个引擎构成了PHP3的基础。PHP/FI中的大部分通用代码都经过改写后引入了PHP3中。
目录结构上,PHP 越来越工程化。
hashtable 出现了,代替了原来的 VarTree
typedef struct hashtable {
uint nTableSize;
uint nHashSizeIndex;
uint nNumOfElements;
uint nNextFreeElement;
uint(*pHashFunction) (char *arKey, uint nKeyLength);
Bucket *pInternalPointer; /* Used for element traversal */
Bucket *pListHead;
Bucket *pListTail;
Bucket **arBuckets;
void (*pDestructor) (void *pData);
unsigned char persistent;
} HashTable;在 PHP 中 HashTable 中非常重要,在 PHP/FI 中函数被保存在一个链表中,但是链表的时间复杂度是 O(N),在 PHP3 中,由于出现了 HashTable,其查询时间复杂度是 O(1),这样用 HashTable 代替链表存储函数也就成了必然。
PHPAPI int _php3_hash_add_or_update(HashTable *ht, char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag) Rasmus Lerdorf 还做一个叫做 Form Interpreter 的项目,可以轻松地将SQL查询嵌入到网页中。它基本上是另一个CGI包装器,可以解析SQL查询,并能在这些查询的基础上轻松创建表单和表格,Rasmus Lerdorf 最终将两个项目合并到了一起,将其合并成为了一个程序,也就是 PHP/FI。现在它已经发展到了嵌入 HTML 文件的简单编程语言的程度,所以代码量飞起,直接涨了接近 20 多倍。
这个阶段的 PHP/FI 此时已经具备了 PHP 的雏形,PHP 完成从“个人主页工具”到 “个人主页建设工具包”。

PHP脚本语言的语法在很多方面与C语言相似。它支持变量、数组、函数调用、不同的变量类型和大多数编写复杂的cgi程序时可能需要的东西。也就是说 PHP 编程语言正式出现了!
在PHP/FI 中引入了 yacc 和 lex 进行词法分析和语法解析,其原理其实还是通过解析 html 中的标记位,然后通过词法解析和语法解析成为C 语言代码进行执行,其核心代码在 lex.c和 parse.c 两个文件中。
我们先来看一下 PHP/FI 的使用,假设你有一个表单。
<FORM ACTION="/cgi-bin/php.cgi/~userid/display.html" METHOD=POST>。
<INPUT TYPE="text" name="name">
<INPUT TYPE="text" name="age"> <INPUT TYPE="text" name="age" >。
<INPUT TYPE="提交">
</FORM>然后你的display.html文件可以包含这样的内容。
<?
if($age>50);
echo "Hi $name, you are ancient!<p>";
elseif($age>30);
echo "Hi $name, you are very old!<p>";
else;
echo "Hi $name.";
endif;
>就是这么简单! PHP/FI为表单中的每个输入字段自动创建一个变量。然后你可以在ACTION URL文件中使用这些变量。
上面的这段例子是来自于 PHP/FI 的文档,其文档地址在压缩包中也存在,目录是 doc。

这个时候的 PHP/FI 还提供了 mysql 数据库:

<?
$name = "bob";
$result = mysql($database,"select * from table where firstname='$name'");
$num = mysql_numrows($result);
echo "$num records found!<p>";
$i=0;
while($i<$num);
echo mysql_result($result,$i,"lcase(fullname)");
echo "<br>";
echo mysql_result($result,$i,"address");
echo "<br>";
$i++;
endwhile;
>其实到了这一步,我们的 PHP/FI 其实就已经可以完成目前我们的日常业务开发了,也就是 CURD。
同时我们可以通过 changelog 就可以看出,越来越多的人参与到了 PHP 的研发工作当中,也正是因此,PHP 逐渐壮大到了今天这个地步。

目前 PHP/FI 的文档还可以访问 https://www.php.net/manual/phpfi2.php ,有兴趣的朋友可以去看一下。
PHP 的本质其实还是 C,这也是大多数脚本语言的本质,我们可以把脚本语言的解释器理解为就是一个特殊的程序,解释器的将其中的各种语法解析、转换成可以执行的 C 代码而已。如果这么理解,那么对于脚本语言的理解就很容易了。
那么我们在定义一个变量,自定义一个函数的时候,其实解释器需要将这些变量、函数存储起来,然后在需要使用的地方进行调用。
接下来,我们就来看一下,PHP/FI中的自定义函数、变量是如何存储的。
在了解自定义函数的存储之前,我们先来看一下内置函数是如何进行处理的,在 PHP/FI 中为我们提供了大量的内置函数。
所有的内置函数都会被存储在一个变量名叫做 cmd_table 的 hashtable 中。
typedef struct _cmd_table_t {
char *cmd;
unsigned int token;
void (*fnc)(void);
} cmd_table_t;


支持三种类型的变量。长整数、双精度浮点和字符串。
#define DNUMBER 258
#define LNUMBER 259
#define STRING 260PHP 通过类型推断来判断变量具体的数据类型。
/*
* Determines if 'str' is an integer, real number or a string
*
* Note that leading zeroes automatically force a STRING type
*/
int CheckType(char *str) {
char *s;
int type=LNUMBER;
s = str;
if(*s=='0' && *(s+1) && *(s+1)!='.') return(STRING);
if(*s=='+' || *s=='-' || (*s>='0' && *s <='9') || *s=='.' ) {
if(*s=='.') type=DNUMBER;
s++;
while(*s) {
if(*s>='0' && *s<='9') {
s++;
continue;
}
else if(*s=='.' && type==LNUMBER) { type=DNUMBER; s++; continue; }
else return(STRING);
}
} else return(STRING);
return(type);
} /* CheckType *//* Expression Stack */
typedef struct Stack {
short type;
char *strval;
long intval;
double douval;
VarTree *var;
struct Stack *next;
int flag;
} Stack;变量的存储,在这个版本中的 PHP 存储变量使用的并不是 HashTable,而是使用的树。这说明 PHP 的设计也是在不断优化调整的过程。
/* Variable Tree */
typedef struct VarTree {
short type;
int count;
char *name;
char *strval;
char *iname;
long intval;
double douval;
int flag;
int scope; /* 0=local to frame, 4=global, 8=static to frame */
struct VarTree *left;
struct VarTree *right;
struct VarTree *next;
struct VarTree *prev;
struct VarTree *lacc;
struct VarTree *lastnode;
int deleted;
int allocated;
} VarTree;这个时候的变量的值全部都是存在 tree 中的,根据 type 来判断变量的类型。
socpe 实现了变量的作用域。
参考
1.0.8 是我们在官网上能够下载到的最古老的版本,虽然这个时候也叫 PHP,但是和现在的 PHP 意思是完全不同的,下面这张图是在源代码中的截图。

PHP Tools - Personal Home Page Tools 个人网站工具集,严格意义上来说,这个时候的 PHP 只是一个 CGI 的工具集,它包含了一个非常简单的语法分析引擎,只能理解一些指定的宏和一些Home Page后台的常见功能,如留言本,计数器和一些其他的素材。

Rasmus Lerdorf,就是这个家伙,在 1995 年仅用了一个下午就完成了 PHP 的第一个版本,对我们的生活其实产生了深远的影响。在 2004 年 PHP 更是成为了年度语言,并且在其后很长的一段时间内保持着前五,虽然近些年来 PHP 一直在被唱衰,甚至在国内大厂不断地用 Go 代替 PHP 的环境下,在 2023 年仍然没有跌出前十。
PHP 源代码下载,历史的归档源代码我们可以到 https://museum.php.net/ 这里进行下载。

我们用 cloc 分析一下源代码发现,这个时候的 'PHP' 其实只有两千多行代码,其中真正用 C 语言实现的代码共计是 2455 行,等到了php2.0.1 的时候, C 语言的代码量就已经打到了进阶 58957 行。

和现在的 PHP 代码行数完全不是一个量级的,这是因为在当年大多数都是静态页面,而开发 PHP 的目的就是为了统计网站的访问人数。
这个时候的 PHP 是如何执行的呢,又或者 CGI 程序是怎么回事?
这里不会跟朋友们扯太多空洞洞的理论,因为理论知识很容易就可以获取到,随便看几本书就可以了,或者去百度百科,都会有非常详细的介绍。
在上世纪 90 年代互联网刚刚起步,那个时候大家都是在用 html 搭建一个个人简历或者是公司简介,这个时代就是 web1.0 时代,很快 CGI 出现了,拜 CGI 之赐,网站不再只有固定不变的图形和文字,借由程序动态产生的网页可以让网站好象『活』了起来。小从简单的网页计数器,留言版,大至处理众多资料的搜寻引擎,可做线上实时交易的电子商务、网络下单等。CGI 简单、开放、跨平台、与程序语言独立的特性,使得编写网站应用程序变得很容易。
按照这个原理,其实任何语言都可以编写 cgi 程序,下面我们用 shell 来编写一个打印 get 参数的 echo 服务。
#!/bin/bash
echo 'Status: 200 OK'
echo 'Content-Type: text/html; charset=utf-8'
echo 'Cache-Control: no-cache'
echo 'Pragma: no-cache'
echo
echo '<html>'
echo '<head>'
echo '<h1>echo query:</h1>'
echo '<p>'
echo $QUERY_STRING
echo '</p>'
echo '</body>'
echo '</html>'
echo

这个时候 PHP 提供了四个 cgi 程序:phpl.cgi, phpf.cgi, phplmon.cgi, phplview.cgi。
其实这个时候的 PHP 就已经在简化 HTML 开发了,其中提供了一些内置函数可以帮我们进行模板替换。
<ul>
<li>$today = <!--$today--></li>
<li>$version = <!--$version--></li>
<li>$_GET['name'] = <!--$name--></li>
<li>system load:<pre><!--! w|head -n1 --></pre></li>
</ul>
<br>This page has been accessed a total of <a href="phplmon.cgi"><b>3</b></a> times now! <b>3</b> times today.<br>Page was last updated on Dec 22, 2021 at 7:43.</center></font>

我们可以看到 <!--$today--> <!--$version--> 都已经被 phpl.cgi 程序给替换掉了。
这就是这个版本的 PHP 主要执行流程。我们来看一下代码目录
.
├── License
├── README
├── build
│ ├── phpf.cgi
│ ├── phpl.cgi
│ ├── phplmon.cgi
│ └── phplview.cgi
├── common.c
├── common.h
├── config.h
├── error.c
├── html_common.h
├── phpf.c
├── phpl.c
├── phplmon.c
├── phplview.c
├── post.c
├── post.h
├── subvar.c
├── version.h
├── wm.c
└── wm.h在这里的 build 文件其实是不存在的,这是为了方便编译我自己就修改了 Makefile 文件。
我们先来看配置文件 config.h :
define ROOTDIR "/usr/somewhere/user_id/public_html"
define HTML_DIR "html"
#define LOGDIR "logs/"
#define ACCDIR "logs/"
#define NOACCESS "NoAccess.html"
这个时候的配置文件可以说是相当简洁了,只有四个参数,所以根本不需要独立的配置文件,我们只需要在编译之前进行修改就可以了。
在 phpl.c 中定义了两个宏。
#ifndef STARTSEP
#define STARTSEP "<!--"
#endif
#ifndef ENDSEP
#define ENDSEP "-->"这两宏的作用其实就是代表着模板替换的开始和结尾。
在这个时候,其实 PHP 有点类似于 Smarty 模板引擎,在 PHP 中预设了很多变量,当加载 HTML 的时候,如果发现 <!-- --> 模板标记,就会将其中的内容替换成已经赋值的变量

我们可以看到截图中代码替换,其实就是检测到 $total 就替换成已经实现准备好的变量。
<!-- --> 中的内容在 651 行中有 $today,我们以此举例。
static int todays_cnt;在 phpl.c的最上方定义了静态变量 todays_cnt,其实该记录存储的就是今日的访问记录。
if(fgets(cntbuf,127,fp)) {
s=strchr(cntbuf,' ');
if(s) *s='\0';
cnt=atoi(cntbuf);
if(s) todays_cnt=atoi(s+1);
} else {
cnt=0;
todays_cnt=0;
}
在 phpl.c 中第 88 行定义了一个全局静态变量, pdtop 其结构定义在 post.h 中。
typedef struct _postdata {
char var[32];
char val[128];
struct _postdata *next;
} postdata;这是一个链表,第一个 char 存储的事 post 的 key,第二个 char 存储的是值,在这里我们可以看出,在这个时候 PHP 最大只能接受 128 个字符。
在 cgi 协议中 http 请求中的数据是通过环境变量交给 cgi 程序的。

在我们申请内存后,必须对其进行释放,否则就会造成内存泄露,当 php 执行完成后会对已经申请过的内存地址进行释放。

其实操作非常简单,就是判断第一个节点是否为空,如果不为空则证明有 post 的数据,然后开始遍历链表,释放掉所有申请的内存地址。
参考