PHP7变量的内部实现(一)

注:
1. 转载自这,方便留存查看
2. 转载文章翻译自Nikita的文章,欢迎指正查看原文
3. 我准备翻译第二部分


受篇幅限制,这篇文章将分为两个部分。本部分会讲解PHP5和PHP7在zval结构体的差异,同时也会讨论引用的实现。第二部分会深入探究一些数据类型如string和对象的实现。

PHP5中的zval
PHP5中zval结构体的定义如下:

typedef struct _zval_struct {
    zvalue_value value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
} zval;

可以看到,zval由value、type和一些额外的__gc信息组成。__gc与垃圾回收相关,我们稍后讨论。value是一个共用体,可以存储一个zval各种可能的值。

typedef union _zvalue_value {
    long lval;                 // For booleans, integers and resources
    double dval;               // For floating point numbers
    struct {                   // For strings
        char *val;
        int len;
    } str;
    HashTable *ht;             // For arrays
    zend_object_value obj;     // For objects
    zend_ast *ast;             // For constant expressions
} zvalue_value;

C语言中,共用体的尺寸与它最大的成员尺寸相同,在某一时刻只能有一个成员处于活动状态。共用体所有的成员都存储在相同的内存,根据你访问的成员不同,内容会被解释成不同的类型。以上面的共用体为例,如果访问lval,值将被解释为一个有符号整型;而访问dval将被解释成双精度浮点型。以此类推。

为了弄清结构体中哪个成员处于活动状态,zval会存储一个整型type来标识具体的数据类型。

#define IS_NULL     0      /* Doesn't use value */
#define IS_LONG     1      /* Uses lval */
#define IS_DOUBLE   2      /* Uses dval */
#define IS_BOOL     3      /* Uses lval with values 0 and 1 */
#define IS_ARRAY    4      /* Uses ht */
#define IS_OBJECT   5      /* Uses obj */
#define IS_STRING   6      /* Uses str */
#define IS_RESOURCE 7      /* Uses lval, which is the resource ID */

/* Special types used for late-binding of constants */
#define IS_CONSTANT 8
#define IS_CONSTANT_AST 9

PHP5中的引用计数
除少数例外,在PHP5中zval都是分配在堆内存的,PHP需要通过某种方式跟踪哪些zval在被使用,哪些应该被释放。为达到这个目的,引用计数被使用。引用计数即在结构体中用refcount__gc成员来记录该结构体被“引用”了多少次。例如,在$a = $b = 42中,42被两个变量引用,所以它的引用计数为2。如果引用计数变成0,则意味着该值没被使用,可以被释放。

需要注意的是引用计数的“引用”(即一个值被引用的次数)与“PHP引用”($a=&$b)毫无关系。在接下来的内容里,我会始终使用“引用”和“PHP引用”这两个术语来释疑这两个概念。就当前来说,我们先把“PHP引用”放在一边。

与引用计数密切相关的一个概念是“写时复制”(copy on write):zval只能在其内容未被修改的时候才能在多个变量间共享。要实现修改,zval必选被复制(分离),而改动只能在复制出的zval上进行。

以下例子展示了写时复制和zval销毁。

$a = 42;   // $a         -> zval_1(type=IS_LONG, value=42, refcount=1)
$b = $a;   // $a, $b     -> zval_1(type=IS_LONG, value=42, refcount=2)
$c = $b;   // $a, $b, $c -> zval_1(type=IS_LONG, value=42, refcount=3)

// 下一行操作会导致zval分离
$a += 1;   // $b, $c -> zval_1(type=IS_LONG, value=42, refcount=2)
           // $a     -> zval_2(type=IS_LONG, value=43, refcount=1)

unset($b); // $c -> zval_1(type=IS_LONG, value=42, refcount=1)
           // $a -> zval_2(type=IS_LONG, value=43, refcount=1)

unset($c); // zval_1 被销毁,因为其refcount=0
           // $a -> zval_2(type=IS_LONG, value=43, refcount=1)

引用计数有一个致命缺陷:它不能检测和释放循环引用。为解决这个问题,PHP额外使用了环收集器。当一个zval的引用计数减少的时候,它就有一定几率是循环引用的一部分,该zval就被写入到“根缓冲区”。当根缓冲区满后,可能的引用环将被标记并收集,同时启动垃圾回收。

为了支持这个环收集器,实际使用了如下的zval结构体:

typedef struct _zval_gc_info {
    zval z;
    union {
        gc_root_buffer       *buffered;
        struct _zval_gc_info *next;
    } u;
} zval_gc_info;

zval_gc_info结构体内置了普通zval和一个指针-注意u是一个共用体,也就是说实际上只有一个指针,它可能指向两种不同的类型。buffered指针用来存储zval在根缓冲区中的引用位置,如果zval在环收集器运行之前就被销毁(这是非常可能的),那么该指针将会从根缓冲区移除。next指针在收集器销毁值的时候会被用到,但是我不会深入讲解这一点。

改进的动机
先讨论一下基于64位系统的内存占用。首先,zvalue_value共用体占用16个字节,因为它的str和obj成员都那么大。整个zval结构体一共24个字节(由于内存对齐[padding]),而zval_gc_info是32字节。除此之外,在堆分配的过程中,又增加了16字节的分配开销。由此一个zval就占用48字节--尽管该zval可能在多个地方都被用到。

现在我们就可以分析下这种zval实现方式低效的地方。考虑用zval存储整数的情况,整数占用8个字节,另外类型标示是必需的,它本身占用一个字节,但是由于内存对齐,实际上就要加上8个字节。

这16字节是我们真正“需要”的空间(近似的),此外,为了处理引用计数和垃圾回收,我们增加了16字节;由于分配开销又增加了另外16字节。更不用提还要处理分配和后续的释放,这都是很昂贵的操作。

由此引发了一个问题:一个简单的整数真的需要存储为一个有引用计数、可垃圾回收,并且是堆分配的值吗?答案当然是不需要,这样做是没道理的。

以下概述了PHP5中zval实现方式的一些主要问题:

zval(几乎)总是需要堆分配。
zval总是会被引用计数且携带环收集信息,即使是在共享值不划算(比如整数)和不能形成引用环的情况下。
当处理对象和资源时,直接对zval进行引用计数会导致双重计数。原因会在下一部分讨论。
某些情况会引入很多的间接操作。比如为了访问一个对象,一共要进行4次指针跳转。这也将在下一篇中分析。
直接对zval进行引用计数意味着值只能在zval间共享。比如我们不能在zval和哈希表key之间共享一个字符串(不将哈希表key用zval变量存放)。
PHP7中的zval
通过以上讨论,我们引进了PHP7新的zval实现。最根本的改变是zval不再是堆分配且它自身不再存储引用计数。相反的,对zval指向的任何复杂类型值(如字符串、数组、对象),这些值将自己存储引用计数。这有以下优点:

简单值不需要分配且不用引用计数。
不再有双重引用计数。对对象来说,只有在对象本身存在引用计数。
由于引用计数保存在值中,这个可以独立于zval结构而被复用。同一个字符串能同时被zval和哈希表key引用。
间接操作少了很多,也就是说在获取一个值的时候需要跳转的指针数量变少了。
新的zval定义如下:

struct _zval_struct {
    zend_value value;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar type,
                zend_uchar type_flags,
                zend_uchar const_flags,
                zend_uchar reserved)
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t var_flags;
        uint32_t next;                 // hash collision chain
        uint32_t cache_slot;           // literal cache slot
        uint32_t lineno;               // line number (for ast nodes)
        uint32_t num_args;             // arguments number for EX(This)
        uint32_t fe_pos;               // foreach position
        uint32_t fe_iter_idx;          // foreach iterator index
    } u2;
};

第一个成员跟之前类似,也是一个value共同体。第二个成员是个整数,用来存储类型信息,它被一个共用体分隔成独立的字节空间(可忽略ZEND_ENDIAN_LOHI_4宏,它是用来保证在不同字节序平台上布局的一致性)。这个子结构中type(它跟之前类似)和type_flags比较重要,我将稍后讨论他们。

此时有一个小问题:value成员占8字节空间,由于结构体内存对齐,即使增加一个字节也会让zval内存增长到16字节。然而很明显我们不需要8个字节来仅仅存放类型信息。这就是为什么此zval包含了一个额外的u2共用体,它默认情况下是没被占用的,但是却可以根据需要存储4字节的数据。这个共用体中不同的成员用来实现该额外数据片段不同的用途。

PHP7中的value共用体看起来略有不同:

typedef union _zend_value {
    zend_long         lval;
    double            dval;
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;

    // Ignore these for now, they are special
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        ZEND_ENDIAN_LOHI(
            uint32_t w1,
            uint32_t w2)
    } ww;
} zend_value;

首先要注意到这个共用体占用8字节而不是16字节。它仅仅会直接存储整数(lval)和双精度浮点数(dval),对其它类型它都会存储对应指针。所有的指针类型(除了什么代码中标记为特殊的)都会引用计数并且有一个通用的头部,定义为zend_refcounted:

struct _zend_refcounted {
    uint32_t refcount;
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,
                uint16_t      gc_info)
        } v;
        uint32_t type_info;
    } u;
};

不用说这个结构会包含引用计数。另外,它还包含type、flags和gc_info。type是复制的zval的type,它使得GC在不存储zval的情况下就能区分不同的引用计数结构。根据类型的不同,flags有不同的使用目的,这些会在下一部分按类型分别讨论。

gc_info等同于老zval中的buffered成员。不同的是它存储了在根缓冲区中的索引,来代替之前的指针。因为跟缓冲区尺寸固定(10000个元素),用16字节的数子而不是64位的指针就足够了。gc_info还含有该节点的“颜色”信息,这在垃圾回收中用来标记节点。

zval内存管理
我已经提到zval不再是单独的堆分配。然而很明显它仍然需要被存在某个地方,那么这是怎么实现的呢?尽管zval大多数时候仍是堆分配数据结构的一部分,不过它们是直接嵌入到这些数据结构中的。比如哈希表就会直接内置zval而不是存放一个指向另一zval的指针。函数的编译变量表或者对象的属性表会直接保存为一个拥有连续内存的zval数组,而不再存储指向散落各处zval的指针。因此当前的zval存储通常都会少了一层的间接引用,也就是说现在的zval相当于之前的zval*。

当一个zval在新的地方被引用时,按照之前的方式,就意味着要复制zavl*并增加它的引用计数。现在则需要复制zval的内容,同时如果该zval指向的值用到引用计数的话则还要增加该值的引用计数。

PHP是如何知道一个值是否用到引用计数的呢?这不能仅仅依靠类型来判断,因为有些类型比如字符串和数组并不总是引用计数的。相反的,会根据构成zval的type_info的一个字节来判断是否引用计数。另外还有其它几个字节编码了该类型的一些特征。

#define IS_TYPE_CONSTANT            (1<<0)   /* special */
#define IS_TYPE_IMMUTABLE           (1<<1)   /* special */
#define IS_TYPE_REFCOUNTED          (1<<2)
#define IS_TYPE_COLLECTABLE         (1<<3)
#define IS_TYPE_COPYABLE            (1<<4)
#define IS_TYPE_SYMBOLTABLE         (1<<5)   /* special */

一个类型能拥有的三个主要特征是引用计数、可回收和可复制。引用计数的含义已讨论过,可回收意味着该zval可能参与循环引用。举例来说,字符串(通常)是引用计数的,但是却没法用字符串构造一个引用环。

可复制性决定了在为一个变量创建“副本”的时候它的值是否需要执行拷贝。副本是硬拷贝,比如复制指向数组的zval时,就不是简单的增加数组的引用计数,而是要创建该数组的一个新的独立拷贝。然而对对象和资源这些类型来说,复制应该仅仅增加引用计数--这些类型就是所谓的不可复制。这与对象和资源在进行传递时的语义相符(当前不是引用传递)。

以下表格展示了不同类型和它们所用的标识。“简单类型”指整数和布尔值这类不需要用指针指向一个单独结构的类型。同时还用一列展示了“不可变”标记,它用来标记不可变数组,这将在下一部分详细讨论。

refcounted collectable copyable immutable
simple types
string x x
interned string
array x x x
immutable array x
object x x
resource x
reference x

来看一下在实际中zval管理是如何工作的。先基于上文PHP5的例子来讨论一下整型实现:

$a = 42;   // $a = zval_1(type=IS_LONG, value=42)

$b = $a;   // $a = zval_1(type=IS_LONG, value=42)
           // $b = zval_2(type=IS_LONG, value=42)

$a += 1;   // $a = zval_1(type=IS_LONG, value=43)
           // $b = zval_2(type=IS_LONG, value=42)

unset($a); // $a = zval_1(type=IS_UNDEF)
           // $b = zval_2(type=IS_LONG, value=42)

这个例子挺无趣的。简单来说就是整型不会再被共用,这些变量都有单独的zval。不要忘了zval不再需要单独分配,它们是内嵌的,我通过把->换成=来表示这种变化。unset一个变量会把对应zval的type设置为IS_UNDEF。现在来考虑一下当涉及复杂类型时的情况,这种案例有趣的多。

$a = [];   // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])

$b = $a;   // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=2, value=[])
           // $b = zval_2(type=IS_ARRAY) ---^

// zval在这里发生了分离
$a[] = 1   // $a = zval_1(type=IS_ARRAY) -> zend_array_2(refcount=1, value=[1])
           // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])

unset($a); // $a = zval_1(type=IS_UNDEF) and zend_array_2 is destroyed
           // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])

本例中每个变量依然有单独的zval(内嵌的),但是这些zval都指向了同一个zend_array(引用计数的)结构。同PHP5一样,当发生修改时,数组需要被复制。

类型
看一下PHP7是如何支持各种数据类型的:

// regular data types
#define IS_UNDEF                    0
#define IS_NULL                     1
#define IS_FALSE                    2
#define IS_TRUE                     3
#define IS_LONG                     4
#define IS_DOUBLE                   5
#define IS_STRING                   6
#define IS_ARRAY                    7
#define IS_OBJECT                   8
#define IS_RESOURCE                 9
#define IS_REFERENCE                10

// constant expressions
#define IS_CONSTANT                 11
#define IS_CONSTANT_AST             12

// internal types
#define IS_INDIRECT                 15
#define IS_PTR                      17

这个列表跟PHP5类似,但有一些内容增加:

IS_UNDEF类型替代了之前的NULL zval指针(注意与IS_NULL zval区分),比如在上面引用计数的例子中,变量被unset时,zval的类型就被置为IS_UNDEF。
IS_BOOL类型被细分成了IS_FALSE和IS_TRUE。由此布尔变量的值就被编码在类型中,这就使得一些基于类型检查的优化成为可能。这个改变对用户层是透明的,仍然有一个“布尔”类型。
在zval上,PHP引用不再使用is_ref标识,而是用IS_REFERENCE类型。下一部分将会讨论。
IS_INDIRECT和IS_PTR是特殊的内部类型。
IS_LONG目前存储的是zend_long类型的值,而不是一个普通的C语言long整数。原因是在64位windows(LLP64)上,long型只有32位,于是在windows上PHP5的IS_LONG总是32位的。在64位操作系统上,即使你使用的是windows,PHP7都允许你使用64位的数字。

zend_refcounted类型相关的细节将在下一部分讨论,现在我们先看一下PHP引用的实现。

引用
PHP7处理PHP引用(&)的方式与PHP5完全不同(我可以告诉你这个改变是PHP7最大的bug来源之一)。PHP5中引用的实现如下:

通常,写时复制(COW)机制意味着在修改之前,zval要先进行分离,以保证不会把其它共用该zval的变量给一起修改了。这与值传递的语义相符。

对PHP引用来说,就不是这种情况了。如果一个值是引用,那么修改的时候就希望其它变量也同步被修改。PHP5用is_ref来判断一个值是不是PHP引用,以及在修改的时候是否要执行分离操作。看一个例子:

$a = [];  // $a     -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[])
$b =& $a; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[])

$b[] = 1; // $a = $b = zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[1])

这种设计一个很重大的问题就是不能在普通变量和PHP引用之前共享一个值。考虑如下情形:

$a = [];  // $a         -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[])
$b = $a;  // $a, $b     -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
$c = $b   // $a, $b, $c -> zval_1(type=IS_ARRAY, refcount=3, is_ref=0) -> HashTable_1(value=[])

$d =& $c; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
          // $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[])
          // $d是$c的引用, 但不是$a和$b的引用,所以zval要复制。
          //现在就有了相同的zval,一个is_ref=0,一个is_ref=1

$d[] = 1; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
          // $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[1])
          // 由于有两个独立的zval $d[] = 1 不会修改到$a和$b.

这种行为就导致使用PHP引用通常比普通变量更慢。下面的例子就有这个问题:

$array = range(0, 1000000);
$ref =& $array;
var_dump(count($array)); // <-- 这里发生zval分离

因为count()的参数是按值传递的,而$array是一个引用变量,在把它传递给count()时,会对该数组执行完整的复制。如果$array不是引用,它的值就可以共用,在传递的时候就不会发生复制。

现在来看下PHP7中引用的实现。由于zval不再是独立分配,不再可能使用PHP5一样的方式。转而增加了IS_REFERENCEl类型,它的值是如下的zend_reference结构:

struct _zend_reference {
    zend_refcounted   gc;
    zval              val;
};

所以zend_reference本质上只是一个有引用计数的zval。在一个引用集合中所有的变量都会保存一份IS_REFERENCEl类型的zval,并且指向同一个zend_reference实例。val跟其他zval类似,特别是它可以共享其指向的复杂值。比如数组可以在普通变量和引用变量之间共享。

还是上面的示例代码,来看一下在PHP7下的情形。为了简洁性,我不会再写变量的zval,只展示它们指向的值。

$a = [];  // $a                                     -> zend_array_1(refcount=1, value=[])
$b =& $a; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[])

$b[] = 1; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[1])

引用赋值会创建一个zend_reference,该引用的引用计数是2(有两个变量用到了这个引用),但是值本身的引用计数是1(只有一个zend_reference指向了该值)。再考虑下引用变量和普通变量混合的情况:

$a = [];  // $a         -> zend_array_1(refcount=1, value=[])
$b = $a;  // $a, $b,    -> zend_array_1(refcount=2, value=[])
$c = $b   // $a, $b, $c -> zend_array_1(refcount=3, value=[])

$d =& $c; // $a, $b                                 -> zend_array_1(refcount=3, value=[])
          // $c, $d -> zend_reference_1(refcount=2) ---^
          // 注意所有的变量共享同一个zend_array, 即使有的是引用,有的不是。

$d[] = 1; // $a, $b                                 -> zend_array_1(refcount=2, value=[])
          // $c, $d -> zend_reference_1(refcount=2) -> zend_array_2(refcount=1, value=[1])
          // 只有当赋值发生的时候,zend_array才会复制,即写时分离。

与PHP5一个重要的不同是所有的变量都能共享同一个数组,即使有的是引用变量有的不是。只有当进行修改的时候才会发生分离。这意味着在PHP7中把一个很大的引用数组传递给count()是安全的,因为不会复制。但是引用仍然会比普通变量慢,因为需要分配zend_reference结构(以及由此产生的间接操作),而且机器码处理起来也不会很快。

总结

总的来说,PHP7主要的改变是zval不再是独立的堆分配且其本身不再存储引用计数。转而是它们指向的复杂类型的值(如字符串、数组、对象)会存储引用计数。这通常会带来更少的内存分配、间接操作和内存使用。

下一部分将会讨论其它复杂类型。

Elasticsearch和PHP结合使用搜索分词

本示例所用环境
* 机器:普通电脑 ThinkPad E470c
* 系统:Ubuntu 16.04 LTS(在ArchLinux下测试也没有任何问题)
步骤

一. 学习Python,使用Python爬虫框架Scrapy爬取测试数据,之前准备爬取微博数据,学艺不精,爬取了豆瓣书籍目录下的一些信息。
1. 学习Python,感谢廖雪峰老师关于Python教程所作的贡献,教程地址
2. 感谢Scrapy文档,感叹:只要有一颗学习的心,知识真是唾手可得。
3. 感谢不知名网友,通过他的示例,完成了自己的需求,我自己爬取豆瓣书籍信息的项目地址


二. 爬取数据时,是保存在文件里的,用PHP把数据导入MySQL。
1. 把抓取到的书籍信息文件items.json放入第四步的demo里,然后使用demo里的PHP脚本导入MySQL.


三. 安装Elasticsearch,用PHP把数据从MySQL导入Elasticsearch。
1. 感谢阮一峰老师关于Elasticsearch教程所作的贡献,教程地址
这里面讲讲解了Elastic的安装和简单使用,还有分词插件。
2. 通过上述教程,结合Elastic官方出的elasticsearch-php库,完成了下面的demo。


四. 完成demo。
1. 项目地址
2. 图示
elasticsearch搜索显示示例图片


demo简介
  • Html+jquery+bootstrap+PHP+Elasticsearch中文分词搜索显示的示例。

demo目录结构及文件说明:

README.md 此说明


composer.json composer资源文件,此demo所使用的两个PHP库,Eloquent和elasticsearch-php库。Eloquent是Laravel里使用的数据库ORM,方便好用,能独立于Laravel使用。elasticsearch是PHP调用Elasticsearch服务的库。clone此项目后,使用命令:composer update -vvv安装依赖。


items.json 使用Python爬虫获取的豆瓣读书目录下的一些信息,JSON格式。此文件已从版本库里去掉,文件已共享


book.sql 数据库和数据表结构


douban.sql 爬取的数据导入MySQL后,导出的小部分SQL数据。觉得自己学习爬数据麻烦的同学,可以直接把数据导入MySQL。觉得数据太少的同学可以下载上面的json文件自己使用下面的脚本导入数据库,再从数据库导入ElasticSearch。


import_data_to_book_table_form_items_json.php 把items.json文件内容导入数据表脚本,命令:php 文件名直接执行


start.php PHP第三方库和数据库等前置配置



以下四个文件是单独测试Elasticsearch示例

createIndex.php 创建elasticsearch index、type(类似创建MySQL数据库、数据表),命令:php createIndex.php

insert.php 向elasticsearch插入测试数据,命令:php insert.php

search.php 根据条件查询数据,命令:php search.php

delete.php 删除elasticsearch index或者document,命令:php delete.php


douban 实际搜索显示demo目录

createDoubanIndex.php 创建elasticsearch index、type(类似创建MySQL数据库、数据表),命令:php createDoubanIndex.php

insertDataToEs.php 把之前导入MySQL的数据导入ElasticSearch,使用Eloquent ORM查出数据,然后批量插入Elasticsearch,命令:php insertDataToEs.php

search.php 前端请求的后端地址文件

view 前端模版文件目录

index.html 搜索显示页面,使用ajax分页和向后端传递数据

在自己本地测试访问地址:localhost/项目路径/elasticsearch_example/douban/view/index.html

完结

自动化检测PHP语法和编程规范(Git pre-commit)

自动化检测PHP语法和编程规范

使用到的知识点:

  • 命令php -l检测文件语法,可以通过php -h查看PHP CLI支持哪些操作,如下:.
[:~]$ php -h
Usage: php [options] [-f] <file> [--] [args...]
   php [options] -r <code> [--] [args...]
   php [options] [-B <begin_code>] -R <code> [-E <end_code>] [--] [args...]
   php [options] [-B <begin_code>] -F <file> [-E <end_code>] [--] [args...]
   php [options] -S <addr>:<port> [-t docroot] [router]
   php [options] -- [args...]
   php [options] -a

  -a               Run interactively
  -c <path>|<file> Look for php.ini file in this directory
  -n               No configuration (ini) files will be used
  -d foo[=bar]     Define INI entry foo with value 'bar'
  -e               Generate extended information for debugger/profiler
  -f <file>        Parse and execute <file>.
  -h               This help
  -i               PHP information
  -l               Syntax check only (lint)
  -m               Show compiled in modules
  -r <code>        Run PHP <code> without using script tags <?..?>
  -B <begin_code>  Run PHP <begin_code> before processing input lines
  -R <code>        Run PHP <code> for every input line
  -F <file>        Parse and execute <file> for every input line
  -E <end_code>    Run PHP <end_code> after processing all input lines
  -H               Hide any passed arguments from external tools.
  -S <addr>:<port> Run with built-in web server.
  -t <docroot>     Specify document root <docroot> for built-in web server.
  -s               Output HTML syntax highlighted source.
  -v               Version number
  -w               Output source with stripped comments and whitespace.
  -z <file>        Load Zend extension <file>.

  args...          Arguments passed to script. Use -- args when first argument
                   starts with - or script is read from stdin

  --ini            Show configuration file names

  --rf <name>      Show information about function <name>.
  --rc <name>      Show information about class <name>.
  --re <name>      Show information about extension <name>.
  --rz <name>      Show information about Zend extension <name>.
  --ri <name>      Show configuration for extension <name>.


  • 安装PHP编程规范工具php-cs-fixer,可以在GitHub上查看详细信息.
    1. 下载:wget http://cs.sensiolabs.org/download/php-cs-fixer-v2.phar -O php-cs-fixer
    2. 给予执行权限:sudo chmod a+x php-cs-fixer
    3. 把文件移动到自己喜欢的目录,我一般是放在/usr/local/bin下:sudo mv php-cs-fixer /usr/local/bin/php-cs-fixer

  • 利用Git的钩子pre-commit达成commit前自动检测功能.
    这个钩子,顾名思意就是在git commit之前会触发,此钩子结合网上和朋友的写法,感谢他们.
    首先,点击查看这个文件,复制文件内容.
    然后,进入到自己的项目下git钩子目录:cd path/to/your/project/.git/hooks,复制pre-commit.sample文件并重命名:cp pre-commit.sample pre-commit,把文件内容替换为上一步复制的内容.
    最后,每次commit时就会先检测语法和规范是否正确,不正确会提示文件名和你需要规范代码格式的命令,简单测试如下图:.
    php-cs-fixer pre-commit

Symfony常用命令

  • 创建数据库

    php app/console doctrine:database:create

  • 删除数据库

    php app/console doctrine:database:drop --force

  • 生成单个实体

    php app/console doctrine:generate:entity

    1. 输入:AppBundle:Product会创建一个Product的Entity
    2. 输入要映要的字段名称:name
    3. 输入字段类型(默认字符串):string
    4. 输入字段长度(默认255):255
    5. 输入字段是否允许为空(默许不能为空):false
    6. 输入字段是否唯一(默认不为唯一值):false
    7. 回车完成
  • 创建|更新数据表(前提是有对应表的Entity)

    php app/console doctrine:schema:update --force

  • 更新所有实体(参数--no-backup不生成备份文件)

    php app/console doctrine:generate:entities AppBundle --no-backup

  • 清理缓存:

    php app/console cache:clear
    生产环境加上-e prod参数:
    php app/console cache:clear -e prod

  • 重新生成 app/bootstrap.php.cache:

    php ./vendor/sensio/distribution-bundle/Resources/bin/build_bootstrap.php

thrift的安装和php的简单示例

  • 首先,请粗略查看Thrift安装说明文档,这里列出了不同系统需要的各种依赖,后面编译安装失败的原因有可能是缺少依赖,第一步就是把需要的依赖解决,我的系统是Ubuntu 16.04 LTS,所以参照的是这里的说明;

  • 其次,下载thrift源码和示例,下载完成后可用md5sum校验文件是否下载完整。解压文件tar -xzvf thrift-0.10.0.tar.gz,解压后目录里包含很多文件,有现有可使用thrift的各种语言的库,在lib目录里,以及对应语言的使用示例,在tutorial目录;

  • 然后,配置,编译,安装:

  1. ./configure && make
  2. sudo make install
  3. 检查是否安装成功thrift -version,我的显示Thrift version 0.10.0,安装成功,版本为0.10.0
    > 可参考官方文档Building from source
  • 最后,创建php服务端和用户端,开发流程:
  1. 配置环境.PHP的php-fpm,Nginx等.
  2. 根据需求,编写thrift接口定义文件(IDL定义文件),解压的目录里已经包含各种示例所需的IDL文件,在解压目录/tutorial/.
  3. 使用thrift程序,为不同的语言生成代码.
  4. 根据需求,修改生成的代码(主要是Server端),编写实际的业务逻辑.
    > 参考博客
  • 实际步骤:
  1. 移动到PHP项目目录下.
  2. 创建目录mkdir thrift_php_server_client_demo.
  3. 复制上面解压目录PHP相关文件到thrift_php_server_client_demo目录下:
    > cp -r 你的解压目录/lib/php/lib/Thrift/ xxx/thrift_php_server_client_demo
    > cp -r 你的解压目录/tutorial/php/ xxx/thrift_php_server_client_demo
    > cp 你的解压目录/tutorial/tutorial.thrift xxx/thrift_php_server_client_demo
    > cp 你的解压目录/tutorial/shared.thrift xxx/thrift_php_server_client_demo
  4. 生成php代码thrift -r --gen php tutorial.thrift
    > 可参考官方文档Apache Thrift Tutorial
  5. 运行PHP服务端php -S localhost:8080
  6. 运行客户端php php/client.php --http,即可以看到结果运行结果,运行服务端的终端也会显示请求过程.
    > 注:我在运行客户端文件时,一直提示server.php里\tutorial\CalculatorProcessor类找不到,开始以为是我命名空间或其它配置有问题,后来查看对比了这个php-thrift-server项目才知道,我使用步骤4生成的目录文件(gen-php/tutorial/Calculator.phpgen-php/shared/SharedService.php)里都缺少相关内容,这个可太坑了,花费了好长时间才发现;我clone了这个项目后,用这个项目里的thrift文件生成代码后也缺少相关类的内容,我怀疑是我安装的thrift版本的问题,把内容加上就正常了。
  7. 我的github示例地址,下载代码即可使用上述步骤5和步骤6运行.

php-fpm里进程管理配置介绍

  • 强调一个观点,给自己的备忘录:
    > 关于开发时使用到的相关软件(php,nginx,php-fpm,mysql等)的使用信息,最好的方式是查看他们的文档和各自的配置信息.
当然没有一定的知识,可能看了也会云里雾里,在此给出自己对php-fpm配置文件的一些理解.
  • 首先,需要知道php-fpm配置文件所在的路径,使用命令ps -ef | grep php,如下图:
    本机
    远程服务器
  1. 可以看到在两台机器上,配置文件的路径不一样,上图的路径是/etc/php/5.6/fpm/php-fpm.conf,下图的路径是/etc/php-fpm.conf,而且php-fpm启动的进程数也不一样,master进程数都为1,worker进程数上图为3个,下图为5个.
  2. 配置文件路径不一样是因为软件安装时软件包里指定的路径不一样,但为什么启动的进程数也不一样呢?这就要看配置文件的详细内容了.
  • 打开php-fpm.conf,内容如下,详细信息请看注释:
;;;;;;;;;;;;;;;;;;;;;
; FPM Configuration ;
;;;;;;;;;;;;;;;;;;;;;

; All relative paths in this configuration file are relative to PHP's install
; prefix (/usr). This prefix can be dynamically changed by using the
; '-p' argument from the command line.

; Include one or more files. If glob(3) exists, it is used to include a bunch of
; files from a glob(3) pattern. This directive can be used everywhere in the
; file.
; Relative path can also be used. They will be prefixed by:
; - the global prefix if it's been set (-p argument)
; - /usr otherwise
; 可以看到,在文件的开始就又加载了另外一个目录下以 .conf 结尾的配置,关于进程运行方式和数量的配置就在这个目录下.
include=/etc/php/5.6/fpm/pool.d/*.conf

........ 以下内容省略,因为此次内容是关于php-fpm进程相关的配置,故只截取了部分内容 ........
  • 打开对应目录/etc/php/5.6/fpm/pool.d/,可以看到www.conf文件,打开文件,可以看到内容有很多,我只截取了与进程数相关的内容,大概从74行开始,不同机器,不同版本php-fpm,文件内容应该大同小异,详情请看注释
; Choose how the process manager will control the number of child processes.
; 注:以何种方式运行php-fpm,进而管理子进程数量,这里列出了三种可选方式:
; 静态模式(static),动态模式(dynamic),按需加载模式(ondemand).

; Possible Values:
; static - a fixed number (pm.max_children) of child processes;
; 注:静态模式,这种模式下,进程数是固定的,固定的个数就是参数 max_children 所设置的个数,此个数不可以动态调整,对于个人本地开发或是小站来说,此模式可以节约资源.

; dynamic - the number of child processes are set dynamically based on the
; following directives. With this process management, there will be
; always at least 1 children.
; 注:动态模式,这种模式下,进程数是会动态改变的,但最少必须保留一个进程,它的启始进程数和动态改变最大进程数受以下参数控制.
; pm.max_children - the maximum number of children that can
; be alive at the same time.
; 最大进程数:动态增加时,进程数的最大值,即使 php-fpm 进程数被使用完要报错了,进程数的总量也不会超过这个值的限定.
;
; pm.start_servers - the number of children created on startup.
; 启始进程数:望文生义即可.

; pm.min_spare_servers - the minimum number of children in 'idle'
; state (waiting to process). If the number
; of 'idle' processes is less than this
; number then some children will be created.
; 最少保留进程数:望文生义即可,需要注意的时,即使没有请求需要处理,空闲的进程数少于这个设定值时,php-fpm的进程数还是会增加,这样就会浪费机器资源,要根据机器实际情况来调整.
;
; pm.max_spare_servers - the maximum number of children in 'idle'
; state (waiting to process). If the number
; of 'idle' processes is greater than this
; number then some children will be killed.
; 最大保留进程数:望文生义即可,需要注意的时,在没有请求需要处理,空闲的进程数大于这个设定值时,php-fpm的进程数会被kill掉.

; ondemand - no children are created at startup. Children will be forked when
; new requests will connect. The following parameter are used:
; 注:按需加载模式,服务启动时没有进程启动,但在请求到来时,会自动fork进程,这种模式下,进程数是会动态改变的,但最少必须保留一个进程,它的启始进程数和动态改变最大进程数受以下参数控制.
;
; pm.max_children - the maximum number of children that
; can be alive at the same time.
; 最大进程数:同上.
;
; pm.process_idle_timeout - The number of seconds after which
; an idle process will be killed.
; 进程空闲保留时间(秒):当进程空闲时,如果在设置的时间内没有被使用将会被kill掉.
; Note: This value is mandatory.
; 注:此参数不能缺省.
pm = dynamic

; The number of child processes to be created when pm is set to 'static' and the
; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'.
; This value sets the limit on the number of simultaneous requests that will be
; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
; CGI. The below defaults are based on a server without much resources. Don't
; forget to tweak pm.* to fit your needs.
; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand'
; Note: This value is mandatory.
; 注:非缺省值,含义见上.
pm.max_children = 5

; The number of child processes created on startup.
; Note: Used only when pm is set to 'dynamic'
; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2
; 注:只能作用于动态模式,以动态模式运行时可为缺省值,缺省值为:最少保留进程数 + (最大保留进程数 - 最少保留进程数) / 2. 含义见上.
; 测试过,如果这个值随便填写重启php-fpm报错,但根据这个公式且不超过max_children,则没有问题,更详细需要进一步验证.
pm.start_servers = 2

; The desired minimum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
; 注:只能作用于动态模式,以动态模式运行时为非缺省值,含义见上.
pm.min_spare_servers = 1

; The desired maximum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
; 注:只能作用于动态模式,以动态模式运行时为非缺省值,含义见上.
pm.max_spare_servers = 3

; The number of seconds after which an idle process will be killed.
; Note: Used only when pm is set to 'ondemand'
; Default Value: 10s
; 注:只能作用于按需加载模式,缺省值为10s.
;pm.process_idle_timeout = 10s;
  • 总结:
  1. 以何种方式运行php-fpm,进而管理子进程数量,这里列出了三种可选方式:
    静态模式(static),动态模式(dynamic),按需加载模式(ondemand).

  2. 不同模式有不同参数控制进程数.

懂了配置参数的含义,就明白在不同的情况下要选择什么运行模式,以及为什么在查看进程数不同的机器上会有所不同了.

php-src源码目录说明

下面简单介绍一下PHP源码的目录结构。

  • 根目录: / 这个目录包含的东西比较多,主要包含一些说明文件以及设计方案。 其实项目中的这些README文件是非常值得阅读的例如:
    • /README.PHP4-TO-PHP5-THIN-CHANGES 这个文件就详细列举了PHP4和PHP5的一些差异。
    • 还有有一个比较重要的文件/CODING_STANDARDS,如果要想写PHP扩展的话,这个文件一定要阅读一下, 不管你个人的代码风格是什么样,怎么样使用缩进和花括号,既然来到了这样一个团体里就应该去适应这样的规范,这样在阅读代码或者别人阅读你的 代码是都会更轻松。
  • build 顾名思义,这里主要放置一些和源码编译相关的一些文件,比如开始构建之前的buildconf脚本等文件,还有一些检查环境的脚本等。
  • ext 官方扩展目录,包括了绝大多数PHP的函数的定义和实现,如array系列,pdo系列,spl系列等函数的实现,都在这个目录中。个人写的扩展在测试时也可以放到这个目录,方便测试和调试。
  • main 这里存放的就是PHP最为核心的文件了,主要实现PHP的基本设施,这里和Zend引擎不一样,Zend引擎主要实现语言最核心的语言运行环境。
  • Zend Zend引擎的实现目录,比如脚本的词法语法解析,opcode的执行以及扩展机制的实现等等。
  • pear “PHP 扩展与应用仓库”,包含PEAR的核心文件。
  • sapi 包含了各种服务器抽象层的代码,例如apache的mod_php,cgi,fastcgi以及fpm等等接口。
  • TSRM PHP的线程安全是构建在TSRM库之上的,PHP实现中常见的*G宏通常是对TSRM的封装,TSRM(Thread Safe Resource Manager)线程安全资源管理器。
  • tests PHP的测试脚本集合,包含PHP各项功能的测试文件
  • win32 这个目录主要包括Windows平台相关的一些实现,比如sokcet的实现在Windows下和*Nix平台就不太一样,同时也包括了Windows下编译PHP相关的脚本。

PHP的测试比较有意思,它使用PHP来测试PHP,测试php脚本在/run-tests.php,这个脚本读取tests目录中phpt文件。 读者可以打开这些看看,php定义了一套简单的规则来测试,例如以下的这个测试脚本/tests/basic/001.phpt:

--TEST--
Trivial "Hello World" test
--FILE--
<?php echo "Hello World"?>
--EXPECT--
Hello World

这段测试脚本很容易看懂,执行–FILE–下面的PHP文件,如果最终的输出是–EXPECT–所期望的结果则表示这个测试通过, 可能会有读者会想,如果测试的脚本不小心触发Fatal Error,或者抛出未被捕获的异常了,因为如果在同一个进程中执行, 测试就会停止,后面的测试也将无法执行,php中有很多将脚本隔离的方法比如: system(),exec()等函数,这样可以使用主测试进程服务调度被测脚本和检测测试结果,通过这些外部调用执行测试。 php测试使用了proc_open()函数, 这样就可以保证测试脚本和被测试脚本之间能隔离开。phpt文件的编写详细信息可参考 附录E phpt文件的编写。 如果你真的那么感兴趣,那么研究下$PHP_SRC/run-tests.php脚本的实现也是不错的选择。这个测试框架刚开始 由PHP的发明者Rasmus Lerdorf编写,后来进行了很多的改进。后面可能会引入并行测试的支持。

PHP魔术常量的示例

php的魔术常量有8个

私以为这个顺序是比较方便记忆的,从整体到局部,分别是:
__DIR__       文件所在目录的绝对路径

__FILE__     文件所在绝对路径

__LINE__    文件所在绝对路径

__NAMESPACE__    当前命名空间名称

__CLASS__    当前类名称

__TRAIT__     当前Trait名称

__MEHTOD__   当前方法名称

__FUNCTION__    当前函数名称

有一点是需要注意的就是trait的优先级,从基类继承的成员会被 trait 插入的成员所覆盖。优先顺序是来自当前类的成员覆盖了 trait 的方法,而 trait 则覆盖了被继承的方法。当前类的方法又会覆盖trait的方法。
当有使用到Trait时,且没有被当前类里的方法覆盖时__METHOD__输出的是trait里的方法;

否则,__METHOD__输出的是当前类里的方法。

以下是代码。

<?php
namespace bobo;

trait Lianbo {
    public function test()
    {
        echo 'hello world'.PHP_EOL;

        // /var/www/html/test
        echo __DIR__.PHP_EOL;

        // /var/www/html/test/magic_const.php
        echo __FILE__.PHP_EOL;

        // 16
        echo __LINE__.PHP_EOL;

        // bobo echo 
        __NAMESPACE__.PHP_EOL;

        // bobo\Lianbo 
        echo __TRAIT__.PHP_EOL;
        // bobo\Bobo
        echo __CLASS__.PHP_EOL;

        // bobo\Lianbo::test
        echo __METHOD__.PHP_EOL;

        // test
        echo __FUNCTION__.PHP_EOL;
    }
}

class Bobo {
    use Lianbo;

    //public function test() {
        //echo __DIR__.PHP_EOL;
        //echo __FILE__.PHP_EOL;
        //echo __LINE__.PHP_EOL;
        //echo __NAMESPACE__.PHP_EOL;
        //echo __CLASS__.PHP_EOL;
        //echo __TRAIT__.PHP_EOL;
        //echo __METHOD__.PHP_EOL;
        //echo __FUNCTION__.PHP_EOL;
    //}
}

$obj = new Bobo;
$obj->test();

图片第一张是class Bobo里test()方法注释时的结果,第二张是注释打开时的结果。

配置nginx + php + php-fpm 出现File not found的错误

nginx php File not found 错误

使用php-fpm解析PHP,"No input file specified","File not found"是令nginx新手头疼的常见错误,原因是php-fpm进程找不到SCRIPT_FILENAME配置的要执行的.php文件,php- fpm返回给nginx的默认404错误提示。

比如我的网站doucument_root下没有test.php,访问这个文件时通过抓包可以看到返回的内容。

HTTP/1.1 404 Not Found
Date: Fri, 21 Dec 2012 08:15:28 GMT
Content-Type: text/html
Proxy-Connection: close
Server: nginx/1.2.5
X-Powered-By: PHP/5.4.7
Via: 1.1 c3300 (NetCache NetApp/6.0.7)
Content-Length: 16

File not found.

 

很多人不想用户直接看到这个默认的404错误信息,想自定义404错误.

给出解决办法前我们来先分析下如何避免出现这类404错误,然后再说真的遇到这种情况(比如用户输入一个错误不存在的路径)时该怎么办,才能显示自定义的404错误页。

一、错误的路径被发送到php-fpm进程

出现这类错误,十个有九个是后端fastcgi进程收到错误路径(SCRIPT_FILENAME),而后端fastcgi收到错误路径的原因大都是配置错误。

常见的nginx.conf的配置如下:

 

server {
    listen   [::]:80;
    server_name  example.com www.example.com;
    access_log  /var/www/logs/example.com.access.log;  

    location / {
        root   /var/www/example.com;
        index  index.html index.htm index.pl;
    }

    location /images {
        autoindex on;
    }

    location ~ \.php$ {
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  /var/www/example.com$fastcgi_script_name;
        include fastcgi_params;
    }
}

这个配置中有很多不合理的地方,其中一个明显的问题就是root指令被放到了location / 块。

如果root指令被定义在location块中那么该root指令只能对其所在的location生效。其它locaiont中没有root指令,像 location /images块不会匹配任何请求,需要在每个请求中重复配置root指令来解决这个问题。因此我们需要把root指令放在server块,这样各个 location就会继承父server块定义的$document_root,如果某个location需要定义一个不同 的$document_root,则可以在location单独定义一个root指令。

另一个问题就是fastCGI参数SCRIPT_FILENAME 是写死的。

如果修改了root指令的值或者移动文件到别的目录,php-fpm会返回“No input file specified”错误,因为SCRIPT_FILENAME在配置中是写死的并没有随着$doucument_root变化而变化,我们可以修改 SCRIPT_FILENAME配置如下:

fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;

 

所以我们不能忘记在server块中配置root指令,不然$document_root的值为空,只会传$fastcgi_script_name到php-fpm,这样就会导致“No input file specified”错误。

 

二、请求的文件真的不存在

当nginx收到一个不在的.php文件的请求时,因为nginx只会检查$uri是否是.php结尾,不会对文件是否存在进行判断,.php结尾 的请求nginx会直接发给php-fpm处理。php-fpm处理时找不到文件就会返回“No input file specified”带着“404 Not Found”头。

解决办法

我们在nginx拦截不存在的文件,请求并返回自定义404错误

使用 try_files 捕捉不存在的urls并返回错误。

location ~ .php$ {
 try_files $uri =404;
 fastcgi_pass 127.0.0.1:9000;
 fastcgi_index index.php;
 fastcgi_param SCRIPT_FILENAME ....
 ...................................
 ...................................
}

上面的配置会检查.php文件是否存在,如果不存在,会返回404页面。