由 Phalcon Model 的 “bug” 引发的对 PHP Iterator 和 ArrayAccess 的探索
0x00 奇怪的现象
最近在写 Phalcon ORM 查询结果集遍历的时候发现 Phalcon ORM 有个很神奇的现象:当你用 foreach 遍历 Model->find() 之后的结果(Phalcon\Mvc\Model\Resultset\Simple
),如果在遍历中有访问下一个($key + 1)元素,就会自动执行 next(),下一个元素就不会出现在 foreach
的下一次遍历中了...
举个例子,比如说我们有一张表 users
,我们创建一个 User
model,执行如下代码:
<?php
$users = \Models\User::find([
'conditions' => 'role = 1' // 这里可以是别的查询条件
]);
// 这里假设查询出来的记录只有两条,即 count($users) == 2
foreach ($users as $key => $user) {
echo "key: $key, user_id: {$user->id}\n";
// 假设这里因为业务逻辑的需要访问下一个 model
$foo = $users[$key + 1];
}
你可能会觉得上面的循环在执行到第二次会报 PHP Notice: Undefined offset
,但实际上,它什么也没报,只是安安静静地输出了:
key: 0, user_id: 1
嗯?第二次循环的输出怎么不见了?试下将 $foo = $users[$key + 1];
那一行去掉,看看输出什么:
key: 0, user_id: 1
key: 1, user_id: 2
0x01 实验
嗯??第二次循环的输出出现了!用 Xdebug 跟一下吧。
结果还是发现,只要在循环中访问了下一个元素,这个元素就不会再出现在遍历中了,amazing,好像跟下面这个代码的执行效果不一样啊
<?php
$foo = [
0 => 'a',
1 => 'b',
];
foreach ($foo as $k => $v) {
echo "key: $k, value: $v\n";
$bar = @$foo[$k + 1];
}
// 输出:
// key: 0, value: a
// key: 1, value: b
0x02 查文档,看源码
翻一下 Phalcon 的文档吧,于是在文档中看到 Resultset
是实现了 Iterator
的,所以它有个 key()
方法,把代码改成这样再看看:
foreach ($users as $key => $user) {
echo "key: $key, key(): {$user->key()}, user_id: {$user->id}\n";
// 假设这里因为业务逻辑的需要访问下一个 model
$foo = $users[$key + 1];
echo "key() again: {$user->key()}";
}
输出:
key: 0, key(): 0, user_id: 1
key() again: 1
明明没有显式执行 Iterator
的 next()
,为啥 key
会加 1 了?!Iterator
的 next()
不应该是 foreach
结束后才执行的吗?找一下 Phalcon 的代码,看看是怎么实现的吧。
Resultset
里的 next()
也很正常,就是简单的一句让指针加一:
/**
* Moves cursor to next row in the resultset
*/
public function next() -> void
{
// Seek to the next position
this->seek(
this->pointer + 1
);
}
0x03 真相大白
不过我发现,Resultset
还实现了 ArrayAccess
,也就是说它有 offsetGet()
这个方法(这个方法允许我们以 []
的方式去访问元素),我们看看它是怎么实现的:
/**
* Gets row in a specific position of the resultset
*/
public function offsetGet(var index) -> <ModelInterface> | bool
{
if unlikely index >= this->count {
throw new Exception("The index does not exist in the cursor");
}
/**
* Move the cursor to the specific position
*/
this->seek(index);
return this->{"current"}();
}
是用 seek()
这个方法来取得的,看下 seek()
是怎么实现的,首先看到 seek()
前的注释:
/**
* Changes the internal pointer to a specific position in the resultset.
* Set the new position if required, and then set this->row
*/
final public function seek(var position) -> void
what???将内部的指针直接指向指定的 position
???看了一下代码(代码比较长,这里就不贴了,感兴趣的可以直接看上面链接指向的代码),还真是,而且,它的 key()
是这样实现的:
/**
* Gets pointer number of active row in the resultset
*/
public function key() -> int | null
{
if !this->valid() {
return null;
}
return this->pointer;
}
也就是说 seek()
会影响到 key()
从而影响到 foreach()
的走向。。。真相大白了。。。
0x04 总结与思考
总结来说就是 Phalcon 对 ArrayAccess
的实现不规范,那我们该如何规避这个问题呢,不难,只需要遍历的时候,在我们需要通过 []
访问非当前元素之前,用 key()
取得当前内部指针并存起来,在访问完之后使用 seek()
将内部指针指向该次循环的实际位置即可
0x06 思考题
最后留一个思考题吧
将最开始那一段代码中的$foo = $users[$key + 1];
改成$foo = isset($users[$key + 1]);
,为什么会有不一样的结果?
如果你仔细看了文中提到的代码,和一些相关的文档,你应该很快就能知道为什么 :-P
很好