分类目录归档:PHP

PHP源码,PHP扩展,PHP程序

使用Yii框架中遇到的三个问题

使用Yii框架中遇到的三个问题

1、main.php文件中欲引入全局变量的问题

还原一下此问题:在Yii框架中,main.php一般会作为整个应用的配置文件,保存Application的各种参数,直接return数组。在使用的过程中,因为main.php文件一定会被Yii提前加载,所以将一些全局性的操作也放在了此文件,加载一些类操作啥的没有什么问题,当有一次加了一个全局变量,并且在其它地方使用global获取全局变量时,发现无论我如何努力都得到的是NULL。各种尝试后,终于,把引入的位置放在入口文件index.php,得以解决。什么原因?我们重现一下Yii的main.php文件加载。如下代码

index.php文件:

 class CApp {
        public function __construct($config) {
            $config = require($config);
        }
    }
 
    $path = "main.php";
    $app = new CApp($path);
 
    global $global;
    var_dump($global);

main.php文件:

 <?php
    $global = array(1, 2, 3);
    return array();

两个文件放在同一目录,直接运行index.php,输出的$global为NULL,如果我们在CApp的构造函数中直接输出$global,则会有结果输出。什么原因?作用域的问题!

当我们在main.php文件中定义了一个变量,虽然是想将其作为全局变量使用,但是当我们在局部的作用域中require时,其仅仅作为一个局部作用域的变量存在。我们在TIPI中有说到函数调用是嵌套的,每个嵌套都会有一个作用域,在这个作用域中的变量仅在当前有效,嵌套结束,变量生命周期结束。

因此,我们如果想把main.php中的全局变量真的作为整个应用的全局变量使用,则需要在入口文件的作用域中require main.php文件。

2、引入第三方扩展时的class_exists问题

Yii框架Yii基于PHP5的autoload机制来提供类的自动加载功能,自动加载器为YiiBase类的静态方法autoload()。当程序中用new创建对象或访问到类的静态成员,PHP将类名传递给类加载器,由类加载器完成类文件的include。但是如果我们引入了第三方扩展,而第三方扩展的命名规则和Yii的不一样,于是我们会经常看到报错说 require XXX 文件失败。如果你在google中搜索“yii framework class_exists”,你会发现Yii框架的作用Xue Qiang有回答使用者可以通过使用类似于: class_exists(‘MyClass’, false)的方式。

class_exists函数检查类是否已定义,如果由 class_name 所指的类已经定义,此函数返回 TRUE,否则返回 FALSE。在PHP内核中,此函数会查找当前类表中由 class_name 所指的类是否存在,在查找之前会全部转化为小写,所以不会区分大小写。其第二个参数是指是否使用autoload,默认为使用,此时class_exists函数会先执行autoload,然后再查找执行了autoload后类表中由 class_name 所指的类是否存在。因此我们可以通过设置第二个参数其为FALSE来绕过自动加载。

这可以解决问题,但是如果我们使用的是无法修改的第三方代码呢?怎么办?我自己是简单的hack了下,在调用第三方的操作之就将需要的类给加载了。

后来又采用了另一种解决方案:直接使用Yii:import的第二个参数,强制加载整个目录。

3、Yii的错误日志

问题就不细述了,只是将生产环境的配置整到了开发环境,于是错误看不到了。调整了下日志的规则,就OK了。

Yii对错误日志的处理依赖于PHP的set_error_handler函数和set_exception_handler函数。在CApplication的initSystemHandlers方法中有对这两个函数的处理。

foreach的指针问题

在PHP中,foreach 语法结构提供了遍历数组的简单方式。 foreach 仅能够应用于数组和对象,如果尝试应用于其他数据类型的变量,或者未初始化的变量,将导致错误。 foreach每次循环时,当前单元的值被赋给 $value 并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个单元)。

但是手册中提醒我们:

Note:
当 foreach 开始执行时,数组内部的指针会自动指向第一个单元。这意味着不需要在 foreach 循环之前调用 reset()。
在循环中修改 foreach 依赖其内部数组指针将可能导致意外的行为。

这里我们所要说的是foreach可能导致的意外情况。如代码1示例:

<?php
$arr = array(1,2,3,4,5);
 
foreach($arr as $key => &$row) {
echo key($arr), '=>', current($arr), "\r\n";
}

会输出什么?

如代码2示例呢?

<?php
$arr = array(1,2,3,4,5);
 
foreach($arr as $key => $row) {
echo key($arr), '=>', current($arr), "\r\n";
}

会输出什么?

代码1会依次输出变量,但是第一个元素并没有在输出结果中出现。

代码2只会输出数组的第二个元素。

为什么呢?

将代码2在VLD扩展中查看,

number of ops:  22
compiled vars:  !0 = $arr, !1 = $key, !2 = $row
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   INIT_ARRAY                                       ~0      1
         1      ADD_ARRAY_ELEMENT                                ~0      2
         2      ADD_ARRAY_ELEMENT                                ~0      3
         3      ADD_ARRAY_ELEMENT                                ~0      4
         4      ADD_ARRAY_ELEMENT                                ~0      5
         5      ASSIGN                                                   !0, ~0
   4     6    > FE_RESET                                         $2      !0, ->20
         7  > > FE_FETCH                                         $3      $2, ->20
         8  >   ZEND_OP_DATA                                     ~5      
         9      ASSIGN                                                   !2, $3
        10      ASSIGN                                                   !1, ~5
   5    11      SEND_REF                                                 !0
        12      DO_FCALL                                      1  $7      'key'
        13      ECHO                                                     $7
        14      ECHO                                                     '%3D%3E'
        15      SEND_REF                                                 !0
        16      DO_FCALL                                      1  $8      'current'
        17      ECHO                                                     $8
        18      ECHO                                                     '%0D%0A'
   6    19    > JMP                                                      ->7
        20  >   SWITCH_FREE                                              $2
   8    21    > RETURN                                                   1

从上面VLD扩展输出结果结合PHP的源代码可以知道,在foreach遍历之前, PHP内核首先会有个FE_RESET操作来重置数组的内部指针,也就是pInternalPointer, 然后通过每次FE_FETCH将pInternalPointer指向数组的下一个元素,从而实现顺序遍历。
并且每次FE_FETCH的结果都会被一个全局的中间变量存储,以给下一次的获取元素使用。

从这两个例子可以引申出三个问题:

1、为什么foreach循环体中执行key或current会显示第二个元素(非引用情况)?
以key函数为例,我们执行函数调用时,会执行中间代码SEND_REF,此中间代码会将没有设置引用的变量复制一份并设置为引用。当进入循环体时,PHP内核已经经过了一次fetch操作,相当于执行了一次next操作,当前元素指向第二个元素。因此我们在foreach的循环体中执行key函数时,key中调用的数组变量为PHP执行了一次fetch操作的数组拷贝,此时foreach的内部指针指向第二个元素。

2、为什么在foreach中执行end等操作,其循环过程不变?
在遍历的代码中通过end,next等操作数组的指针,数组的指针不会变化,这是因为在PHP内核进行FETCH操作时,会通过中间变量存储当前操作数组的内部指针,每遍历一个元素,会先获取之前存储的指针位置,获取下一个元素后,再恢复指针位置。

3、为什么$row的引用和非引用情况下输出结果不同?
如果是引用,PHP内核在reset数组时,会直接分裂数组,生成一个数组的拷贝,并将其设置为引用。
如果是非引用,PHP内核在reset数组时,当数组的引用计数大于1,并且不存在引用时,会拷贝数组供foreach使用,其它情况使用原数组,将其引用计数加1。

因为引用的不同,在循环体中给函数传递参数时其结果不同,导致看到的foreach数组内部指针变化的不同。对于非引用且引用计数大于1的情况,其本身就是两个不同的数组,在RESET时就不同了。

Bash之命令历史的存储和记录

在Bash中我们可以使用 history 命令回顾,修改和重用之前使用过的历史命令。去掉信号机制、去掉作业控制、去掉各种参数调用,今天我们只看下Bash中如何记录命令,如何存储历史命令。

在 Bash 的源代码中,history 命令的定义代码为 builtins/history.def , builtins 目录下存放的是内部命令的源代码,每个内部命令是一个def文件,如history.def,cd.def等。 Makefile中DEFSRC声明了所有内部命令的def文件。由 mkbuiltins.c 生成编译时辅助工具 mkbuiltins,mkbuiltins 处理 *.def 文件,生成命令的 *.c 源程序以及 builtins.c 、 builtext.h。 builtins.c 和 builtext.h 相当于各个内部命令的索引。

history.def 的作用是解析命令并将不同的参数分发命令到不同的功能实现,如读取命令、覆盖命令等。一些功能实现代码文件在 bashhist.c/bashhist.h,但是关于历史记录的基础操作并不在这两个文件中,它们在 lib/readline 中。这是因为 Bash 采用的GNU Readline函数库中。 Readline提供了统一的行编辑和历史记录功能的命令行交互方式。

history命令在显示时会显示所有的历史记录,这里的所有历史记录包括最开始从文件中读取的历史记录,还包括当前会话产生的记录。假设你的历史记录中已经有了500条命令,如果你用其它文档编辑器将历史日志文件写到1000条,打开终端,你会发现显示的还只有500记录。这是因为在打开终端初始化时,不仅仅有历史记录列表的读操作,还会有关于文件记录数限制的初始化操作,确保文件中的记录条数不会大于设定的最大值。这个初始化操作在 load_history 函数中实现,函数最开始就做了两次历史记录文件的截取操作,一次是默认500,一次是设置的最大值: HISTFILESIZE 。

在历史记录中有一个会话的概念,不同会话中的命令在没有保存到文件前是不会互相冲突的。比如,打开终端A如果你删除 .bash_history 的前10行命令,保存,在命令行中输入history,你会发现输出的命令历史记录并不是从1开始,而是从11开始的。如果此时,你再开一个终端B,输入若干条命令后,再输入history,你会发现历史记录中没有在终端A输入的命令。这是由于在一个终端会话中,历史记录从固化存储位置的读取操作只有一次,写入操作也只有一次,即在打开终端时读取,在关闭终端时写入。

除了文件存储,历史记录也可以记录在MMAP,对应的宏定义为 HISTORY_USE_MMAP。

如果以文件存储方式,命令记录以一行一条命令的方式存储在.bash_history(默认,如果有设置 HISTFILE 则优先使用 HISTFILE 中的值),虽然我们使用 history 命令时看到每条命令前都会有一个标号,我们可以使用 !标号 的方式重新执行命令,这个标号并不是唯一不变的,标号是在初始化时在读取文件时在程序操作中标记的,后续有命令加入时,标号会自增,这个自增并不会受历史记录的最大值限制。

当一次会话退出时保存历史记录文件,将历史数据固化存储,并将会话统计清零。当存储到文件时,Bash 会将此前会话中的命令直接存储到文件末尾,如果文件的记录数大于定义的最大记录数,则清空旧的历史命令,并且当下次再存储时会重写此前文件。代码示例如下:

      if (history_lines_this_session <= where_history () || force_append_history)
        append_history (history_lines_this_session, hf);
      else
        write_history (hf);
 
      sv_histsize ("HISTFILESIZE");

存储操作最终还是归集于存储介质的读写操作,如对文件的读写,增加的只是对业务逻辑规则的各种限制。命令可以在执行命令时记录,也可以在命令刚输入,但已经识别的情况下记录,Bash 属于后者。 Bash 在 yacc 做语法分析时将用户输入的命令通过 maybe_add_history 函数写入到当前会话的命令历史记录表中。在做语法分析时就已经记录了用户输入的命令,此时记录就不用管命令最终的结果是怎样,也不用管如果执行过程出了异常会怎样处理。它只是如实的在执行前记录用户输入的什么命令,由此,我们可以定义 Bash 的命令历史记录的定义为用户输入的命令历史记录。

参考资料: http://files.linjian.org/articles/techreport/bash_study.tar.gz