Boot And Booted Methods in Laravel
Published: 2023-11-07 09:03:32

當談論 Laravel 的 Eloquent ORM 時，通常會聚焦於它如何簡化資料庫操作，以及如何透過 Model 來表達資料結構。然而，Eloquent ORM 也提供了一組生命周期掛鉤 ( hook )，讓開發人員可以在生命週期的不同階段介入，加入自己的邏輯。

今天，將專注於兩個經常被提起但有時會混淆的方法：`boot` 和 `booted`。

## `boot` 方法的探索

`boot` 方法是一個靜態方法，當 Model 在請求生命周期中第一次被使用時，它會被呼叫一次。這是設定 Model 事件監聽器的理想地點，比如建立、更新、或刪除時的操作。它被用來定義如何 **啟動（ booting ）** Model 類別的行為。

例如：假設我們正在建立一個多租戶應用程式 ( Multi Tenants )，我們希望每次建立 Model 時自動設定 `tenant_id` 屬性：

```php
protected static function boot()
{
    parent::boot();

    static::creating(function ($model) {
        if (auth()->check()) {
            $model->tenant_id = auth()->user()->tenant->id;
        }
    });
}
```

上述程式碼中，透過 creating 事件確保在 Model 被保存之前設定了正確的 tenant_id。這樣的處理讓多租戶邏輯集中並且自動化，使其在應用中穿透性強且一致。

## `booted` 方法的使用時機

與 `boot` 方法相對的是 `booted` 方法。`booted` 是在所有的 `boot` 方法被執行之後，且 Model 準備好被使用時執行。這是在 Model 的生命周期中加入方法呼叫的絕佳時機，特別是當這些方法呼叫依賴於在 `boot` 方法中設定的狀態時。

再一次使用多租戶應用程式的例子，這次希望確保每次對 Model 進行查詢時，都會自動加入特定的範圍限制：

```php
protected static function booted()
{
    static::addGlobalScope('tenantScope', function (Builder $builder) {
        if (auth()->check()) {
            $builder->where('tenant_id', auth()->user()->tenant_id);
        }
    });
}
```

上述程式碼中，在 `booted` 方法裡加入了一個全域作用域 ( Global Scopes )，這確保了每個查詢都會限制於當前用戶的租戶。

這使得查詢邏輯簡潔且集中，同時也提供了一層安全性 —— 用戶只能查詢到他們應該看到的資料。

## 思考的問題

> 為什麼 static::addGlobalScope 是在 booted 裡？
> 而 User::observe(UserObserver::class)，卻是在 boot 裡？

在 Laravel 中，static::addGlobalScope 不能在 `boot` 方法內部使用的主要原因是全域作用域的設定，需要在 Model 的 **啟動（ booting ）** 過程之後進行，確保所有的啟動邏輯，包括來自父類別的邏輯，都已經完成。全域作用域是對查詢構造器的全局修改，如果在啟動過程中太早加入，可能會與其他啟動邏輯相衝突，或者被後來的啟動邏輯覆蓋。

`booted` 方法是在 Model 類別的所有啟動邏輯完成之後呼叫的。這就為我們提供了一個安全的地方來加入全域作用域，因為這時候可以確保所有模型的啟動設定都已經完成，包括父類別中定義的設定。此時加入全域作用域，可以保證它們將被用於所有後續的查詢，而不會被其他啟動邏輯所影響。

例如，如果你在 `boot` 方法中設定了全域作用域，並且父類別也有自己的啟動邏輯，那麼父類別的啟動邏輯可能會無意中覆蓋或改變了設定的全域作用域。將全局作用域的設定放在 `booted` 方法中，可以避免這種情況發生，確保作用域總是像我們所期望的那樣被正確設定到模型上。

所以，按照 Laravel 的設計：

> `boot` 方法用於設定那些需要在模型啟動時立即生效的行為，如事件監聽器或是註冊觀察者；而 `booted` 方法則是在模型完全準備好後，用來進一步修改或增加設定，例如全域作用域。

這樣的分工使得模型的設定過程更加清晰與有序。

## 追蹤程式碼

追朔 Laravel 的程式碼，可以在 `Illuminate\Database\Eloquent\Model` 中看到初始化的流程中， 依序的呼叫了 `booting`、`boot`、`booted` 方法，並且觸發 `booting` 以及 `booted` 事件：

```php
/**  
 * Create a new Eloquent model instance. * * @param  array  $attributes  
 * @return void  
 */  
public function __construct(array $attributes = [])  
{  
    $this->bootIfNotBooted();  
  
    $this->initializeTraits();  
  
    $this->syncOriginal();  
  
    $this->fill($attributes);  
}  
  
/**  
 * Check if the model needs to be booted and if so, do it. 
 * 
 * @return void  
 */  
protected function bootIfNotBooted()  
{  
    if (! isset(static::$booted[static::class])) {  
        static::$booted[static::class] = true;  
  
        $this->fireModelEvent('booting', false);  
  
        static::booting();  
        static::boot();  
        static::booted();  
  
        $this->fireModelEvent('booted', false);  
    }  
}
```

而其中的 `boot` 方法註解中提到 `Bootstrap the model and its traits`：

```php
/**  
 * Bootstrap the model and its traits. 
 * 
 * @return void  
 */  
protected static function boot()  
{  
    static::bootTraits();  
}
```

而 booted 方法為 `Perform any actions required after the model boots.`：

```php
/**  
 * Perform any actions required after the model boots. 
 * 
 * @return void  
 */  
protected static function booted()  
{  
    //  
}

```

由此可知，若要加入類似全域作用域的設定，就必須要設定在 `booted` 方法裡！

## 結論

透過 `boot` 和 `booted` 方法，Laravel 讓開發人員能夠精確控制 Model 的生命周期，這對於維持乾淨、有組織的程式碼非常重要。理解這些方法及其時機，能夠幫助建立更加可靠和可維護的應用程式。無論是在加入事件監聽器還是定義全局作用域，這些方法都是控制 Eloquent 行為的強大工具。