新版本的 Laravel 提供了一種更便捷的作法來定義 accessor 和 mutator,下面將會比較新舊版本之間的差異,原有方式在新版本當中還是有作兼容,但新的寫法是相對來說更加清晰好懂。

原做法

// 假定 users 有 name:
// accessor
protected function getNameAttribute($value)
{
    return Str::mask($value, '*', 2);
}

// mutator
protected function setNameAttribute($value)
{
    $this->attributes['name'] = 'Mr.'.$value;
}

// 要偽裝一個不存在的欄位 accessor
protected function getFirstNameAttribute($value)
{
    return ucfirst($this->name);
}

新做法

doc,將原有 accessor 和 mutator 綜合為同一個方法進行操作。

// 假定 users 有 name:
protected function name(): Attribute
{
    return Attribute::make(
        get: fn (string $value) => Str::mask($value, '*', 2),
        set: fn(string $value) => 'Mr.'.$value
    );
}

// 要偽裝一個不存在的欄位 accessor
protected function firstName(): Attribute
{
    return Attribute::make(
        get: fn () => ucfirst($this->name),
    );
}

// 也可以透過第二參數進行組合屬性
protected function address(): Attribute
{
    return Attribute::make(
        get: fn (mixed $value, array $attributes) => new Address(
            $attributes['address_line_one'],
            $attributes['address_line_two'],
        ),
    );
}

底層探勘

在文件後面我看到有一個方法叫做 shouldCache(),這邊的 cache 指的應該是類似 in-memory 的那種 cache,而不是持久型(如 redis / database)這樣子,具體討論這邊有描述

我模擬了一下範例中的 get: fn (string $value) => bcrypt(gzuncompress($value)),,步驟如下:

  1. 確保自己配置 gzip 壓縮
  2. 創建添加欄位 hash(blob type):
$table->binary('hash')->nullable();
  1. User model 內添加相關 fillable
  2. 寫 accessor / mutator
protected function hash(): Attribute
{
    return Attribute::make(
        get: fn (string $value) => bcrypt(gzuncompress($value)),
        set: fn(string $value) => gzcompress($value),
    )->shouldCache();
}
  1. 配置相關 factory 產生,或是自己 create:
'hash' => gzcompress(Str::random()),
  1. 取得演算數據 $users->hash

看了一下底層的配置跟設計似乎有點像是 singleton 的設計方式,具體來說就是在進程下共用同樣的屬性空間:

// Illuminate\Database\Eloquent\Casts\Attribute.php

public $withCaching = false;

public function shouldCache()
{
    $this->withCaching = true;

    return $this;
}

// Illuminate/Database/Eloquent/Concerns/HasAttributes.php
protected function mutateAttributeMarkedAttribute($key, $value)
{
    if (array_key_exists($key, $this->attributeCastCache)) {
        return $this->attributeCastCache[$key];
    }

    $attribute = $this->{Str::camel($key)}();

    $value = call_user_func($attribute->get ?: function ($value) {
        return $value;
    }, $value, $this->attributes);

    // 在單例中發現 $attribute->withCaching == true -> 配置 attributeCastCache
    if ($attribute->withCaching || (is_object($value) && $attribute->withObjectCaching)) {
        $this->attributeCastCache[$key] = $value;
    } else {
        unset($this->attributeCastCache[$key]);
    }

    return $value;
}

在沒有這個方法之前,我在網路上有看過有類似這樣子的作法: 參考,但其實邏輯是一樣的:

return Attribute::make(
    get: function($value) {
        if (! $this->cacheHash) {
            $this->cacheHash = bcrypt(gzuncompress($value));
        }

        return $this->cacheHash;
    },
);