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. 的屬性。
- 附加在引數上的屬性實例。
- 正在解析相依的容器。
- 代表解析對象的
ReflectionParameter。
#[CourierConfig(default: 30)] int $timeout 會參照 courier.timeout。如果希望即使使用端變更引數名稱也能讀取相同的設定,請像 #[CourierConfig('timeout', default: 30)] 這樣明確指定鍵名。
在確認的時間點,ContextualAttribute 是一個不宣告任何方法的標記介面。因此光是 implements 並不能保證 resolver 存在或其簽章正確。只要像上面這樣準備 resolve(),就不需要為這個屬性另外註冊一般的 bind()。
從容器建立使用端類別
在src/CourierOptions.php 中加上屬性。
$options->timeout 為 45,$options->retries 為 3。屬性只負責取得設定,不會進行「是否為正整數」、「是否在允許的最大值以內」等業務上的驗證。型別宣告也無法取代設定值的驗證處理。
解讀建構子中的解析順序
Container::resolveDependencies() 會針對每個引數,依以下順序決定值。
- 使用透過
makeWith()等方式傳入、與引數名稱相符的覆寫值。 - 以
Util::getContextualAttributeFromDependency()尋找情境屬性並解析。 - 若屬性的結果為
null,則進入一般的原始值或類別解析。 - 經過屬性的後處理後,將決定的值傳給建構子。
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 的測試環境為前提。
7 的範例僅用於測試優先順序。在這個容器內,它會影響同一屬性的所有解析,包括 retries。
僅靠這個單元測試,無法確認與套件的提供者註冊、mergeConfigFrom() 及設定快取的整合。也請在以 Orchestra Testbench 測試 Laravel 套件的應用程式環境中,驗證發佈前、已發佈、有設定快取等狀態。
發佈前的確認項目
- 將屬性的類別名稱、建構子引數名稱、設定鍵、回傳型別視為公開 API。也要注意以具名引數使用的
key、default的變更。 - 若規格是在省略鍵名時使用引數名稱,請確認使用端類別變更引數名稱時,不會改變所參照的設定。
- 分別測試沒有屬性的既有綁定、附屬性的解析,以及依引數名稱的覆寫。
- 在
make()與call()兩者中測試null、設定不足、型別不正確的情況。 - 確認設計不會讓 singleton 或長壽命 worker 保留舊的值。
- 更新 Laravel 支援版本時,請透過官方原始碼與測試重新確認 resolver 的引數、優先順序與屬性的後處理。
相關頁面
套件設定的合併與快取
確認預設設定、已發佈設定與快取的更新邊界。
套件服務的生命週期設計
選擇保存注入值之服務的壽命。
套件的容器標籤與外掛彙整
確認內建的 Tag 屬性與已註冊實作的彙整。
PHP Reflection API
確認讀取屬性與引數中繼資料的機制。
參考的第一手資料
- Laravel 官方文件:Defining custom attributes
- Laravel 官方文件:套件設定
- Container:resolveDependencies()、resolveFromAttribute()、whenHasAttribute()、屬性的後處理
- BoundMethod:callable 的引數解析
- Util:透過 Reflection 偵測 ContextualAttribute
- ContextualAttribute:標記介面
- Config:內建的設定屬性
- Config Repository:get() 與設定值的取得
- Arr:get() 的預設值與已存在 null 的處理
- 官方測試:屬性的 resolver、已註冊 handler、ReflectionParameter、call 中的解析