上面的分析给N1的师傅看了一下,说是要仔细看一下find,漏了点东西,所以单独拎出来看看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
/**
* 查询数据
* @access public
* @param mixed $options 表达式参数
* @return mixed
*/
public function find($options = array())
{
if (is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options;
$options = array();
$options['where'] = $where;
}
// 根据复合主键查找记录
$pk = $this->getPk();
if (is_array($options) && (count($options) > 0) && is_array($pk)) {
// 根据复合主键查询
$count = 0;
foreach (array_keys($options) as $key) {
if (is_int($key)) {
$count++;
}
}
if (count($pk) == $count) {
$i = 0;
foreach ($pk as $field) {
$where[$field] = $options[$i];
unset($options[$i++]);
}
$options['where'] = $where;
} else {
return false;
}
}
// 总是查找一条记录
$options['limit'] = 1;
// 分析表达式
$options = $this->_parseOptions($options);
// 判断查询缓存
if (isset($options['cache'])) {
$cache = $options['cache'];
$key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options));
$data = S($key, '', $cache);
if (false !== $data) {
$this->data = $data;
return $data;
}
}
$resultSet = $this->db->select($options);
if (false === $resultSet) {
return false;
}
if (empty($resultSet)) {
// 查询结果为空
return null;
}
if (is_string($resultSet)) {
return $resultSet;
}
// 读取数据后的处理
$data = $this->_read_data($resultSet[0]);
$this->_after_find($data, $options);
if (!empty($this->options['result'])) {
return $this->returnResult($data, $this->options['result']);
}
$this->data = $data;
if (isset($cache)) {
S($key, $data, $cache);
}
return $this->data;
}
|
- 首先将当前对象的
$options
设置为空数组,而$options
的值为:string(1) “2”
- 首先通过
is_numeric()
和is_string()
函数对参数$option
进行检查。如果传入的参数是数字或者字符类型,就会假设这是要根据主键进行查询
1
2
3
4
5
6
7
8
9
10
|
/**
* 获取主键名称
* @access public
* @return string
*/
public function getPk()
{ echo "getPk方法被调用!!\r\n";
var_dump($this->pk);
return $this->pk;
}
|
(有些东西自己debug的时候加的)
这里返回的当前对象的主键
其实也就是将$options
赋值给了这个where[“id”]
- 如果传入的参数是数字或字符串类型,他会创建一个
$where
数组,并用主键作为键,传入的参数为值,然后这个$where
数组放入$options['where']
中
dump->$options[‘where’]:
1
2
3
4
|
array(1) {
'id' =>
string(1) "2"
}
|
其实就是交换了一下并且转化为数组的形式(我的理解)
- 接着就是将当前对象利用
getPk
方法获取当前对象的主键赋值给$pk
变量
dump(虽然知道和前面是一样的但是我还是写出来一下,主打一个仔细):
接下来进入下一个判断
- 判断
$options
是否为数组且不为空,并且检查$pk
是否为数组(也就是复合主键)
- 先将count赋值为0,利用
arrary_key
函数返回$options
数组中所有的键或键的子集(也就是里面的where和id吧)
- 如果某个键是整数(
is_int($key)
成立),则递增 $count
。
- 把增加完的
count
与$pk
进行比较判断是否数量相同
- 使用循环将复合主键字段和对应的值存储到
$where
数组中,并从 $options
中移除相应的元素。
- 将
$where
数组赋值给 $options['where']
,若前面的count不匹配则返回false
- 接下来设置了
$options[‘limit’] = 1
也就是只能返回一条记录
接下来就就是调用_parseOptions方法并且将结果赋值给$options
,然后我这里想到可能有些方法没有完全分析完,所以接下来我会粘贴出来之前分析的过程,然后在里面再补上我未分析的部分(考虑到这里直接加入的话来回切有点费眼睛,所以就这么办了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
/**
* 分析表达式
* @access protected
* @param array $options 表达式参数
* @return array
*/
protected function _parseOptions($options = array())
{
if (is_array($options)) {
$options = array_merge($this->options, $options);
}
if (!isset($options['table'])) {
// 自动获取表名
$options['table'] = $this->getTableName();
$fields = $this->fields;
} else {
// 指定数据表 则重新获取字段列表 但不支持类型检测
$fields = $this->getDbFields();
}
// 数据表别名
if (!empty($options['alias'])) {
$options['table'] .= ' ' . $options['alias'];
}
// 记录操作的模型名称
$options['model'] = $this->name;
// 字段类型验证
if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
// 对数组查询条件进行字段类型检查
foreach ($options['where'] as $key => $val) {
$key = trim($key);
if (in_array($key, $fields, true)) {
if (is_scalar($val)) {
$this->_parseType($options['where'], $key);
}
} elseif (!is_numeric($key) && '_' != substr($key, 0, 1) && false === strpos($key, '.') && false === strpos($key, '(') && false === strpos($key, '|') && false === strpos($key, '&')) {
if (!empty($this->options['strict'])) {
E(L('_ERROR_QUERY_EXPRESS_') . ':[' . $key . '=>' . $val . ']');
}
unset($options['where'][$key]);
}
}
}
// 查询过后清空sql表达式组装 避免影响下次查询
$this->options = array();
// 表达式过滤
$this->_options_filter($options);
return $options;
}
```
- 根据上面传递进来的值,dump出来看一下传入了哪些东西
全局的`$options`
```php
array(2) {
'where' =>
array(1) {
'id' =>
string(1) "2"
}
'limit' =>
int(1)
}
|
当前对象的options
:
1
2
3
4
|
array(1) {
'field' =>
string(7) "content"
}
|
接下来来分析逻辑
- 判断传入的
$options
是否是数组(当然这里直接看就知道是数组),使用array_merge
将当前对象的options
和传入的options
进行合并
合并之后:
1
2
3
4
5
6
7
8
9
10
11
|
array(3) {
'field' =>
string(7) "content"
'where' =>
array(1) {
'id' =>
string(1) "2"
}
'limit' =>
int(1)
}
|
- 当前没有传入表名,所以会自动通过
getTableName()
方法进行自动获取表名,这个方法上面已经分析过了,这里就不再次分析了
- (++)获取完表名之后将当前对象的
$field
赋值给了变量$fields
当前对象$fields
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
array(5) {
[0] =>
string(2) "id"
[1] =>
string(7) "content"
[2] =>
string(5) "title"
'_pk' =>
string(2) "id"
'_type' =>
array(3) {
'id' =>
string(7) "int(11)"
'content' =>
string(4) "text"
'title' =>
string(4) "text"
}
}
|
- (++)否则调用
getDbFields
方法来赋值
(不过这个地方的option也没办法传入表名呀,似乎好像不会跳到getdbfield方法里面.jpg)所以我还是看一下这个getDbFields
方法吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
/**
* 获取数据表字段信息
* @access public
* @return array
*/
public function getDbFields()
{
if (isset($this->options['table'])) {
// 动态指定表名
if (is_array($this->options['table'])) {
$table = key($this->options['table']);
} else {
$table = $this->options['table'];
if (strpos($table, ')')) {
// 子查询
return false;
}
}
$fields = $this->db->getFields($table);
return $fields ? array_keys($fields) : false;
}
if ($this->fields) {
$fields = $this->fields;
unset($fields['_type'], $fields['_pk']);
return $fields;
}
return false;
}
|
- 首先判断当前对象的表名(键名)是否设置了
- 若有设置:
- 判断 table是否是数组,获取数组的键作为表名,如果是字符串那么就直接设置为表名,如果里面包含了’(‘则认为是子查询直接返回false
- 使用数据库连接对象的
getFields
方法获取字段信息,这个方法在之前也分析过一次了
- 如果成功获取到字段信息,就返回字段名的数组,否则返回false
- 若没有设置:
- 检查当前对象的fields是否存在,如果定义了,就返回字段列表,移除
_type
and _pk
到这里都是新加入的分析。
- 判断[‘alias’]这个键值是否存在(也就是数据库别名) 就会将表名与别名结合起来进行查询(当然通过前面的dump可以看出来是没有别名的)
- 记录当前操作模型的名称传入
model
的值(Articles
)
- 判断
[where]
是否存在且是否为数组;[$field]
存在且[join]
不存在,则对数组查询条件进行字段类型检查
- 首先遍历
where
的值,先利用trim进行去除空格,判断$key
是否存在于$fields
之中(下面dump出来好分析),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
array(5) {
[0] =>
string(2) "id"
[1] =>
string(7) "content"
[2] =>
string(5) "title"
'_pk' =>
string(2) "id"
'_type' =>
array(3) {
'id' =>
string(7) "int(11)"
'content' =>
string(4) "text"
'title' =>
string(4) "text"
}
}
|
(++)这里的进入判断类型之前还有一个判断,这里忘记说了,这里的in_array
检查$key
是否存在于数组fields
中,且true表示要执行严格的比较,比较完之后才要判断下面的东西
- 判断键名的值是否是标量(标量变量是包含int、 float、string或bool的变量。类型array、object、resources 和null不是标量。)很明显是的
- 进入_praseTyoe方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/**
* 数据类型检测
* @access protected
* @param mixed $data 数据
* @param string $key 字段名
* @return void
*/
protected function _parseType(&$data, $key)
{
if (!isset($this->options['bind'][':' . $key]) && isset($this->fields['_type'][$key])) {
$fieldType = strtolower($this->fields['_type'][$key]);
if (false !== strpos($fieldType, 'enum')) {
// 支持ENUM类型优先检测
} elseif (false === strpos($fieldType, 'bigint') && false !== strpos($fieldType, 'int')) {
$data[$key] = intval($data[$key]);
} elseif (false !== strpos($fieldType, 'float') || false !== strpos($fieldType, 'double')) {
$data[$key] = floatval($data[$key]);
} elseif (false !== strpos($fieldType, 'bool')) {
$data[$key] = (bool) $data[$key];
}
}
}
|
where
的键值传入了$data
(注意这里直接传的是地址&),键名key
传入$key
,很明显这里第一个if语句是成立的,我们直接看下面做了什么操作就行了
- 判断是什么类型(ENUM、INT、BOOL、DOUBLE、FLOAT)这里的枚举类型具有优先级,但是没有进行什么操作,int类型将值转化为整数,float和double转化为浮点数,bool类型还是转化为布尔型。
- 其实就是为了确保数据可以以正确形式写入数据库进行查询
回到_parseOptions方法
(++)
这里的elseif后面没有分析到
elseif的条件是:
- $key不是一个纯数字,而是一个字段名或者是表达式
- $key不能以下划线开头
- 确保不能包含点号
- 确保不能包含括号
- 确保不能包含
|
和 &
如果key不满足上面二的条件,那么配置中的strict
选项可能会报错,并且移除$key
- 查询完之后又将当前对象的
options
进行清空
- 后面的filter没有进行什么操作就不分析了,最后返回
$options
转化后的options
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
array(5) {
'field' =>
string(7) "content"
'where' =>
array(1) {
'id' =>
int(2)
}
'limit' =>
int(1)
'table' =>
string(8) "articles"
'model' =>
string(8) "Articles"
}
|
可以和前面原来的options对比一下看看是不是跟着逻辑走的
这里返回后回到find
方法
- 判断查询缓存是否存在,跟进后发现直接跳出去了,说明是不存在的也就是cache这个缓存不存在
(++)这里加一下,若存在的话会做什么操作:
- 将缓存配置保存到
$cache
变量中,如果缓存配置中设置了键名key
则将其作为缓存的键名,否则,使用序列化后的查询选项作为键名;从cache中获取数据赋值给$data
(调用了S()方法);如果成功就赋值给当前对象的data之中
- 接下来利用
select
方法进行查询返回给$resultSet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/**
* 查找记录
* @access public
* @param array $options 表达式
* @return mixed
*/
public function select($options = array())
{
$this->model = $options['model'];
$this->parseBind(!empty($options['bind']) ? $options['bind'] : array());
$sql = $this->buildSelectSql($options);
$result = $this->query($sql, !empty($options['fetch_sql']) ? true : false);
return $result;
}
|
- 同样的还是将
$options
设置成空数组(当前对象中),设置当前模型名称,进入parseBind
方法,进行参数绑定
1
2
3
4
5
6
7
8
9
10
|
/**
* 参数绑定分析
* @access protected
* @param array $bind
* @return array
*/
protected function parseBind($bind)
{
$this->bind = array_merge($this->bind, $bind);
}
|
- 接下来调用
buildselectsql
方法,生成查询sql语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/**
* 生成查询SQL
* @access public
* @param array $options 表达式
* @return string
*/
public function buildSelectSql($options = array())
{
if (isset($options['page'])) {
// 根据页数计算limit
list($page, $listRows) = $options['page'];
$page = $page > 0 ? $page : 1;
$listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20);
$offset = $listRows * ($page - 1);
$options['limit'] = $offset . ',' . $listRows;
}
$sql = $this->parseSql($this->selectSql, $options);
return $sql;
}
|
- 如果
$options
中包含page键,表示需要进行分页查询,提取出页数和每页的行数,根据他们计算出limit
字句,将其添加到$optionsp[’limit’]中,如果未设置每页行数,默认使用20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/**
* 替换SQL语句中表达式
* @access public
* @param array $options 表达式
* @return string
*/
public function parseSql($sql, $options = array())
{
$sql = str_replace(
array('%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'),
array(
$this->parseTable($options['table']),
$this->parseDistinct(isset($options['distinct']) ? $options['distinct'] : false),
$this->parseField(!empty($options['field']) ? $options['field'] : '*'),
$this->parseJoin(!empty($options['join']) ? $options['join'] : ''),
$this->parseWhere(!empty($options['where']) ? $options['where'] : ''),
$this->parseGroup(!empty($options['group']) ? $options['group'] : ''),
$this->parseHaving(!empty($options['having']) ? $options['having'] : ''),
$this->parseOrder(!empty($options['order']) ? $options['order'] : ''),
$this->parseLimit(!empty($options['limit']) ? $options['limit'] : ''),
$this->parseUnion(!empty($options['union']) ? $options['union'] : ''),
$this->parseLock(isset($options['lock']) ? $options['lock'] : false),
$this->parseComment(!empty($options['comment']) ? $options['comment'] : ''),
$this->parseForce(!empty($options['force']) ? $options['force'] : ''),
), $sql);
return $sql;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
替换 %TABLE%:
调用 $this->parseTable($options['table']) 方法,解析数据表名。
替换 %DISTINCT%:
调用 $this->parseDistinct(isset($options['distinct']) ? $options['distinct'] : false)
方法,解析是否需要 DISTINCT。
替换 %FIELD%:
调用 $this->parseField(!empty($options['field']) ? $options['field'] : '*')
方法,解析查询的字段。
替换 %JOIN%:
调用 $this->parseJoin(!empty($options['join']) ? $options['join'] : '') 方法,解析
JOIN 子句。
替换 %WHERE%:
调用 $this->parseWhere(!empty($options['where']) ? $options['where'] : '') 方法,解析
WHERE 子句。
替换 %GROUP%:
调用 $this->parseGroup(!empty($options['group']) ? $options['group'] : '') 方法,解析
GROUP BY 子句。
替换 %HAVING%:
调用 $this->parseHaving(!empty($options['having']) ? $options['having'] : '')
方法,解析 HAVING 子句。
替换 %ORDER%:
调用 $this->parseOrder(!empty($options['order']) ? $options['order'] : '') 方法,解析
ORDER BY 子句。
替换 %LIMIT%:
调用 $this->parseLimit(!empty($options['limit']) ? $options['limit'] : '') 方法,解析
LIMIT 子句。 替换 %UNION%:
调用 $this->parseUnion(!empty($options['union']) ? $options['union'] : '') 方法,解析
UNION 子句。
替换 %LOCK%:
调用 $this->parseLock(isset($options['lock']) ? $options['lock'] : false)
方法,解析是否加锁。
替换 %COMMENT%:
调用 $this->parseComment(!empty($options['comment']) ? $options['comment'] : '')
方法,解析注释。
替换 %FORCE%:
调用 $this->parseForce(!empty($options['force']) ? $options['force'] : '') 方法,解析
FORCE 选项。
将上述解析结果应用到 SQL 查询语句中。
使用 str_replace 函数将 SQL 查询语句中的占位符替换为具体的解析结果。
|
- 其实就是构造sql语句
生成完之后返回然后在select方法中返回查询结果
当然很明显可以知道,查询的结果是:
1
2
3
4
5
6
7
|
array(1) {
[0] =>
array(1) {
'content' =>
string(41) "This is the content of the second article"
}
}
|
find函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/**
* 数据读取后的处理
* @access protected
* @param array $data 当前数据
* @return array
*/
protected function _read_data($data)
{
// 检查字段映射
if (!empty($this->_map) && C('READ_DATA_MAP')) {
foreach ($this->_map as $key => $val) {
if (isset($data[$val])) {
$data[$key] = $data[$val];
unset($data[$val]);
}
}
}
return $data;
}
|
- 首先检查模型类中是否定义了字段映射并且读取(READ_DATA_MAP)这个配置项,判断是否启动数据映射功能,这里是没有开启的,所以就直接跳出
- _after_find方法没做什么操作。判断
result
键名是否存在,存在则进行returnResult
方法,不存在则不作操作。接下来将$data
赋值给当前对象的data
属性
(++)
data:
1
2
3
4
|
array(1) {
'content' =>
string(41) "This is the content of the second article"
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
protected function returnResult($data, $type = '')
{
if ($type) {
if (is_callable($type)) {
return call_user_func($type, $data);
}
switch (strtolower($type)) {
case 'json':
return json_encode($data);
case 'xml':
return xml_encode($data);
}
}
return $data;
}
|
//这里就直接粘贴了gpt解释的
if ($type) {
: 检查是否提供了输出类型 $type
。
if (is_callable($type)) {
: 如果 $type
是一个可调用的函数或方法,则通过 call_user_func
调用该函数,并传递查询结果 $data
,然后将结果返回。这样允许用户定义自己的输出处理逻辑。
switch (strtolower($type)) {
: 如果 $type
不是可调用的函数,则转换为小写,并使用 switch
语句基于不同的输出类型执行相应的处理。
case 'json':
: 如果输出类型是 JSON,使用 json_encode
将查询结果 $data
转换为 JSON 格式,并返回。
case 'xml':
: 如果输出类型是 XML,调用 xml_encode
方法将查询结果 $data
转换为 XML 格式,并返回。
return $data;
: 如果没有提供输出类型或者提供的输出类型不在处理范围内,直接返回原始查询结果 $data
- 判断缓存cache是否存在,存在的话就利用S方法进行对缓存进行读取和写入
- 最后返回查询结果也就是当前对象刚刚被赋值为
$data
自己的一些思考:
因为在写到returnresult这个方法的时候,我本来想写一下data传入的值,可能正常都会觉得就是前面的content,但是因为师傅说了有地方漏掉了,并且刚刚有分析到cache这里,最后会读取cache缓存并且赋值给了data,所以我认为这里是要利用方法将cache的值给他覆盖掉(或者说是要让他生成一个cache)最后被外带出来从而获取flag,但是具体的操作,如何触发还是有点懵