Laravel 动态属性的实现

什么是动态属性

假设我们有一个 User 模型:

class User extends Model
{
    /**
     * 获取与用户关联的电话号码记录。
     */
    public function phone()
    {
        return $this->hasOne('App\Phone');
    }

    public function getFullNameAttribute()
    {
        return "{$this->first_name} {$this->last_name}";
    }
}
$user = User::find(1);
// 这种调用不存在的属性就是 laravel 中的动态属性
$user->full_name;
$user->phone

动态属性的实现

1. 万恶之源 __get()

PHP 的魔术方法 __get(),当读取不可访问属性的值时,__get()会被调用。我们在在使用动态属性时
都会触发这个魔术方法。然后我顺利的在 Eloquent\Model.php 中找到了这个这个方法:

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

2. getAttribute

继续追踪:

/**
     * Get an attribute from the model.
     *
     * @param  string  $key
     * @return mixed
     */
    public function getAttribute($key)
    {
        if (! $key) {
            return;
        }

        // If the attribute exists in the attribute array or has a "get" mutator we will
        // get the attribute's value. Otherwise, we will proceed as if the developers
        // are asking for a relationship's value. This covers both types of values.
        if (array_key_exists($key, $this->attributes) ||
            $this->hasGetMutator($key)) {
            return $this->getAttributeValue($key);
        }

        // Here we will determine if the model base class itself contains this given key
        // since we don't want to treat any of those methods as relationships because
        // they are all intended as helper methods and none of these are relations.
        if (method_exists(self::class, $key)) {
            return;
        }

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

第二个 if 处理了model有这个attributemodel有对应访问器的情况。

3. getRelationValue

继续追踪, 关联关系的动态属性用法:

/**
    * 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);
    }
}

这里可以看到首先会读一个关联关系的缓存,若没有缓存才会判断是否存在和所调用属性同名的方法,
如果存在则调用 getRelationshipFromMethod($key) 方法。

4. getRelationshipFromMethod($method)

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;
    }

setRelation 的意思是将没有加载的 relation 进行加载,那么下次需要时就可以在getRelationValue($key)的第一个 if 中即返回需要的结果。

此方法的返回值返回的是 Collection 类型,也就是说动态属性访问关联模型返回的是Collection类型,而如果我们直接调用方法返回的则是 Relation 类型。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 6

显式的定义不好嘛

6年前 评论

那访问动态属性和直接访问方法有什么区别吗

5年前 评论

这个挺有用的,我在做短信通知的时候,在数据库中保存短信模板,模板中包括了需要返回的变量名,可以用动态属性的方式将其中的变量赋值,再发送给短信通道,实现短信模板的可配置。

3年前 评论

@孟繁永 没明白,不用动态属性不能赋值吗?

2年前 评论

@ZhangHe

<?php
/*
 * @Date: 2020-07-15 17:40:56
 * @LastEditors: Future Meng
 * @LastEditTime: 2020-07-22 16:00:36
 */

namespace App\Helpers;

use App\Models\Notice;
use Illuminate\Support\Arr;
use Leonis\Notifications\EasySms\Channels\EasySmsChannel;

trait NotificationTraits
{

    public function getChannels($notifiable)
    {
        $channels = [];

        // $event = Str::replaceFirst('App\\Notifications\\', '', get_class($this));

        //查找和事件匹配的通知
        $notice = Notice::where('name', $this->notice)->first();

        if ($notice) {

            //调出当前可用的模板,后续需要增加检查organizer和corporation相关的自定义设置来决定,每种channel取一个。
            // $templates = $notice->templates()->where(['is_online' => 1]);

            //判断当前notice可用的模板来决定启用哪些频道,后续需要增加检查organizer和corporation相关的自定义设置来决定
            $smsTemplate = $notice->templates()->where(['has_registered' => 1, 'is_online' => 1])->where(['channel' => 'sms'])->orderBy('is_default', 'desc')->first();
            if ($smsTemplate) {
                $notifiable->smsTemplate = $smsTemplate;
                $channels = Arr::prepend($channels, EasySmsChannel::class);
            }

            $emailTemplate = $notice->templates()->where(['has_registered' => 1, 'is_online' => 1])->where(['channel' => 'email'])->orderBy('is_default', 'desc')->first();
            if ($emailTemplate) {
                $notifiable->emailTemplate = $emailTemplate;
                $channels = Arr::prepend($channels, 'mail');
            }

        }

        return $channels;

    }

    public function getData($template, $origin)
    {

        $data = [];
        // ->setData(['title' => $this->organizer->title]);
        // 尊敬的用户您好,您申请的${title}已经通过审核,请登录主办方后台继续操作。
        // 正则表达式,获取${value}或${value:originvalue}样式的值,以${开头,以}结尾,且不包括前后的字符
        // preg_match_all('/(?<=\$\{)[a-z_]+(:[a-z]+(_[a-z]+)?)?(?=\})/', $template, $matches, PREG_SET_ORDER);

        // 正则表达式,获取${key}或${key:origin_key.origin_kid_key}样式的值,以${开头,以}结尾,且不包括前后的字符
        preg_match_all('/(?<=\$\{)[a-z_]+(:[a-z]+(_[a-z]+)*(\.[a-z]+(_[a-z]+)*)*)?(?=\})/', $template, $matches, PREG_SET_ORDER);

        foreach ($matches as $match) {
            $keypair = preg_split('/:/', $match[0]); //$match[0]是获取最完整匹配

            //当没有冒号,即没有属性映射时
            if (count($keypair) == 1) {
                $key = $keypair[0];
                $value = $origin->getAttribute($key) ?? '空';
            }

            //当有冒号是,即有属性映射
            if (count($keypair) == 2) {
                $key = $keypair[0];

                // $keypair[1]为exhibition.title
                $originkeyrefers = preg_split('/\./', $keypair[1]);

                $value = $origin;
                foreach ($originkeyrefers as $originkey) {
                    $value = $value->getAttribute($originkey) ?? '空';
                }
            }

            $data = Arr::add($data, $key, $value);

        }

        return $data;
    }

}
2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!