最近在使用pdo時,bindValue的第一個參數中有一個“-”,就觸發了這個bug,
<?php $dsn = 'mysql:dbname=cm_code;host=127.0.0.1'; $user = 'root'; $password = 'Dsdsd56'; try { $dbh = new PDO($dsn, $user, $password); } catch (PDOException $e) { echo 'Connection failed: ' . $e->getMessage(); } $name = "sdsds"; $phone = '13522222264'; $sth = $dbh->prepare('SELECT username, phone FROM user WHERE username =:userss-name'); $sth->bindValue(':userss-name', $name, PDO::PARAM_STR); $sth->execute(); var_dump($sth->fetchAll());exit;
運行上面的代碼,會出現
PHP Warning: PDOStatement::execute(): SQLSTATE[HY093]: Invalid parameter number: parameter was not defined
Warning: PDOStatement::execute(): SQLSTATE[HY093]: Invalid parameter number: parameter was not defined
雖然很快就定位到了bug的原因,是含有“-”導致的,但是還是很不爽,為什么命名占位符中不能含有“-”呢?
當然第一就是google了,發現他是php很早之前的bug(PDO Common: Bug #43130 (Bound parameters cannot have - in their name)),也修復了(當出現“-”時,就報waring);
https://bugs.php.net/bug.php?id=43130
下面是pdo作者的回復
[2007-10-30 09:51 UTC] uw@php.net
I disagree with the decision to allow "-" in parameter names. Parameter names should consist of [a-zA-Z] and nothing else. "-" is an operator in most databases. For BC compatibility I'm also fine with the old pattern [:][a-zA-Z0-9_]+ . Though I must say, that I'd prefer [:][a-zA-Z]+[a-zA-Z0-9_]+, don't allow ":0". ":0" looks a bit like "operator" + "number"... However, the underlying problem here is that there is absolutely no specification for PDO. This makes PDO a guessing game and error prone.
大意是,他不允許變量名中出現‘-’,而且在大部分數據庫中‘-’是作為運算符使用的。
但是,感覺還是如鯁在喉啊,他只是一個占位符,字符串,和“-”有什么關系呢,匹配替換不就行了嗎? 但是網上沒有對此的回答,所以只能去看源碼
pdo的實現是在 phpsrc/ext/pdo/pdo_stmt.c 中,找到 execute 方法。讀代碼后找到可疑的地方, 在495行
495 ret = pdo_parse_params(stmt, stmt->query_string, stmt->query_stringlen, &stmt->active_query_string, &stmt->active_query_stringlen);
pdo_parse_params方法的實現是在 pdo_sql_parser.c 中, 解析參數就在這個while循環中
/* phase 1: look for args */ while((t = scan(&s)) != PDO_PARSER_EOI) { if (t == PDO_PARSER_BIND || t == PDO_PARSER_BIND_POS) { if (t == PDO_PARSER_BIND) { int len = s.cur - s.tok; if ((inquery < (s.cur - len)) && isalnum(*(s.cur - len - 1))) { continue; } query_type |= PDO_PLACEHOLDER_NAMED; } else { query_type |= PDO_PLACEHOLDER_POSITIONAL; } plc = emalloc(sizeof(*plc)); memset(plc, 0, sizeof(*plc)); plc->next = NULL; plc->pos = s.tok; plc->len = s.cur - s.tok; plc->bindno = bindno++; if (placetail) { placetail->next = plc; } else { placeholders = plc; } placetail = plc; } }
其中的秘密就在 上面的 scan方法中,方法實在是太長,static int scan(Scanner *s),只放個聲明吧,具體解析實現它是通過指針移動和goto來實現整個queryString的遍歷,當遇到非法字符的時候,比如“-”,就會斷開,所以最開始的代碼命名占位符,就變成了“:userss”了,這就是參數未定義的原因了。這也解決了我的疑問,他不是匹配替換的,而是逐個字符遍歷來實現解析的。
要更詳細了解請移步源代碼的實現。
static int scan(Scanner *s) { char *cursor = s->cur; s->tok = cursor; { YYCTYPE yych; if ((YYLIMIT - YYCURSOR) < 2) YYFILL(2); yych = *YYCURSOR; switch (yych) { case 0x00: goto yy2; case '"': goto yy3; case '\'': goto yy5; case '(': case ')': case '*': case '+': case ',': case '.': goto yy9; case '-': goto yy10; case '/': goto yy11; case ':': goto yy6; case '?': goto yy7; default: goto yy12; } ..............
當然,以上都是屬於結果部分了,不想深究的到此就可以了,想了解我是怎么定位找到的呢,接着往下看。
懷疑(斷點)和 行動(gdb)
來運行我們的我們的神器gdb,加載我們的代碼文件pdo.php
root@iZwz90qokcxygcqwhd7j6kZ ~]# gdb php GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-94.el7 Copyright (C) 2013 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-redhat-linux-gnu". For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>... Reading symbols from /data/server/php7/bin/php...done. (gdb) set args pdo.php
在我們懷疑的地方加斷點
(gdb) b pdo_parse_params Breakpoint 1 at 0x696aa0: file /data/src/php-7.0.18/ext/pdo/pdo_sql_parser.c, line 379.
然后執行 run
為了方便查看我們直接到while循環結束,當然你也可以一直next,看看到底是怎么執行的
(gdb) until 423 pdo_parse_params (stmt=stmt@entry=0x7fffeee85380, inquery=<optimized out>, inquery_len=<optimized out>, outquery=outquery@entry=0x7fffeee853e8, outquery_len=outquery_len@entry=0x7fffeee853f0) at /data/src/php-7.0.18/ext/pdo/pdo_sql_parser.c:424
然后打印存儲命名占位符的變量
(gdb) print *placetail $1 = {pos = 0x7fffeee71171 ":userss-name", len = 7, bindno = 0, qlen = 0, quoted = 0x0, freeq = 0, next = 0x0
發現了嗎? len長度是7,正好是“:userss”的長度,證明了我們的說法,我們的占位符被截斷了。結束。
當然,正確的變量名命名規則,是不能出現“-”的,所以你有好的命名習慣,是完全可以避免這個問題的。希望對你有所幫助。