php pdo bindValue / bindParam 中不能含有连字符

最近在使用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”的长度,证明了我们的说法,我们的占位符被截断了。结束。

 

当然,正确的变量名命名规则,是不能出现“-”的,所以你有好的命名习惯,是完全可以避免这个问题的。希望对你有所帮助。

 



posted @ 2017-07-27 10:33  狄磊  阅读(2139)  评论(0编辑  收藏  举报