專案中有個功能是比較會員是否過期,review同事的程式碼,發現其寫法比較奇葩,但線上竟也未出現bug。
實作:
-
- $expireTime = "2014-05-01 00:00:00";
- $currentTime = date('Y-m-d H:i:s' , time());
-
- if($currentTime return false;
- } else {
- return true;
- }
-
-
-
-
- 複製程式碼
-
-
- 如果兩個時間需要比較,通常是轉換成unix時間戳,用兩個int型的數字比較。該實作卻刻意將時間表示成string,然後對兩個string進行比較運算。
-
撇開寫法不談,我很好奇的是php內部是如何比較的。
閒話少說,還是從原始碼開始追蹤。
編譯期
在zend_language_parse.y中可以發現類似下述語法:
- expr === expr { zend_do_binary_op(ZEND_IS_IDENTICAL, &$$, &$1, &$3 TSRMLS_CC); }
- zend_do_binary_op(ZEND_IS_NOT_IDENTICAL, &$$, &$1, &$3 TSRMLS_CC); }
- expr == expr { zend_do_binary_op(ZEND_IS_EQUAL, &$$, &$1, & zend_do_binary_op(ZEND_IS_EQUAL, &$$, &$1, &$3 TSRM_CC); zend_do_binary_op(ZEND_IS_NOT_EQUAL, &$$, &$1, &$3 TSRMLS_CC); }
- expr expr > expr { zend_do_binary_op(ZEND_IS_SMALLER, & >, &$3, &$1 TSRMLS_CC); ZEND_IS_SMALLER_OR_EQUAL, &$$, &$3, &$1 TSRMLS_CC); }
-
-
- 複製程式碼
-
-
- 很明顯,這裡編譯成opcode用的便是zopcode用的便是zop。
-
-
void zend_do_binary_op(zend_uchar op, znode *result, const znode *op1, const znode *op2 TSRMLS_DC) /RM> zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC); opline->opcode = op; opline->result.op_type = IS_TMP_Rult_TMP_Rult. .var = get_temporary_variable(CG(active_op_array)); opline->op1 = *op1; opline->op2 = *op2; *result = opline->result ;- }
- >
-
-
- 複製程式碼
-
-
- 函數並沒有做什麼特別的處理,只是簡單保存了opcode、運算元1和操作數2。
-
- 執行期
根據opcode,跳到對應的處理函數:ZEND_IS_SMALLER_SPEC_CONST_CONST_HANDLER。
-
-
-
-
-
- static int ZEND_FASTCALL ZEND_IS_SMALLER_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANivER_ARGS) zval *result = &EX_T(opline->result.u.var).tmp_var;
compare_function(result, &opline->op1.u.constant, &opline->op2.u.constant TSRMLS_CC) ; ZVAL_BOOL(result, (Z_LVAL_P(result) ZEND_VM_NEXT_OPCODE(); } -
-
-
- 注意到,兩個zval的比較是利用compare_function來處理。
-
-
-
-
-
- ZEND_API int compare_function(zval *result, zval *op1, zval *op2 TSRMLS_DC) /* {{{ */
- /
- int converted = 0;
- zval op1_copy, op2_copy;
- zval *op_free;
-
- while (1) {
- switch (TYPE_PAIRffidf ( op2))) {
- case TYPE_PAIR(IS_LONG, IS_LONG):
- ...
- case TYPE_PAIR(IS_DOUBLE, IS_LONG):
- ...
- case TYPEDOUB_IS_y_
- ...
- ...
- // 兩個字串進行比較
case TYPE_PAIR(IS_STRING, IS_STRING): zendi_smart_strcmp(result, op1, op2); return SUCCESS; ... } }} 複製程式碼此函數例舉了若干種情況,根據本文case,進入zendi_smart_strcmp一窺究竟:
-
- ZEND_API void zendi_smart_strcmp(zval *result, zval *s1, zval *s2) /* {{{ */
- { 🎜> , ret2;
- 長 lval1, lval2;
- double dval1, dval2;
-
- // 嘗試將字串轉成數字型別
- if ((ret1=is_numeric_string(Z_STRVAL, Z_STRLEN_P(s1), &lval1, &dval1, 0)) &&
- (ret2=is_numeric_string(Z_STRVAL_P(s2), Z_STRLEN_P(s2), &lval2, &dval2, 0))) {比較
- ...
- } else {
- // 無法全部轉成數字
- // 則呼叫zend_binary_zval_strcmp
- // 本質為memcmp的一層封裝
- Z_LVAL_🎜> // 本質為memcmp的一層封裝
- Z_LVAL_P(result)_P(result) = zend_binary_zval_strcmp(s1, s2);
- ZVAL_LONG(result, ZEND_NORMALIZE_BOOL(Z_LVAL_P(result)));
- }
- }
-
-
那麼「2014-05-01 00:00:00」能否轉換成數字麼?
還是得看下is_numeric_string的實作規則。
static inline zend_uchar is_numeric_string(const char *str, int length, long *lval, double *dval, int allow char *ptr;
複製程式碼🎜>
程式碼比較長,不過仔細閱讀,str轉num的規則還是很清楚的。
尤其註意的是allow_errors這個參數,它直接決定了本例中無法將「2014-05-01 00:00:00」轉換成數字。
因而最後其實「2014-04-17 00:00:00」
既然是memcmp,便不難理解為何文章開始提到的寫法也能正確運作。
容錯轉換
何時allow_errors為true呢?一個極好的例子就是zend_parse_parameters,zend_parse_parameters的實作不再細述,有興趣的讀者可以自行研究。其中呼叫is_numeric_string時將allow_errors置為了-1。
舉個例子:
-
- static void php_date(INTERNAL_FUNCTION_PARAMETERS, int localtime)
- {
- charformatts;
- char *string;
-
- // 期望的第二個參數為timestamp,為long
- // 假設上層呼叫時,誤傳入了string,那麼zend_parse_parameters依然會盡可能的嘗試將string解析為long
- if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l", &format, &format_len, &ts) == FAILURE) {
- RETURN_FALSEd; ) == 1) {
- ts = time(NULL);
- }
-
- string = php_format_date(format, format_len, ts, localtime TSVALLS_CC); }
-
-
-
- 複製程式碼
-
-
- 這是php的date函數內部實作。
在呼叫date時,如果將第二個參數傳入string,效果如下:
echo date('Y-m-d', '0-1-2'); // 輸出 PHP Notice : A non well formed numeric value encountered in Command line code on line 1 1970-01-01-
-
-
- 複製代碼
-
-
-
複製代碼
雖然報出🎜>等級的錯誤,但仍成功將'0-1-2'轉成了0
|