关于N1star分析中find的详细分析

上面的分析给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的时候加的) 这里返回的当前对象的主键

1
string(2) "id"

其实也就是将$options赋值给了这个where[“id”]

  • 如果传入的参数是数字或字符串类型,他会创建一个$where数组,并用主键作为键,传入的参数为值,然后这个$where数组放入$options['where']中 dump->$options[‘where’]:
1
2
3
4
array(1) {
'id' =>
string(1) "2"
}

其实就是交换了一下并且转化为数组的形式(我的理解)

  • 接着就是将当前对象利用getPk方法获取当前对象的主键赋值给$pk变量 dump(虽然知道和前面是一样的但是我还是写出来一下,主打一个仔细):
1
string(2) "id"

接下来进入下一个判断

  • 判断$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;
    }
  • 这个方法用于解析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函数:

  • 接下来进入_read_data方法
 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解释的

  1. if ($type) {: 检查是否提供了输出类型 $type
  2. if (is_callable($type)) {: 如果 $type 是一个可调用的函数或方法,则通过 call_user_func 调用该函数,并传递查询结果 $data,然后将结果返回。这样允许用户定义自己的输出处理逻辑。
  3. switch (strtolower($type)) {: 如果 $type 不是可调用的函数,则转换为小写,并使用 switch 语句基于不同的输出类型执行相应的处理。
    • case 'json':: 如果输出类型是 JSON,使用 json_encode 将查询结果 $data 转换为 JSON 格式,并返回。
    • case 'xml':: 如果输出类型是 XML,调用 xml_encode 方法将查询结果 $data 转换为 XML 格式,并返回。
  4. return $data;: 如果没有提供输出类型或者提供的输出类型不在处理范围内,直接返回原始查询结果 $data
  • 判断缓存cache是否存在,存在的话就利用S方法进行对缓存进行读取和写入
  • 最后返回查询结果也就是当前对象刚刚被赋值为$data

自己的一些思考: 因为在写到returnresult这个方法的时候,我本来想写一下data传入的值,可能正常都会觉得就是前面的content,但是因为师傅说了有地方漏掉了,并且刚刚有分析到cache这里,最后会读取cache缓存并且赋值给了data,所以我认为这里是要利用方法将cache的值给他覆盖掉(或者说是要让他生成一个cache)最后被外带出来从而获取flag,但是具体的操作,如何触发还是有点懵