MENU

PHP像数组一样访问对象

March 13, 2019 • PHP

今天的主角就是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类实现了ArrayAccessPHP预定义的接口。

原理

那么什么是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