[Plugin]豆瓣秀 For WordPress

我喜欢在博客中显示我在豆瓣上的一些信息,比如想看哪些书哪些电影神马的。刚开始用的是 Robin的 http://www.robb.com.cn/plugins/ 的 WP-DoubanShow插件,他用的是豆瓣API。这个插件需要手动修改主题模板文件。刚开始用的挺好,修改就修改吧。不过后来换过几次主题后发现每次修改模板文件还挺“脏”的。遂问robin能否修改成widget的方式,他说官方推出了一个豆瓣秀功能http://www.douban.com/service/badgemaker, 所以不继续维护了。看过官方的说明。发现要在Wordpress中用也只能手动修改模板,不过官方提供了一个生成js的设置项,可以根据需要生成相应的脚本,选项也还算简单。 在网上搜了一番,没有给wordpress用的插件,所以自己写了一个,设置项和官方的一样。下面简单的说明一下:

  1. 第一步:下载插件文件: DoubanShow.zip, 或者在管理界面中添加插件, 搜索douban即可看到”豆瓣秀For WordPress” 选择安装, 如果这样的话,下面的上传步骤就不需要了
  2. 第二步:上传安装。 后台管理的  插件 -> 添加插件 -> 上传中上传下载的文件。

  3. 上传完后记得“启用插件”

  4. 第三步:在后台管理的 外观 -> 小工具 中选择”豆瓣秀“ 拖到右侧你想放置的位置。 然后点击拖过去的豆瓣秀箭头。出现如下设置:

    标题默认为空,就是不显示标题。也可以设置成你想要的标题。然后要设置好你的豆瓣ID,记住不是豆瓣的登录用户名。 设置好以后。去你的页面看看效果吧,也可以看我博客页面右下角。

PHP Puzzle(一): 有趣的变量作用域-PHP中global和Javascript中的var关键字

昨天在网上看到几道有意思的PHP题, 下面这道题让我想起了对应的Javascript版本.

function multiply($b) {
$a = 100;
global $a;
return $a * $b;
}
echo multiply(100);

这段代码运行结果是什么呢? 别急着执行这段代码,先想想你的结果.然后再对比一下吧.

我们看先看看global的定义 http://www.php.net/manual/en/language.variables.scope.php 这里也没有太为规范的解释.只是说可以通过global关键字来访问全局变量. 这里还涉及到一个类型转换的问题.

大家都知道PHP脚本是编译为opcode逐语句执行的. 那么现在要一句语句解释就很容易了.

function multiply($b) {
$a = 100; // 定义局部变量$a
global $a; // 访问全局变量$a, $a变量现在的是全局变量了
return $a * $b; // 返回$a和$b的乘积
}
echo multiply(100);

这里可能比较困惑的的是现在变量$a到底是局部变量还是全局变量了.因为global在定义局部变量之后.所以$a变为了全局变量,而在最后输出结果的时候$a并没有值.所以最后在相乘的时候是 NULL * 100; 也就是0了;可能会有人有疑问, 后面只是把$a变为了全局变量, 他的值应该不变的啊. 让我通过下面的例子来看把:

function func($d) {
$a = 100;
global $a;

var_dump(get_defined_vars()); // get_defined_vars()返回当前作用域的所有变量信息
}

func();

array(2) {
["d"]=>
int(0)
["a"]=>
&NULL
}

变量a是NULL的一个引用,因为全局作用域内没有a这个变量. 所以即使在函数前面定义了一个a变量,但是它的值已经指向了全局作用域了.
实际上 global关键字首先从全局符号表中查找变量名叫做a的变量,并把这个变量值设置为当前作用域的符号表中的a变量(更新了当前变量的值). 如果全局作用域内没有这个变量则会在全局作用域内增加这个变量, 实现代码见: $PHP_SRC/Zend/zend_vm_execute.h

static int ZEND_FASTCALL zend_fetch_var_address_helper_SPEC_CONST(int type, ZEND_OPCODE_HANDLER_ARGS) {

// ...
if (zend_hash_find(target_symbol_table, varname->value.str.val, varname->value.str.len+1, (void **) &retval) == FAILURE) {
switch (type) {
case BP_VAR_R:
case BP_VAR_UNSET:
zend_error(E_NOTICE,"Undefined variable: %s", Z_STRVAL_P(varname));
/* break missing intentionally */
case BP_VAR_IS:
retval = &EG(uninitialized_zval_ptr);
break;
case BP_VAR_RW:
zend_error(E_NOTICE,"Undefined variable: %s", Z_STRVAL_P(varname));
/* break missing intentionally */
case BP_VAR_W: {
zval *new_zval = &EG(uninitialized_zval);

Z_ADDREF_P(new_zval);
zend_hash_update(target_symbol_table, varname->value.str.val, varname->value.str.len+1, &new_zval, sizeof(zval *), (void **) &retval);
}
break;
EMPTY_SWITCH_DEFAULT_CASE()
}
}

//...
}

看了这个解释大家可能觉得理所当然.一句一句执行的嘛. 看完了PHP中全局作用域的例子,咱们再看看类似的Javascript中的局部变量的版本吧

var a = 1;
function multiply(b)
{
a = 100;
var a;

return a * b;
}
alert(a);
alert(multiply(100));

那这段代码的输出将会是多少呢?
如果还是同样的思路,结果可能是你的期望完全不一样的结果. 这里的var定义变量和php中global不是一样的东西, php中的global是会在运行时执行的.而Javascript中的var在运行之前就已经"处理"好了.在运行之前的"语法分析"(没有看过Javascript引擎的实现.姑且这么分把)过程中,multiply函数中出现了var a;则把变量a加到函数体内的"局部变量表"中了.在运行过程中并不会执行var a;这一句. 这也是Javascript"怪异"的地方.定义变量的位置并没有关系.所以在函数内定义局部变量最好放在函数体的前面.

所以第一个alert输出的1, 函数的执行并没有改版全局范围内的a变量; 第二就没有什么问题了, 是10000;

怎样获取PHP变量的变量名之PHP实现

上一篇文章里提到是用PHP扩展实现获取变量的变量名的方法. 今天发现有一个PHP实现的版本 . 实现方法来自:http://mach13.com/how-to-get-a-variable-name-as-a-string-in-php

刚开始以为这个方法好使, 仔细想想其实也是有问题的.

这个解决方法是用的PHP里的get_defined_vars()方法,该方法返回当前作用域内的所有变量信息.也是和$GLOBALS一样,以变量名 => 值的方式返回.
他的代码很简单:


function var_name (&$iVar, &$aDefinedVars)
{
foreach ($aDefinedVars as $k=>$v)
$aDefinedVars_0[$k] = $v;

$iVarSave = $iVar;
$iVar =!$iVar; // 将当前变量的值取反

$aDiffKeys = array_keys (array_diff_assoc ($aDefinedVars_0, $aDefinedVars)); // 对比取反前后的变量
$iVar = $iVarSave; // 恢复当前变量的值

return $aDiffKeys[0];
}

?>

它通过引用的方式改变当前变量的值, 然后通过对比前后两个数组的差异来获取值被改变了的变量.然后返回其名字.经过测试这的确是一个方法.相对我实现的方法. 它提供的方法移植性较好, 不需要赖以扩展. 而这个php版本的实现, 必须传递一个get_defined_vars()的参数, 我实现的那个扩展,则不需要. 对于类似 var_name($a=10,get_defined_vars()); 的调用,该方法无法正常获得变量名.

这个今天又仔细想了想,下面提供的方法是有问题的.. 他解决问题的方法是通过修改变量的值, 并对比前后所有的变量来找出值发生变化的变量. 而实际上.修改了其中一个变量另一个变量的值也会发生变化: 这就是引用, 如下

$a = 10;
$b = &$a;

echo var_name($b, get_defined_vars()); // 这回返回a, 而不是b. 和预期的并不一样.

PS: 如果你真的需要这种方法. 请重新思考一下你的需要真的需要这样的方法么?

[PHP-Internal]怎么样获取PHP变量的变量名之扩展实现

很长时间没有更新博客了. 一来最近工作比较忙,没有时间好好研究问题, 二是觉得没有很好的材料可以写. 也有一些没有彻底研究透的问题,写着写着没有了头绪,都扔在了草稿箱里了. 这次顺带也要更新一下博客的模版了, 现在的这个模版主体有点窄,不适合阅读. 我这个博客现在,以后主要还是写一些技术的东西.还是换一个眼睛友好的主题吧.

本文要解决的是从去年就一直在考虑的一个PHP的问题: 怎么样获取PHP变量的变量名. 一直以来都没有好好的研究.最近断断续续的开始看PHP源代码.并尝试解决. 直到两星期前把问题都解决了才开始把这些东西都记下来.

如果有兴趣先看看这个功能是怎么实现的. 可以先点击这里下载代码.

1.问题:能在PHP中获取php变量本身的名字么?

一年多前做一个模版引擎的什么时候有了这样一个需求: 获取变量的变量名. 比如:

$some_variable_name = "blahblah";
//...
echo get_var_name($some_variable_name); // 这里期望输出"some_variable_name";
?>

如果你也有这样的需求. 你对需求的理解绝对有问题.  不过后来想想这需求虽然不合理. 但是如果我偏有这样不合理的需求, 我有办法真的能满足么?

2.有哪些解决方法

在遇到这个问题之前,没有太系统的去看过PHP的C实现. 从问题提出到目前为止,我想到了如下几种方法:

  • 直接写一个PHP函数来获取.比如:

    function get_var_name($var) {
    // 但是... 我怎么的到变量的名字呢...
    // echo ? How To?
    }

    用过$GLOBALS变量的人应该知道可以通过 $GLOBALS['var']的方式来获取变量$var的值. 这样的话,我应该就能这样实现了

    function get_var_name($var) {
    foreach($GLOBALS as $var_name => $var_value) {
    if($var === $var_value) {
    return $var_name;
    }
    }
    }

    这个是不可行的. 首先, 这个方法只能返回全局作用域内的变量. 如果在函数体内调用这个函数会有问题. 并且通过值比较也完全不可靠.
  • 随后我开始看PHP的内部实现.知道了在PHP执行过程中所有的变量都是存放在符号表(symbol_table)中, 和$GLOBALS变量类似, 以变量名 =>值的方式存储.. 并且在不同的作用域内有不同的active_symbol_table, 这样的话就不存在作用域的问题了, 那我们是不是可以从当前的符号表中来根据传递进来的变量值来进行比较呢. 在符号表的值是存放在一个指向zval结构的指针. 那我们是否可能通过比较指针地址的方式来查找保存该值的变量名呢? 其实这也是行不通的. 因为在PHP内部可能有多个变量指向同一个内部值.也就是引用计数. 看来通过符号表还是解决不了问题.
  • 通过对PHP内部实现的进一步学习发现在脚本运行的时候还是有很多其他丰富的内部信息可以利用.比如如下的脚本运行时全局变量.  这也是解决这个问题的突破口所在, 本文将根据这些运行时信息来编写一个实现该功能的扩展.


    struct _zend_executor_globals {
    zval **return_value_ptr_ptr;

    zval uninitialized_zval;
    zval *uninitialized_zval_ptr;

    zval error_zval;
    zval *error_zval_ptr;

    zend_ptr_stack arg_types_stack;

    /* symbol table cache */
    HashTable *symtable_cache[SYMTABLE_CACHE_SIZE];
    HashTable **symtable_cache_limit;
    HashTable **symtable_cache_ptr;

    zend_op **opline_ptr;

    HashTable *active_symbol_table; // 当前作用域的变量符号表
    HashTable symbol_table; /* main symbol table */ // 全局符号表

    HashTable included_files; /* files already included */

    //..
    };

  • - 最后肯定能实现的一种方式是为PHP增加一个类似echo的语法结构. 这种方式的侵入性最大, 在这篇日志中将不讨论这种实现方式, 我将在下一篇日志中介绍通过修改PHP语法的方式来支持开篇所提出的问题.
    • 3.扩展实现

      比如模块提供一个叫做get_var_name()的函数来获取变量名字.  如果大家有写过PHP扩展的经验的话,应该看过类似如下的函数实现(取自php json扩展$PHP_SRC/ext/json/json.c):

      /* {{{ proto string json_encode(mixed data [, int options])
      Returns the JSON representation of a value */
      static PHP_FUNCTION(json_encode)
      {
      zval *parameter;
      smart_str buf = {0};
      long options = 0;

      if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", &parameter, &options) == FAILURE) { // 这里将传入的参数取出来. 参考文档 http://www.php.net/manual/en/internals2.funcs.php
      return;
      }

      php_json_encode(&buf, parameter, options TSRMLS_CC);

      ZVAL_STRINGL(return_value, buf.c, buf.len, 1);

      smart_str_free(&buf);
      }
      /* }}} */

      这中实现在函数体内可以通过zend_parse_parameter的方式来获取传递进来的变量, 但这样只能获取到变量的值. 却无法得到其他更多的信息.,我们往低层看看在PHP中函数是怎么调用,参数是怎么传递的.

      3.1 PHP中函数的调用

      在研究函数怎么调用之前, 我们需要看看PHP代码是怎么执行的.

      大致可以分为2个步骤:

      - 词法分析,语法分析然后编译成opcode

      - 执行opcode

      PHP函数的执行也只能在opcode执行阶段执行.

      这里之前要介绍一个查看OPCODE的绝佳工具 vld(http://pecl.php.net/package/vld)

      装好这扩展。可以在命令行下查看php脚本编译后的opcode

      我们看看下面这个php脚本被编译后opcode是什么样的.

      $str = "http://reeze.cn";
      $len = strlen($str);

      $len2 = strlen($str2=10);

      echo $len;
      echo $len2;
      ?>
      $ php -dvld.active=1 func_call.php


      也可以再增加一个参数  -dvld.verbosity=3, 这样将会显示更多的信息.

      它被编译为上面的10条opcode命令.

      op的名称一看也能看出什么意思. .. 其中以 “!”开头的数字表示编译后的变量,, 以”~”开头的变量表示零时变量.

      上面可可以看出如果函数调用存在参数的话,在DO_FCALL之前会执行SEND_VAR 或者  SEND_VAR_NO_REF指令。并且这些指令后面操作的是编译过变量或者一个临时变量.

      在PHP中调用时我们是可以访问到DO_FCALL这个操作的opcode信息 。可以通过 EG(active_opline_ptr) 获取到当前指令

      PHP中存在一系列*G宏, EG 则为在执行opcode时的全局变量。
      见文件: $PHP_SRC/Zend/zend_globals.h

      struct _zend_executor_globals {
      // ...
      zend_op **opline_ptr; // 指向当前正在执行的zend_op对象
      HashTable *active_symbol_table;
      HashTable symbol_table; /* main symbol table */
      HashTable included_files; /* files already included */
      jmp_buf *bailout;
      int error_reporting;
      int orig_error_reporting;
      int exit_status;
      zend_op_array *active_op_array;
      // ...
      };

      struct _zend_op_array {
      /* Common elements */
      zend_uchar type;
      char *function_name;
      zend_class_entry *scope;
      zend_uint fn_flags;
      union _zend_function *prototype;
      zend_uint num_args;
      zend_uint required_num_args;
      zend_arg_info *arg_info;
      zend_bool pass_rest_by_reference;
      unsigned char return_reference;
      /* END of common elements */

      zend_bool done_pass_two;

      zend_uint *refcount;

      zend_op *opcodes; // zend_op数组.
      zend_uint last, size;

      zend_compiled_variable *vars; // 所有编译后的变量信息Since PHP5.1 这是一个数组
      int last_var, size_var; // last_var 最后一个编译变量的索引

      // ...
      };


      当前执行的op_array中保存所有编译变量的信息, 再看看zend_compiled_variable的结构吧。

      typedef struct _zend_compiled_variable {
      char *name;
      int name_len;
      ulong hash_value;
      } zend_compiled_variable;

      这正是我想获取的变量名称.

      我们可以通过全局变量EG(opline_ptr)指针获取到当前执行的zend_op, zend_op的结构如下:

      struct _zend_op {
      opcode_handler_t handler; // 处理该OPCODE的处理函数
      znode result; // 该opcode执行的结果
      znode op1; // 有的opcode需要1个,有的需要两个操作数。
      znode op2;
      ulong extended_value;
      uint lineno;
      zend_uchar opcode; // 该opcode的值 见$PHP_SRC/Zend/zend_vm_opcodes.h
      };

      这也就是我们函数调用时执行的opcode.我们现在可以获取到DO_FCALL时的opcode,  通过VLD察看opcode工具很容易就知道函数调用之前,如果函数有参数的话,在DO_FCALL之前一定有SEND_VAR或者SEND_VAR_NO_REF指令, 指针后退一个则一定是指向SEND_VAR或SEND_VAR_NO_REF指令的。 这样的话我们根据DO_FCALL获取到的zend_op指令后退不久可以获取SEND_VAR指令了么. SEND_VAR指令会操作compiled_var,这样我们就能得到变量的信息了..

      看看znode都有哪些信息:

      typedef struct _znode {
      int op_type;
      union {
      zval constant;
      zend_uint var; // 这个var就是当前变量在zend_op_array.vars 中的compiled_variable数组中的索引.不过这个索要并不是字面上的. 详情请看最后的代码实现.
      zend_uint opline_num; /* Needs to be signed */
      zend_op_array *op_array;
      zend_op *jmp_addr;
      struct {
      zend_uint var; /* dummy */
      zend_uint type;
      } EA;
      } u;
      } znode;

      如在在上面的注释. 通过获取znode.u.var的值就可以获取到变量的信息了.

      这样的话.程序的实现也就简单了.

      下面是实现:

      /* {{{ get_var_name
      *
      * 这个扩展要求PHP >= 5.1
      * 因为依赖PHP 5.1引入的compiled variable
      *
      * 在PHP空间导出一个get_var_name函数.
      * echo get_var_name($var_name); // expect: var_name
      * echo get_var_name($lineno=100); // expect: lineno
      */
      PHP_FUNCTION(get_var_name)
      {
      int len;
      char *strg = "";

      if(ZEND_NUM_ARGS() < 1) {
      return;
      }

      /* 显示所有的编译变量
      int i;
      zend_compiled_variable *vars = EG(active_op_array)->vars;
      for(i=0; i < EG(active_op_array)->last_var; ++i) { // last_var 最后一个编译变量的索引
      spprintf(&strg, 0, "%snVar:%sn", strg, EG(active_op_array)->vars[i].name);
      ++vars;
      }
      */

      zend_op *pre_opline_ptr = *EG(opline_ptr);
      pre_opline_ptr--;

      // 支持这类的调用: get_var_name($a="VALUE"); // expect: a
      // 这里增加在赋值的情况下也能正确返回变量的名字的处理方法, 如果方法参数是赋值的的话, 编译的OPCODE 中SEND_VAR之前将会
      // 有一个ZEND_ASSIGN 操作, 并且ZEND_ASSIGN操作的返回值被使用.比如: $c = $d + 1; $d + 1的返回值就被使用了. 就可以确认
      // 是前面的调用方式
      zend_op *pre_pre_online_ptr = pre_opline_ptr - 1;
      if(pre_pre_online_ptr && pre_pre_online_ptr->opcode == ZEND_ASSIGN && !(pre_pre_online_ptr->result.u.EA.type & EXT_TYPE_UNUSED)) {
      // 通过赋值之前的zend_op来获取变量信息
      pre_opline_ptr = pre_pre_online_ptr;
      }

      int index;
      // 比如get_var_name($name); 这时SEND_VAR OPCODE的op1操作数类型就是IS_CV 也就是IS Compiled Variable
      // 只有compiled variable才是直接存储索引的. PHP >= 5.1
      if(pre_opline_ptr->op1.op_type == IS_CV) {
      index = pre_opline_ptr->op1.u.var;

      }
      else {
      // 请参考VLD的源代码 $VLD_SRC/srm_oparray.c LINE:320 vld_dump_znode函数
      index = pre_opline_ptr->op1.u.var / sizeof(temp_variable);
      }

      zend_compiled_variable var = EG(active_op_array)->vars[index];
      len = spprintf(&strg, 0, "%s", strg, var.name);

      RETURN_STRINGL(strg, len, 0);
      }
      /* }}} */

      点击这里下载代码

在PHP中检查PHP文件是否有语法错误

之前在当当的时候的一个项目中用到了一个简单的模板引擎,其实也是借鉴discuz来做的模板引擎,很简单,它所作的事情就是把一些自定义的标签编译成php代码。已经说了很简单了,所以编译的时候也名优进行模板语法的检查,那么在开发过程中就会出现编译出来的php文件有语法问题,有语法问题没有关系,我修改重新编译一下就好了。首先不能在每次请求的时候都把php模板重新编译一下,会严重影响性能,折中的处理时在每个编译好的php文件末尾检查一下该模板文件是否已经修改过,根据设定的更新频率,如果又需要则重新编译模板文件,现在的问题是编译出来的php文件自己有语法错误,根本执行不到模板检查那一步,所以即使修改了模板文件中的问题也不会重新编译。 所以我想寻找一种简单的方法来检查生成的php文件是否合法。不合法就重新编译,这样开发过程中就不用出现错误就得手动删除缓存文件了。

在网上找了一下。刚开始以为 token_get_all()函数能处理语法错误的问题,结果发现,它只是做简单的词法分析。没有办法。后来到论坛上去问了一下

http://groups.google.com/group/professional-php/browse_thread/thread/b8581f6b07b10ff0/2601a63c406bb1c1?lnk=gst&q=reeze#2601a63c406bb1c1

有人告诉我有这样一个函数 php_check_syntax() http://www.php.net/manual/en/function.php-check-syntax.php 我想问题就这么坚决了。。我真应该RTF(Read The Fuck Mannual). 仔细一看。这个函数已近被弃用了:
Note: For technical reasons, this function is deprecated and removed from PHP. Instead, use php -l somefile.php from the commandline.

这个technical reason 到底是什么呢? 先不管了,以后再慢慢研究,反正不能使用这个方法就对了。
他们的建议是使用命令行$php -l filename.php 来检查语法。
Gary Every给了我一个代码片段参考:

在命令行下检查问题也不大。如果我要放在在线应用呢? 这就涉及到可移植性的问题了。首先是操作系统,然后就是环境变量。这样的话就会依赖于服务器端的配置。在http://www.php.net/manual/en/function.php-check-syntax.php 上有人贴出了自己的php_check_syntax()函数实现。
有的采用的就是上面的命令行的方法。
后面有提到使用eval的方法来验证。eval方法会执行传入的代码, 如果代码有语法错误则会抛出parser error, 可以使用’@'错误抑制符去掉错误信息,eval和echo一样并不是函数,不能使用变量函数的方法调用比如:
$func = ‘eval’
$func()这样的调用就是无效的。它会提示没有eval函数,如果你自己定义这么一个函数也是有问题的。因为eval是一个关键字。
eval调用和include差不多,如果被包含文件中没有明确return就返回null。如果直接eval我们需要检查的文件会造成被检查的文件内代码被执行,这可不是我们想要的,我们只需要检查一下这个文件的语法是否正确。 我们可以在要检查的文件之前添加return 语句,让代码提前跳出,那么后面的代码就不会执行了。好的,就这么干。代码如下:
checker.php

if(!function_exists('php_check_syntax')) {
function php_check_syntax($file_name, &$error_message = null) {
$file_content = file_get_contents($file_name);

$check_code = "return true; ?>";
$file_content = $check_code . $file_content . "

if(!@eval($file_content)) {
$error_message = "file: " . realpath($file_name) . " have syntax error";
return false;
}

return true;
}
}

if(!php_check_syntax("file.php", $msg)) {
echo $msg;
}
else {
echo "Woohoo, OK!";
}

file.php

foreach:: a => b
?>

因为Parse error 是没法被 set_error_handler处理函数处理的。这个异常没办法catch到。所以才使用了@来抑制错误。这带来的问题就是我们无法得到详细的错误信息。 不过目前我需要的功能也只是检查语法是否正确。不正确的话重新编译模板文件,就这么简单,至于语法错误,在显示网页的时候自然会看得到。
最好的办法就是这个被遗弃的php_check_syntax这个方法回到php中。下次再研究下他们是出于什么原因把这个函数去掉的。

实现一个简单在线HTML编辑器

一直没有仔细研究过在线HTML编辑器,以前以为编辑功能很复杂,需要用大量的JavaScript来模拟编辑器的效果,以前都是使用一些开源的HTML编辑器,HTML在各网上随处可见,发表文章,评论。最近自己想做一个类似Things这样的Web版的应用,需要一个想Google  Notebook(可惜的是现在已经停止开发了) 那样的编辑功能,看看现在网上的这些编辑器都庞大了,都是一些自己根本用不到的功能,其实我的需求很简单:简单的编辑既可以,并且需要轻量级一些,因为页面上可能需要开很多个编辑器实例。 所有想自己也来研究一下,看看能不能自己开发一个。

几天前花了一个晚上用firebug看了下Google Docs是怎么做。第一个遇到的问题就是如何让光标停在鼠标点击所在得地方。我刚开始一味都是js模拟出来的,这得有多复杂啊。并且还要兼容各个浏览器,天啊!后来上网一搜发现,浏览器早就想到了我们会有这样的一个需求。其实很简单,两条语句就可以说明HTML编辑器的最为核心的部分:

document.designMode = 'On';
document.contentEditable = true;

参考Mozilla上的这篇文章,介绍了HTML编辑的基本信息,要自己DIY一个常用功能的HTML编辑,这篇文章已经够你用的了。

在你的网页中嵌入这两条语句试试看:),你就会发现你的网站整个得都变的可以编辑了。可以随便乱修改。 不过放心,这样修改并不会破坏你的网站, 当然你也不希望你的整个网站是可以编辑的。例如我们只希望别人发布一条评论,只希望评论输入框可以输入。 要实现这样的效果可以有两种做法:

第一种就是使用直接让某个元素变成可编辑的例如:

index.html

Test TextEditor

Hi, HTML Editor!

Your comment



点击该区域后该区域的内容就变成可以编辑的了,这是我们就是对其进行简单的编辑。你可能会觉得直接编辑的功能太简单了,比如想要插入链接或者图片什么的。就没有办法了。这些功能浏览器并没有帮我们做好,不过实现这些功能也不麻烦, 参考上面Mozilla的文档。浏览器都提供了常用的功能API。

一般的编辑器都会提供一个工具栏之类的按钮来编辑文本内容。比如我使用的WordPress提供的编辑器:

wordpress提供的编辑器

wordpress提供的编辑器

可以对文字内容进行操作,加粗下划线,字体,对齐等等,并且提供可视化以及HTML编辑模式。

这个和FCKeditor 以及tinymce之类的编辑器使用的编辑方式和上面我提到的直接编辑html对象的方法不一样,他们使用的是iframe,使用iframe有很多好处,iframe中的文档和当前文档并不会因为样式或者HTML结构而影响到彼此,所以大部分的编辑器都是使用这种方式。它们基本的方式都是:

  1. 在页面中使用一个不可见的字段比如:“input, textarea”之类表单字段,他们的值就是需要编辑的内容。
  2. 页面初始化好,比如载入编辑器相关的一些脚本,也是就是window.onload好以后。创建一个iframe来显示可编辑区域,iframe初始化好以后读取父窗(相对于这个iframe)口的这个不可见表单值的内容,使之成为iframe中的body的html,也就是把所有需要编辑的内容添加到iframe中
  3. iframe中的内容初始化好以后,在iframe中执行上面提到的:

    document.designMode = 'On';
    document.contentEditable = true;

    把iframe整个窗口变成可以编辑的
  4. 进行编辑,这时候的编辑可能需要一个工具栏,基于同样的原因,一般工具栏也会是一个iframe,显示它们自己的编辑按钮。编辑是就利用浏览器提供的接口来对ifame中的内容进行编辑
  5. 父窗口中提交表单之前或者你需要的地方需要把编辑器中编辑完的内容回写到你的表单字段中去,否则编辑结果没有保存写来就没有意义了。

基本原理就是这样。要做出这样一个东西来,需要的就是一些细活了。要想做出一个FCk这样好用的编辑器也不是那么简单的。但是至少我们知道它是怎么运作的。 这就够了。

花了点时间做了一个简单的编辑器,真正要用的话很多的细节还是需要好好处理的,代码没有怎么清理,是变想边写,不是很完整。

需要的同学可以下来参考参考。
猛击  >> 这里 << 下载代码。

基于var_export 和 include返回值的缓存方案

前一篇文章我们研究了include调用返回值的问题,并指出可以通过这种方式来完成序列化相同的功能,现在我就来研究一下这种方法的可行性和效率,因为直接的返回php值肯定是比unserialize()函数要快。

第一步我们来研究下怎么将php对象持久化的保存起来。下面是我定义的一些变量:


private $_var;
public $pub = array('pub value', 3, 4);
public function __constructor($var)
{
$this->_var = $var;
}

public function show()
{
echo $this->_var;
}
}

$string = "It's a string...";
$array = array(1, 2, 'key' => 'value', array('sub-array'));
$number = 135345.55;
$class = new MyClass('class var');

//通过serialize()方法我们可以将他们持久化比如:
echo serialize($string); //s:16:"It's a string...";
echo serialize($array); //a:4:{i:0;i:1;i:1;i:2;s:3:"key";s:5:"value";i:2;a:1:{i:0;s:9:"sub-array";}}
echo serialize($number); //d:135345.5499999999883584678173065185546875;
echo serialize($class); //O:7:"MyClass":2:{s:13:"MyClass_var";N;s:3:"pub";a:3:{i:0;s:9:"pub value";i:1;i:3;i:2;i:4;}}
// 我们可以将这些序列化的结果存到文件中,在需要的时候unserialize()返回得到相应的值,但是现在我不会这么做。

前篇文章提到了通过include返回值来直接取得php值对象,首先我们要把值保存起来,因为我们要通过include来包含它,首先遇到的问题就是我们的序列化函数必须要生成合法的php表达式才行,否则include是无法得到相应的返回值的
比如我们要序列化 字符串 “abcd” 我们可以这么做

file_puts_content("data.php", "return 'abcd';");
//然后这样取得相应的值
$string = include "data.php";
echo $string; // 它应该输出 abcd

那数组怎么办呢?比如上面的数组。我们可以自己编写这个序列化函数

function encode($var){
if (is_array($var)) {
$code = 'array(';
foreach ($var as $key => $value) {
$code .= "'$key'=>".encode($value).',';
}
$code = chop($code, ','); //remove unnecessary coma
$code .= ')';
return $code;
} else {
if (is_string($var)) {
return "'".$var."'";
} elseif (is_bool($var)) {
return ($var ? 'TRUE' : 'FALSE');
} elseif (is_numeric($var)) {
return "$var";
}
else
{
return 'NULL';
}
}
}

这个函数可以将字符串,数组以及数字变成合法的php表达式。
比如:
file_put_contents(“data.php”, “<?phpn return ” . encode($array) . “;n”);
data.php文件的结果是:

return array ( 0 => 1, 1 => 2, 'key' => 'value', 2 => array ( 0 => 'sub-array', ), )array(4) { [0]=> int(1) [1]=> int(2) ["key"]=> string(5) "value" [2]=> array(1) { [0]=> string(9) "sub-array" } }

我们的目的达到了。可以直接的通过include这个文件来得到我们的值。
但是现在有个问题,我们没有序列化对象类型的值,这个该怎么处理呢?
一个类对象有对象的状态和对象的行为,行为在类定义完以后就确定了,所以每个类的实例的行为都是一样的。所以我们可以不考虑,我们只需要考虑类对象的状态就可以了,简单来讲就是类的属性状态需要保存起来。那怎么样得到一个类的属性呢?
经过一番搜寻以后发现一个函数
get_object_vars
(PHP 4, PHP 5)get_object_vars — 返回由对象属性组成的关联数组
这个函数可以获得对象的属性关联数组,也就只可以得到对象的状态,但是对象的属性有各种访问控制,get_object_vars()函数在对象外访问只能得到对象的公开属性,而无法得到私有属性,这样的话我们就无法得到对象的全部状态,不可行,但是在对象内可以得到对象的所有属性,那我们可不可以在对象内定义一个___get_properties()方法来返回这些状态呢。
给类增加这样一个方法

public function __get_properties()
{
return get_obj_vars($this);
}

这样我们就可以得到类的所有属性了。第一步算是完成了,我们得到状态该怎么重新恢复出来呢?要在对象外部给对象设置属性我们只有两种情况:一种是这个属性是公开属性,我们可以直接赋值 比如: $obj->prop = $value; 如果是私有属性我们则需要自己增加setter方法比如 setProp($value);方法,来设置。但是这就会遇到一个问题,我们的属性要么是公开属性,要么必须要有setter方法来设置,很多情况下我们不希望给类增加这么多没有实际用处的方法,也为了封装性,不会有这么多的setter方法。虽然我们能得到对象的状态,但是却无法恢复对象状态,这样的话,我们的序列化方法也就没有什么意义了。我们探索到现在算是失败了。
解决办法:var_export()函数。
在看symfony代码的时候发现了这个函数,手册是这么描述的:
———
var_export
(PHP 4 >= 4.2.0, PHP 5)var_export — 输出或返回一个变量的字符串表示
描述
mixed var_export ( mixed expression [, bool return] )

此函数返回关于传递给该函数的变量的结构信息,它和 var_dump() 类似,不同的是其返回的表示是合法的 PHP 代码。

您可以通过将函数的第二个参数设置为 TRUE,从而返回变量的表示。

这个就是我们想要的那个函数,我们来看看这个函数是怎么使用的。

//变量继续使用上面定义的变量
echo var_export($string); //'It's a string...'
echo var_export($array); /* array (0 => 1, 1 => 2, 'key' => 'value', 2 => array (0 => 'sub-array', ),)*/
echo var_export($number); // 135345.55
echo var_export($class); /* MyClass::__set_state(array('_var' => NULL, 'pub' => array (0 => 'pub value', 1 => 3, 2 => 4)) */

我们可以看到生成的都是合法的PHP表达式。通过设置第二个参数为true,就可以将返回结果赋值给变量比如$new_array_string = var_export($array, TRUE);然后将这个结果写入文件持久化

file_put_contents(“data.php”, “public function __constructor($var)
{
$this->_var = $var;
}

public function show()
{
echo $this->_var;
}
public static function __set_state(array $array)
{
$tmp = new MyClass();
foreach($array as $key => $value)
{
$this->$key = $value;
}
}
}

// 一些变量
$string = “It’s a string…”;
$array = array(1, 2, ‘key’ => ‘value’, array(‘sub-array’));
$number = 135345.55;
$class = new MyClass(‘class var’)

// 缓存
cache(“string.data.php”, $string); // 当然扩展名不一定非得php, 文件名我也只是简单的处理
cache(“array.data.php”, $array);
// 等等。。。

// 获取数据,当然也可以在其他文件中来获取。
$class = cache_get(“class.data.php”);

参考:http://www.thoughtlabs.com/2008/02/02/phps-mystical-__set_state-method/?dsq=12016456

PHP中include语句的返回值及避免序列化开销的方法

以前乱翻symfony生成的缓存文件的时候看到很多类似:


"value"); ?>

这种表达式,当时并没有怎么在意,今天研究symfony代码的时候看到这样一句代码


classes = include($file); ?>;

一直都是通过include require来包含文件,但是从来没有使用过他的返回值,印象中include返回的无非是true或者false吧。今天自己看了下文档,发现在php文件中是可以直接调用return的。比如:


// return.php
$value = array('haha', 1, 3);
return $value;
?>
// get_return.php
$value = include("return.php");

echo $value; // 输出 Array, 因为$value 是从return.php返回的一个数组
?>

其实项目中很少情况需要这样的返回方法。如果想要从return.php中得到返回值一般是通过调用return.php中所调用的函数来得到。

在symfony中这种方式就很合理,如果大家熟悉symfony的话,应该知道,symfony运行起来以后会在cache目录下生成系统配置文件的缓存 ,诚然可以通过序列化的方式来缓存这些信息,但是反序列化是需要消耗资源的。通过这种方式来做持久化也是个不错的选择.

PHP5.2.6中无法在exception_handler函数中抛出异常

在PHP bugs列表中也找到这个bug,但是似乎没有被处理,bug提出的时间是2005年,不知道新版本的有没有解决。
PHP:5.2.6
OS: Mac OS Leopard 10.5.7
Server: Apache 2.2

这个代码就有问题:
function e_handler($e)
{
throw new Exception();
}
set_exception_handler(‘e_handler’);
throw new Exception();

这将会导致
Fatal error: Exception thrown without a stack frame in Unknown on line 0

Update: 这应该属于设计问题,如果在exception_handler()函数中可以抛出异常,而这个异常又会继续调用exception_handler(),这样下去就会出现死循环,这就是为什么程序会出错的原因吧。
建议在exception_handler函数体内的代码包在try catch中,避免意外抛出异常导致出这个问题

function e_handler($e)
{
try
{
throw new Exception();
}
catch (Exception $e)
{
echo “catched…”;
}
}
set_exception_handler(‘e_handler’);

throw new Exception();

PHP调试函数

在项目中经常要调试程序,但是我电脑上的ZendStudio总是没配置好,不能单步调试,不过有时候不一定需要让ZendStudio来帮我们调试,所以写了下面这个辅助函数来方便调试,因为有时候调试的位置加多了自己也不知道到底是加在什么地方了,下面的函数就是方便的dump对象信息,同时显示调试的问题和所在的行数。


//调试函数,方便显示调试函数的位置和文件
function p(){
$args = func_get_args();

// 调用栈,debug_backtrace()可以返回调用栈。这样 我们就可以方便的知道函数在哪里调用的。
$backtrace = debug_backtrace();

$file = $backtrace[0]['file'];
$line = $backtrace[0]['line'];
echo "

";
  echo "$file:$linen";
  foreach ($args as $arg)
  {
    var_dump($arg);
  }
  echo "

";
exit;
}