跳轉到主要內容
有時你會希望套件的使用者不必在服務提供者中逐一列出設定鍵,就能注入所需的值。公開情境屬性後,就能依據附加在引數上的中繼資料,解析出套件專屬的設定或服務。 本頁確認了 Laravel Framework v13.39.0 的官方原始碼,並實作一個讀取 courier 設定的自訂屬性。如果內建的 #[Config('courier.timeout')] 就能達成目的,則不需要另外新增自訂屬性。只有在要將自有的鍵名慣例或解析方針作為套件 API 提供時,才需要考慮。

屬性的角色是決定要注入的值

實作了 Illuminate\Contracts\Container\ContextualAttribute 的屬性,會在 Laravel 容器解析引數時被讀取。傳給使用端類別的不是屬性物件本身,而是 resolver 回傳的值。 一般的綁定與內建屬性請參閱服務容器。情境屬性與 Bind、Singleton、Scoped 這類宣告類別或介面綁定的屬性是不同的機制。

建立讀取套件設定的屬性

註冊預設設定

首先,在套件的 config/courier.php 中準備預設值。
在 src/CourierServiceProvider.php 的 register() 中合併設定。提供者的自動註冊請參閱套件自動偵測的內部結構,發佈與設定快取的處理請參閱套件設定的合併與快取。

實作 ContextualAttribute

在 src/Attributes/CourierConfig.php 中,定義一個會在設定鍵前加上 courier. 的屬性。
在確認的實作中,resolver 會收到以下 3 個引數。
  1. 附加在引數上的屬性實例。
  2. 正在解析相依的容器。
  3. 代表解析對象的 ReflectionParameter。
此範例在省略鍵名時會使用引數名稱。#[CourierConfig(default: 30)] int $timeout 會參照 courier.timeout。如果希望即使使用端變更引數名稱也能讀取相同的設定,請像 #[CourierConfig('timeout', default: 30)] 這樣明確指定鍵名。 在確認的時間點,ContextualAttribute 是一個不宣告任何方法的標記介面。因此光是 implements 並不能保證 resolver 存在或其簽章正確。只要像上面這樣準備 resolve(),就不需要為這個屬性另外註冊一般的 bind()。
屬性的建構子只用來保存中繼資料。容器會在解析時與屬性的後處理時分別呼叫 newInstance(),因此這裡並不是「只執行一次的初始化」的地方。請不要在其中進行外部通訊或保存可變狀態。

從容器建立使用端類別

在 src/CourierOptions.php 中加上屬性。
從使用端應用程式覆寫設定後,下一次解析時就會注入該值。
此範例中 $options->timeout 為 45,$options->retries 為 3。屬性只負責取得設定,不會進行「是否為正整數」、「是否在允許的最大值以內」等業務上的驗證。型別宣告也無法取代設定值的驗證處理。

解讀建構子中的解析順序

Container::resolveDependencies() 會針對每個引數,依以下順序決定值。
  1. 使用透過 makeWith() 等方式傳入、與引數名稱相符的覆寫值。
  2. 以 Util::getContextualAttributeFromDependency() 尋找情境屬性並解析。
  3. 若屬性的結果為 null,則進入一般的原始值或類別解析。
  4. 經過屬性的後處理後,將決定的值傳給建構子。
第 1 步的覆寫路徑會略過該引數的屬性解析與後處理。 makeWith() 的鍵不是設定鍵,而是使用端類別的引數名稱。
在此情況下,無論設定值為何,timeout 都是 5。另一方面,當屬性回傳 null 以外的值時,屬性的結果會優先於一般的 when()->needs('$timeout')->give(...)。也請測試在套件中新增屬性,是否可能改變既有使用者的情境綁定。 Util 會透過 Reflection 取得實作了 ContextualAttribute 的屬性,並選擇其中的第一個用於解析。即使並列多個不同的情境屬性,resolver 也不會依序組合。

已註冊的 handler 優先於 resolve()

Container::resolveFromAttribute() 會以屬性類別名稱為鍵,先尋找以 whenHasAttribute() 註冊的 handler。若沒有,則使用屬性的 resolve();兩者都沒有時,會拋出 BindingResolutionException。 whenHasAttribute() 不是針對個別使用端類別,而是針對屬性種類的設定。對同一個屬性類別重新註冊,會覆寫先前的 handler。若套件無條件變更其他套件或 Laravel 內建屬性的 handler,會影響整個使用端應用程式。請將自訂屬性的標準行為放在 resolve() 中,並將透過 handler 的變更視為明確的擴充點。

make()、call() 與直接呼叫並不相同

app()->call() 也會解析屬性,但一般的 BoundMethod 路徑在找到屬性後,會直接將其結果加入引數。與建構子的解析不同,即使屬性的結果為 null,也不會回到預設值的解析。 以下是在沒有其他情境綁定的狀態下,讀取不存在的 courier.missing 的範例。
new CourierOptions(timeout: 5, retries: 1) 也是一般的 PHP 建立方式。光是加上屬性,並不會為直接的 new 或方法呼叫加入相依注入。
如果建構子與 callable 都需要相同的預設行為,請不要只依賴引數的預設值,而是像 #[CourierConfig('timeout', default: 30)] 這樣在屬性端定義預設值。此 default 會在設定鍵不存在時使用;若鍵存在但值為 null,則不會被取代。在將 null 本身視為有意義值的 API 中,請分別驗證這兩種解析路徑。

區分值的重新注入與服務的壽命

屬性讀取的是解析引數當下的設定。它不是用來監看或更新已建立的 CourierOptions 屬性的功能。 例如將 CourierOptions 設為 singleton 時,最初注入的值會保留在該實例中。之後即使變更設定並呼叫一般的 make(),同一個實例的值也不會改變。 建立讀取每個請求資訊的屬性時,屬性本身同樣不會決定 singleton 或 scoped 的壽命。請依照套件服務的生命週期設計,選擇保存注入值或物件之服務的壽命。也請將設定快取的重新產生,與長壽命行程或已建立服務的更新分開考量。

測試解析路徑

屬性的單元測試可以像官方測試一樣,使用新的 Container 與 Illuminate\Config\Repository 撰寫。以下 PHPUnit 範例以已載入上面定義的 CourierConfig、CourierOptions、NullableTimeoutOptions 的測試環境為前提。
將 handler 固定為 7 的範例僅用於測試優先順序。在這個容器內,它會影響同一屬性的所有解析,包括 retries。 僅靠這個單元測試,無法確認與套件的提供者註冊、mergeConfigFrom() 及設定快取的整合。也請在以 Orchestra Testbench 測試 Laravel 套件的應用程式環境中,驗證發佈前、已發佈、有設定快取等狀態。

發佈前的確認項目

  • 將屬性的類別名稱、建構子引數名稱、設定鍵、回傳型別視為公開 API。也要注意以具名引數使用的 key、default 的變更。
  • 若規格是在省略鍵名時使用引數名稱,請確認使用端類別變更引數名稱時,不會改變所參照的設定。
  • 分別測試沒有屬性的既有綁定、附屬性的解析,以及依引數名稱的覆寫。
  • 在 make() 與 call() 兩者中測試 null、設定不足、型別不正確的情況。
  • 確認設計不會讓 singleton 或長壽命 worker 保留舊的值。
  • 更新 Laravel 支援版本時,請透過官方原始碼與測試重新確認 resolver 的引數、優先順序與屬性的後處理。

相關頁面

套件設定的合併與快取

確認預設設定、已發佈設定與快取的更新邊界。

套件服務的生命週期設計

選擇保存注入值之服務的壽命。

套件的容器標籤與外掛彙整

確認內建的 Tag 屬性與已註冊實作的彙整。

PHP Reflection API

確認讀取屬性與引數中繼資料的機制。

參考的第一手資料

最後修改於 2026年10月31日