Laravel模型中的动态属性

Laravel的文档里对于Model的动态属性讲的比较简略,理解起来有些模糊,最近在伟大的虾米的带领下终于搞明白了,在此做一个比较详细的总结。

一、引入

首先上Laravel文档相关部分。一对一关联是很基本的关联。例如一个User模型也许会对应一个Phone。要定义这种关联,我们必须将phone方法放置于User模型上。phone方法应该要返回基类Eloquent上的hasOne方法的结果:

<?phpnamespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{ 
    /** 
    * 获取与指定用户互相关联的电话纪录。 
    */ 
    public function phone() { 
        return $this->hasOne('App\Phone'); 
    }
}

传到hasOne方法里的第一个参数是关联模型的类名称。定义好关联之后,我们就可以使用Eloquent的动态属性来获取关联纪录。动态属性让你能够访问关联函数,就像他们是在模型中定义的属性:

$phone = User::find(1)->phone;

但这里只讲了动态属性的最简单的一种形式,也就是调用的属性不存在,但存在同名的方法时,则会调用同名的方法,返回的类型是collection类型(Eloquent的集合)。下文让我们走一遍Laravel的源代码看看还有其他几种不同种类的动态属性。

二、Laravel源代码trace

1、对于动态属性疑问的产生

虾米在梅林项目的blade里用到了一个方法,但是usermodel里并不存在同名的avatar_src()方法,但是存在一个getAvatarSrcAttribute()名字有点像的方法,当时就觉得很懵逼,看代码的确是调用了这个方法,但不知是如何关联起来的,所以想搞明白这里面的逻辑究竟是怎么回事。

<a href="#"><img src="{{ Auth::user()->avatar_src }}" alt=""></a>

2、__get()

那么问题来了,如何追溯?这里需要的一个预备知识是关于PHP的魔术方法__get(),当读取不可访问属性的值时,__get()会被调用。所以决定从这个方法开始进行追溯。具体的方法是在PhpStorm里打开user模型的代码,在菜单栏选择Navigate-File Structure,弹出的框子里勾选Show inherited members,英文输入状态下输入get可以找到我们想要的方法,点进去可以看到__get()方法源代码如下:

    /**
     * Dynamically retrieve attributes on the model.
     *
     * @param  string  $key
     * @return mixed
     */
    public function __get($key)
    {
        return $this->getAttribute($key);
    }

3、getAttribute($key)

    /**
     * Get an attribute from the model.
     *
     * @param  string  $key
     * @return mixed
     */
    public function getAttribute($key)
    {
        if (array_key_exists($key, $this->attributes) || $this->hasGetMutator($key)) {
            return $this->getAttributeValue($key);
        }

        return $this->getRelationValue($key);
    }

第一个if的左半边,如果这个model有这个attribute那么就直接返回,没什么可说的。
第一个if的右半边mutator是变异体的意思事实上处理了本节开头的疑问,看一下源代码:

    /**
     * Determine if a get mutator exists for an attribute.
     *
     * @param  string  $key
     * @return bool
     */
    public function hasGetMutator($key)
    {
        return method_exists($this, 'get'.Str::studly($key).'Attribute');
    }

本方法的作用是判断所调用的这个不存在的属性是否存在“按照一定格式变形的类似名字的方法”。所谓的“一定格式”可以参考Studly caps命名法,对应的源代码:

    /**
     * Convert a value to studly caps case.
     *
     * @param  string  $value
     * @return string
     */
    public static function studly($value)
    {
        $key = $value;

        if (isset(static::$studlyCache[$key])) {
            return static::$studlyCache[$key];
        }

        $value = ucwords(str_replace(['-', '_'], ' ', $value));

        return static::$studlyCache[$key] = str_replace(' ', '', $value);
    }

注意到经studly caps处理过的-和_都会被去掉。再回到hasGetMutator($key)这个方法,我们可以看到Laravel会尝试去寻找名字形似getStudlyCapsNameAttribute()的方法,如果有的话则会在getAttribute($key)里返回相关的值。第一小节提到的例子对应的方法名我们可以知道当调用这个不存在的属性avatar_src时,Laravel会尝试调用getAvatarSrcAttribute()这个方法,看了下代码果然是存在这个方法的,开始的疑问解决啦~

4、getRelationValue($key)

回到getAttribute($key)这个方法,如果在第一个if里没有返回则会调用getRelationValue($key)这个方法,源代码如下:

    /**
     * Get a relationship.
     *
     * @param  string  $key
     * @return mixed
     */
    public function getRelationValue($key)
    {
        // If the key already exists in the relationships array, it just means the
        // relationship has already been loaded, so we'll just return it out of
        // here because there is no need to query within the relations twice.
        if ($this->relationLoaded($key)) {
            return $this->relations[$key];
        }

        // If the "attribute" exists as a method on the model, we will just assume
        // it is a relationship and will load and return results from the query
        // and hydrate the relationship's value on the "relationships" array.
        if (method_exists($this, $key)) {
            return $this->getRelationshipFromMethod($key);
        }
    }

第一个if注释写得很清楚了,第二个if就是判断是否存在和所调用属性同名的方法,如果存在则调用getRelationshipFromMethod($key)方法。

5、getRelationshipFromMethod($method)

这个方法比较关键,我们看一下源代码:

    /**
     * Get a relationship value from a method.
     *
     * @param  string  $method
     * @return mixed
     *
     * @throws \LogicException
     */
    protected function getRelationshipFromMethod($method)
    {
        $relations = $this->$method();

        if (! $relations instanceof Relation) {
            throw new LogicException('Relationship method must return an object of type '
                .'Illuminate\Database\Eloquent\Relations\Relation');
        }

        $this->setRelation($method, $results = $relations->getResults());

        return $results;
    }

注意if语句块那里的判断,意味着与属性同名的方法的返回类型必须是Relation类型或者是它的子类,例如hasMany等。所以如果要另外做处理,返回的类型不为Relation的话可以参考第四小节那样的命名法构造相关方法名。另外,setRelation那一行的意思是将没有加载的relation进行加载,那么下次需要时就可以在getRelationValue($key)的第一个if中即返回需要的结果。还有值得注意的是此方法最后的返回值返回的$resultsCollection类型,也就是说如果调用不存在的动态属性后返回的是Collection类型,而如果我们直接调用方法返回的则是Relation类型,可以在其上构造查询进一步处理,而再调用getResults()后才能再获得Collection类型的返回值。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,183评论 6 516
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,850评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,766评论 0 361
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,854评论 1 299
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,871评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,457评论 1 311
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,999评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,914评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,465评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,543评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,675评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,354评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,029评论 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,514评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,616评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,091评论 3 378
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,685评论 2 360

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 11,007评论 6 13
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,708评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,664评论 18 399
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,726评论 0 9
  • 那是遗落在冷清街道的一个影子 凭身形,依稀辨来是个醉汉 旁边瘫坐着几个酒瓶,唱得有气无力: 生活,已是如此艰难 我...
    青色的木头阅读 244评论 1 2