今天的主角就是PHP提供的预定义接口ArrayAccess
。她提供了像访问数组一样访问对象的能力的接口。如果学习过laravel源码部分可以看到在laravel中大量使用了这个接口。
[TOC]
前导
比如在laravel中我们查询一个用户的信息可以像下面这样
- 对象的方式
$user = \App\Models\User::query()->find(1);
dd($user->name);
- 数组的方式
$user = \App\Models\User::query()->find(1);
dd($user['name']);
同样的,我们也可以通过这2种方式来修改模型的属性。比如
$user = \App\Models\User::query()->find(1);
$user->name = 'Kevin';
//或者
$user['email'] = 'lepig@qq.com';
$user->save();
看到没,在操作对象的时候不光能使用箭头符号->
,还能使用数组声明符号[]
来操作对象。 当你不知道底层是如何实现的,你肯定会更加的好奇?所以可能会有一声感叹:PHP果然是世界上最好的语言!!嘿嘿
所以带着这个疑问我们继续深入
探究
我们可以通过ide的代码追踪功能来看到User
模型继承了Authenticatable
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
...
然后我们继续跟踪Authenticatable
class User extends Model implements
AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword;
}
在继续查看Model
类
abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable
{
use Concerns\HasAttributes,
Concerns\HasEvents,
Concerns\HasGlobalScopes,
Concerns\HasRelationships,
...
到这里就可以看到Model
类实现了ArrayAccess
PHP预定义的接口。
原理
那么什么是ArrayAccess接口?实现了这个接口又能干什么事情?
首先查看下官方文档可以发现这个接口定义了4个抽象方法:
interface ArrayAccess {
public function offsetExists($offset); #检查一个偏移位置是否存在
public function offsetGet($offset); #获取一个偏移位置的值
public function offsetSet($offset, $value); #设置一个偏移位置的值
public function offsetUnset($offset); #复位一个偏移位置的值
}
同样在我们的Model
类中也实现了这4个方法:
...
public function offsetExists($offset)
{
return ! is_null($this->getAttribute($offset));
}
public function offsetGet($offset)
{
return $this->getAttribute($offset);
}
public function offsetSet($offset, $value)
{
$this->setAttribute($offset, $value);
}
public function offsetUnset($offset)
{
unset($this->attributes[$offset], $this->relations[$offset]);
}
...
到此,我们就可以大概看出点眉目了。当我们使用
isset($obj['xx'])
会触发offsetExists
方法
$obj['xx']
会触发offsetGet
方法
$ojb['xx'] = 'oo'
会触发offsetSet
方法
unset($obj['xx'])
会触发offsetUnset
方法
所以,下面我写一个小示例就可以更加清晰的了解ArrayAccess
了。
实验
<?php
class User implements \ArrayAccess
{
private $data;
public function __construct()
{
$this->data = [
'name' => 'lePig',
'email' => 'lepig@qq.com'
];
}
/** 检查某个属性是否存在 */
public function offsetExists($offset)
{
// return ! is_null($this->data[$offset]);
return isset($this->data[$offset]);
}
/** 获取某个属性的值 */
public function offsetGet($offset)
{
return $this->data[$offset];
}
/** 设置某个属性的值 */
public function offsetSet($offset, $value)
{
$this->data[$offset] = $value;
}
/** 移出某个属性 */
public function offsetUnset($offset)
{
unset($this->data[$offset]);
}
}
$user = new User;
//获取用户的email
var_dump($user['email']); //lepig@qq.com
// 检查age是否存在
$ageExists = isset($user['age']);
var_dump($ageExists); //bool(false)
// 设置一个age属性
$user['age'] = 27;
//继续判断age是否存在
$ageExists = isset($user['age']);
var_dump($ageExists); //bool(true)
//删除age
unset($user['age']);
var_dump(isset($user['age'])); //bool(false)
从上面就很明显可以看出,我们就可以像操作数组一样来操作对象了。
update:
今天在做一个tp5
的项目的时候也遇到了一个疑惑,就和这个ArrayAccess
相关。基本代码如下(已简化)
这段代码的意图是展示一个管理员用户列表,列表里面有一个字段要显示管理员所属的组,所以对应的model里声明了一个多对多的关联模型
$admin = new Admin();
$list = $admin
// ->with('groups')
->order('id', 'desc')
->limit(1, 5)
->select();
foreach ($list as $k => &$v) {
$v['groupx_text'] = implode(',', array_map(function ($item) {
return $item['name'];
}, $v['groups']));
}
unset($v);
关联模型
app\admin\model\Admin.php
public function groups()
{
return $this->belongsToMany(AuthGroup::class, 'auth_group_access', 'group_id', 'uid');
}
当我注释掉->with('groups')
的时候我以为返回的列表里就没有对应的groups键了。可是下面的foreach循环内部依然能正确的拿到$v['groups']
的值。所以我追踪了下源码发现在tp的Model里里面同样实现了ArrayAccess的4个抽象方法:
thinkphp/library/think/Model.php
// ArrayAccess
public function offsetSet($name, $value)
{
$this->setAttr($name, $value);
}
public function offsetExists($name)
{
return $this->__isset($name);
}
public function offsetUnset($name)
{
$this->__unset($name);
}
public function offsetGet($name)
{
return $this->getAttr($name);
}
所以,当我在上面的往array_map
函数里传递第二个参数$v['groups']
的时候,内部其实是调用到了Model的offsetGet
方法,然后获取到我声明的关联模型方法groups
。这也能反应出我们常说的N+1查询问题。如果你不用with('groups')
的话,相当于会执行5+1条查询。但如果使用with('groups')
那么只会产生2次查询。
SELECT
`fa_auth_group`.*, pivot.uid AS pivot__uid,
pivot.group_id AS pivot__group_id
FROM
`fa_auth_group`
INNER JOIN `fa_auth_group_access` `pivot` ON `pivot`.`group_id` = `fa_auth_group`.`id`
WHERE
`pivot`.`uid` = 27;
-------------------------------------------------------------------------------------
SELECT
`fa_auth_group`.*, pivot.uid AS pivot__uid,
pivot.group_id AS pivot__group_id
FROM
`fa_auth_group`
INNER JOIN `fa_auth_group_access` `pivot` ON `pivot`.`group_id` = `fa_auth_group`.`id`
WHERE
`pivot`.`uid` = 26;
-------------------------------------------------------------------------------------
SELECT
`fa_auth_group`.*, pivot.uid AS pivot__uid,
pivot.group_id AS pivot__group_id
FROM
`fa_auth_group`
INNER JOIN `fa_auth_group_access` `pivot` ON `pivot`.`group_id` = `fa_auth_group`.`id`
WHERE
`pivot`.`uid` = 25;
-------------------------------------------------------------------------------------
SELECT
`fa_auth_group`.*, pivot.uid AS pivot__uid,
pivot.group_id AS pivot__group_id
FROM
`fa_auth_group`
INNER JOIN `fa_auth_group_access` `pivot` ON `pivot`.`group_id` = `fa_auth_group`.`id`
WHERE
`pivot`.`uid` = 24;
-------------------------------------------------------------------------------------
SELECT
`fa_auth_group`.*, pivot.uid AS pivot__uid,
pivot.group_id AS pivot__group_id
FROM
`fa_auth_group`
INNER JOIN `fa_auth_group_access` `pivot` ON `pivot`.`group_id` = `fa_auth_group`.`id`
WHERE
`pivot`.`uid` = 16;
使用with的SQL
SELECT
`fa_auth_group`.*, pivot.uid AS pivot__uid,
pivot.group_id AS pivot__group_id
FROM
`fa_auth_group`
INNER JOIN `fa_auth_group_access` `pivot` ON `pivot`.`group_id` = `fa_auth_group`.`id`
WHERE
`pivot`.`uid` IN (
27,
26,
25,
24,
16
)
参考资料 :https://www.cnblogs.com/foreverno9/p/8640232.html
最近也在看相关框架的源码也发现了这个知识点。博主总结的挺好的,赞@(真棒)