Laravel Eloquent Accessor Mutator Attribute Casting Enum
Laravel PHP

Laravel Eloquent Accessor、Mutator & Attribute Casting,Laravel 9 開始已大改

商業,創業,業務,職涯,美食,葡萄酒,閱讀,網路科技。

從 Larry 創業以及商業的經驗,希望以白話的口吻,介紹給大家這個商業的世界。

FB粉專會頻繁地更新 Larry 對於商業、社會、人生的觀察與心得,歡迎大家追蹤互動~

去年 (2022) 發布的 Laravel 9 中,Eloquent Accessor & Mutator 迎來了一次大改,Attribute Casting 的部分則是調整不大。

伴隨著 PHP 8.1 的釋出,Laravel 8 的後繼版本已經支援 Enum Casting (當然 Laravel 9 之後的版本也會支援)。

延伸閱讀:Laravel 9 的新功能:最低要求 PHP 8.0,底層更新為 Symfony 6.0,開始使用 PHP 8.1 的 Enum

larry 想藉 Laravel 9 大幅更新 Accessor & Mutator 的這個機會,走一遍 Mutator & Casting 的官方文件。本篇文章會以 Laravel 9 Mutator & Casting 官方文件為主。
https://laravel.com/docs/9.x/eloquent-mutators

首先,依照官方文件的語意,Accessor 指的是 get 這個動作,Mutator 指的是 set。

什麼時機會使用到 Accessor / Mutator 呢?例如,每次存一個值都要 encrypt,或是取出來用時都要 decrypt。或是,你要每次都要把 array 轉成 json 存到資料庫,使用時取出 json 要再轉回 array。

也就是,如果你每次「存」或「取」都要做一定動作,可以考慮使用 Accessor / Mutator。Laravel 9 開始,假如你要存取一個 model 裡的 first_name attribute,要這樣寫

use Illuminate\Database\Eloquent\Casts\Attribute;

// 注意有 return type
// 注意是 protected method, 不允許調用端直接調用
protected function firstName(): Attribute
{
    // 可以只寫 get 或 set 其一
    return Attribute::make(
        get: fn ($value) => ucfirst($value),
        set: fn ($value) => strtolower($value),
    );
}

有時後程式邏輯需要把資料庫中的幾個欄位,重組成一個新的 object,Laravel 稱之為 value object。

use Illuminate\Database\Eloquent\Casts\Attribute;

// 注意是 protected method
// 當調用端存取 address 時會觸發這個 function 
// 也就是自動新增了一個 address attribute
protected function address(): Attribute
{
    // class Address 就是所謂的 value object
    return Attribute::make(
        get: fn ($value, $attributes) => new Address(
            $attributes['address_line_one'],
            $attributes['address_line_two'],
        ),
        set: fn (Address $value) => [
            'address_line_one' => $value->lineOne,
            'address_line_two' => $value->lineTwo,
        ],
    );
}

讀者可以觀察一下,get / set 使用的都是 PHP 7.4 的 arrow function。get return 的是一個新生成的 object,set 中設定的是一個 array。

class Address 就是所謂的 value object。調用端要使用,會像是 $user->address->lineOne

定義 value object 時,lineOnelineTwo 都要是 public。

class Address
{
    public $lineOne;
    public $lineTwo;

    public function __construct(...)
    {
        //…
    }
}

其實把 DB 中的欄位重新包裝成 value object (如上方的 class Address),在程式邏輯中使用,已經算是 Attribute Casting。Laravel 提供了一個不用寫 Accessor / Mutator 的方式,將 DB 欄位轉成常用的資料格式。

Attribute Casting

例如 Laravel 的 DB migration boolean,實際在 MySql 裡是 tinyint(1),存的值是 1 或 0。這時你可以 cast 成 boolean

use Illuminate\Database\Eloquent\Casts\AsStringable;
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
use Illuminate\Database\Eloquent\Casts\AsCollection;

class User extends Model
{
    protected $casts = [
        'is_admin' => 'boolean',
        'directory' => AsStringable::class,
        'options' => 'array',
        // 也可以 cast 成 PHP ArrayObject 或 Laravel Collection
        //'options' => AsArrayObject::class,
        //'options' => AsCollection::class,
    ];
}

也就是如果 DB migration 是 boolean,這樣可以使用沒錯。但如果要寫漂亮一點,model attribute 可以 cast 成 boolean。字串的部分,你也可以 cast 成 Laravel fluent string,方便後續操作。

另外,如果你的 DB migration 有 jsontext,model attribute 可以 cast 成 array,存取時自動會在 array 和 json 格式轉換。等同於資料庫欄位可以直接存 array,這是很有趣的。

接下來是一個重要功能 Enum Casting (僅支援 PHP 8.1 以上的環境)

use App\Enums\ServerStatus;
 
protected $casts = [
    'status' => ServerStatus::class,
];

這樣要存或取 $model->status 都會直接轉換成 enum。

if ($server->status == ServerStatus::Provisioned) 
{
    $server->status = ServerStatus::Ready;
    $server->save();
}

Custom Casts

如果你執行 Artisan command

php artisan make:cast YourClass

就會新增一個 YourClass 在 app/Casts 目錄下。例如 class Address

use App\ValueObjects\Address as AddressValueObject;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class Address implements CastsAttributes
{
    public function get($model, $key, $value, $attributes)
    {
        // 將 DB 欄位轉成 value object return
        return new AddressValueObject(
            $attributes['address_line_one'],
            $attributes['address_line_two']
        );
    }

    public function set($model, $key, $value, $attributes)
    {
        if (! $value instanceof AddressValueObject) 
        {
            //throw exception
        }
 
        // return array 去設定 DB 欄位
        return [
            'address_line_one' => $value->lineOne,
            'address_line_two' => $value->lineTwo,
        ];
    }
}

記得文章上方的 Accessor / Mutator,使用的是 PHP 7.4 的 arrow function。function expression 內容就如同上方範例,只是 Custom Casts 把程式獨立到一個 class。

記得要設定 model,這樣等於新增了一個 address attribute (DB 存的是 address_line_one, address_line_two)

use App\Casts\Address;
 
protected $casts = [
    ‘address’ => Address::class,
];

結論

Accessor / Mutator 與 Casting 是一體的兩面。你可以寫 Accessor / Mutator,但如果正好有 Laravel 預先定義的 cast 資料類型,就直接用 casting 比較方便,包含 PHP 8.1 的 enum。

使用情境上,如果每次存取資料都有一個固定動作,可以考慮使用 Accessor / Mutator 或 Casting。例如每次存一個值都要 encrypt,或是每次都要把 array 轉成 json 存到資料庫,使用時取出 json 要再轉回 array。

或是轉成 Laravel Stringable 或 Collection 等資料格式,方便後續處理。將幾個 DB 欄位重新包裝成 value object 也是一個使用情境。

Accessor / Mutator 與 Casting 不是 Laravel 的大主題,但實際走過文件,還是有很多細節要注意。希望大家在資料庫欄位 / 程式邏輯之間的轉換 (ORM,Object Relational Mapping),也有更深一層的理解囉。 

商業,創業,業務,職涯,美食,葡萄酒,閱讀,網路科技。

從 Larry 創業以及商業的經驗,希望以白話的口吻,介紹給大家這個商業的世界。

FB粉專會頻繁地更新 Larry 對於商業、社會、人生的觀察與心得,歡迎大家追蹤互動~