服務容器
簡介
Laravel 服務容器是一個強大的工具,用於管理類別依賴關係並執行依賴注入。依賴注入是一個高深的詞彙,本質上的意思是:類別的依賴項是透過建構子,或者在某些情況下透過「setter」方法被「注入」到類別中。
讓我們來看一個簡單的範例:
<?php
namespace App\Http\Controllers;
use App\Services\AppleMusic;
use Illuminate\View\View;
class PodcastController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
protected AppleMusic $apple,
) {}
/**
* Show information about the given podcast.
*/
public function show(string $id): View
{
return view('podcasts.show', [
'podcast' => $this->apple->findPodcast($id)
]);
}
}在這個範例中,PodcastController 需要從諸如 Apple Music 之類的資料源取得 Podcast。因此,我們將注入一個能夠取得 Podcast 的服務。由於該服務是被注入的,當我們在測試應用程式時,能夠輕鬆地「mock」或建立 AppleMusic 服務的虛擬實作。
深入瞭解 Laravel 服務容器對於建構強大且大型的應用程式至關重要,同時也是為 Laravel 核心本身做出貢獻的必備知識。
零設定解析
如果一個類別沒有任何依賴,或者僅依賴於其他具體類別(而非介面),則容器不需要被指示如何解析該類別。例如,您可以在 routes/web.php 檔案中放置以下程式碼:
<?php
class Service
{
// ...
}
Route::get('/', function (Service $service) {
dd($service::class);
});在這個範例中,造訪應用程式的 / 路由將會自動解析 Service 類別,並將其注入到您的路由處理常式中。這是一項顛覆性的特色。這意味著您可以開發應用程式並善用依賴注入的好處,而無需擔心龐大的設定檔。
幸運的是,在建構 Laravel 應用程式時,您撰寫的許多類別都會自動透過容器接收其依賴,包括控制器、事件監聽器、中介層等等。此外,您還可以在佇列任務的 handle 方法中對依賴進行型別提示。一旦您體驗過自動化與零設定依賴注入的威力,就會發現沒有它簡直無法進行開發。
何時使用容器
得益於零設定解析,您通常會在路由、控制器、事件監聽器及其他地方對依賴項進行型別提示,而無需手動與容器進行互動。例如,您可以在路由定義中對 Illuminate\Http\Request 物件進行型別提示,以便輕鬆存取當前請求。儘管我們在撰寫此程式碼時從不需要手動操作容器,但它會在幕後管理這些依賴項的注入:
use Illuminate\Http\Request;
Route::get('/', function (Request $request) {
// ...
});在許多情況下,得益於自動依賴注入和 facades,您可以建構 Laravel 應用程式,而完全不需要手動從容器綁定或解析任何東西。那麼,究竟何時才會手動與容器互動呢?讓我們來看看兩種情況。
首先,如果您撰寫了一個實作介面的類別,並且希望在路由或類別建構子中對該介面進行型別提示,您必須告訴容器如何解析該介面。其次,如果您正在撰寫 Laravel 套件並打算與其他 Laravel 開發者分享,您可能需要將套件的服務綁定到容器中。
綁定
綁定基礎
簡單綁定
幾乎您所有的服務容器綁定都會在服務提供者(Service Providers)中註冊,因此大多數範例都會展示在該情境下如何使用容器。
在服務提供者中,您隨時可以透過 $this->app 屬性存取容器。我們可以使用 bind 方法來註冊綁定,傳入我們希望註冊的類別或介面名稱,以及一個傳回該類別實例的閉包:
use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
$this->app->bind(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});請注意,我們接收到容器本身作為解析器的引數。接著,我們便能使用容器來解析我們正在建構的物件的子依賴項。
如前所述,您通常會在服務提供者中與容器進行互動;然而,如果您想在服務提供者之外與容器互動,可以透過 App Facade 來完成:
use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\App;
App::bind(Transistor::class, function (Application $app) {
// ...
});您可以使用 bindIf 方法,僅在指定的型別尚未註冊任何綁定時,才註冊容器綁定:
$this->app->bindIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});為了方便起見,您可以省略將要註冊的類別或介面名稱作為獨立引數傳入,改為讓 Laravel 從您提供給 bind 方法的閉包傳回型別中推導出該型別:
App::bind(function (Application $app): Transistor {
return new Transistor($app->make(PodcastParser::class));
});📌 備註
如果類別不依賴於任何介面,就不需要將這些類別綁定到容器中。容器不需要被指示如何建構這些物件,因為它可以透過反射機制自動解析這些物件。
綁定單例
singleton 方法將類別或介面綁定至容器中,且只會被解析一次。一旦單例綁定被解析,後續呼叫容器時都會傳回相同的物件實例:
use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
$this->app->singleton(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});您可以使用 singletonIf 方法,僅在指定的型別尚未註冊任何綁定時,才註冊單例容器綁定:
$this->app->singletonIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});Singleton 屬性
或者,您可以為介面或類別加上 #[Singleton] 屬性,以指示容器該物件應僅被解析一次:
<?php
namespace App\Services;
use Illuminate\Container\Attributes\Singleton;
#[Singleton]
class Transistor
{
// ...
}綁定作用域單例
scoped 方法將類別或介面綁定到容器中,並使其在給定的 Laravel 請求 / 任務生命週期內僅被解析一次。雖然此方法與 singleton 方法類似,但使用 scoped 方法註冊的實例會在 Laravel 應用程式開始新的「生命週期」時被清除,例如當 Laravel Octane worker 處理新請求或 Laravel 佇列 worker 處理新任務時:
use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
$this->app->scoped(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});您可以使用 scopedIf 方法,僅在指定的型別尚未註冊任何綁定時,才註冊作用域容器綁定:
$this->app->scopedIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});Scoped 屬性
或者,您可以為介面或類別加上 #[Scoped] 屬性,以指示容器該物件在給定的 Laravel 請求 / 任務生命週期內應僅被解析一次:
<?php
namespace App\Services;
use Illuminate\Container\Attributes\Scoped;
#[Scoped]
class Transistor
{
// ...
}綁定實例
您也可以使用 instance 方法將現有的物件實例綁定到容器中。後續呼叫容器時將總是傳回該給定的實例:
use App\Services\Transistor;
use App\Services\PodcastParser;
$service = new Transistor(new PodcastParser);
$this->app->instance(Transistor::class, $service);將介面綁定至實作
服務容器一個非常強大的功能,就是能夠將介面綁定至給定的實作。例如,假設我們有一個 EventPusher 介面和一個 RedisEventPusher 實作。一旦編寫好該介面的 RedisEventPusher 實作後,我們就可以像這樣將其註冊至服務容器:
use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
$this->app->bind(EventPusher::class, RedisEventPusher::class);這個敘述告訴容器,當某個類別需要 EventPusher 的實作時,應該注入 RedisEventPusher。現在,我們可以在由容器解析的類別建構子中,對 EventPusher 介面進行型別提示 (Type-hint)。請記住,Laravel 應用程式中的控制器、事件監聽器、中介層以及其他各種型態的類別,都是透過容器來解析的:
use App\Contracts\EventPusher;
/**
* Create a new class instance.
*/
public function __construct(
protected EventPusher $pusher,
) {}Bind 屬性
為了更加方便,Laravel 還提供了 Bind 屬性。您可以將此屬性套用至任何介面,以告訴 Laravel 每當請求該介面時,應該自動注入哪一個實作。使用 Bind 屬性時,不需要在應用程式的服務提供者(Service Providers)中進行任何額外的服務註冊。
此外,可以在介面上放置多個 Bind 屬性,以便針對特定環境設定注入不同的實作:
<?php
namespace App\Contracts;
use App\Services\FakeEventPusher;
use App\Services\RedisEventPusher;
use Illuminate\Container\Attributes\Bind;
#[Bind(RedisEventPusher::class)]
#[Bind(FakeEventPusher::class, environments: ['local', 'testing'])]
interface EventPusher
{
// ...
}此外,還可以套用 Singleton 與 Scoped 屬性,以表示容器綁定應該僅解析一次,還是在每個請求 / 任務(Job)生命週期中解析一次:
use App\Services\RedisEventPusher;
use Illuminate\Container\Attributes\Bind;
use Illuminate\Container\Attributes\Singleton;
#[Bind(RedisEventPusher::class)]
#[Singleton]
interface EventPusher
{
// ...
}對於取決於任意條件的綁定,您可以使用 BindWhen 屬性。該 Closure 可以接收容器,並應在需要套用綁定時回傳 true。Bind 和 BindWhen 屬性會按照它們被宣告的順序進行評估:
use App\Services\BetaEventPusher;
use Illuminate\Container\Attributes\BindWhen;
use Laravel\Pennant\Feature;
#[BindWhen(BetaEventPusher::class, static fn () => Feature::active('beta-events'))]
interface EventPusher
{
// ...
}📌 備註
BindWhen 屬性需要 PHP 8.5 或更高版本。
情境綁定
有時候,您可能有兩個使用相同介面的類別,但您希望向每個類別注入不同的實作。例如,兩個控制器可能依賴於 Illuminate\Contracts\Filesystem\Filesystem 契約(Contracts) 的不同實作。Laravel 提供了一個簡單且流暢的介面來定義此行為:
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\UploadController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
$this->app->when([VideoController::class, UploadController::class])
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});情境屬性
由於情境綁定通常用於注入驅動程式實作或設定值,Laravel 提供了一系列情境綁定屬性 (Attributes),讓你無須在服務提供者中手動定義情境綁定,就能直接注入這些類型的數值。
例如,Storage 屬性可用於注入特定的儲存磁碟:
<?php
namespace App\Http\Controllers;
use Illuminate\Container\Attributes\Storage;
use Illuminate\Contracts\Filesystem\Filesystem;
class PhotoController extends Controller
{
public function __construct(
#[Storage('local')] protected Filesystem $filesystem
) {
// ...
}
}除了 Storage 屬性外,Laravel 還提供了 Auth、Cache、Config、Context、DB、Give、Log、RequestAttribute、RouteParameter 以及 Tag 屬性:
<?php
namespace App\Http\Controllers;
use App\Contracts\UserRepository;
use App\Models\Organization;
use App\Models\Photo;
use App\Repositories\DatabaseRepository;
use Illuminate\Container\Attributes\Auth;
use Illuminate\Container\Attributes\Cache;
use Illuminate\Container\Attributes\Config;
use Illuminate\Container\Attributes\Context;
use Illuminate\Container\Attributes\DB;
use Illuminate\Container\Attributes\Give;
use Illuminate\Container\Attributes\Log;
use Illuminate\Container\Attributes\RequestAttribute;
use Illuminate\Container\Attributes\RouteParameter;
use Illuminate\Container\Attributes\Tag;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Database\Connection;
use Psr\Log\LoggerInterface;
class PhotoController extends Controller
{
public function __construct(
#[Auth('web')] protected Guard $auth,
#[Cache('redis')] protected Repository $cache,
#[Config('app.timezone')] protected string $timezone,
#[Context('uuid')] protected string $uuid,
#[Context('ulid', hidden: true)] protected string $ulid,
#[DB('mysql')] protected Connection $connection,
#[Give(DatabaseRepository::class)] protected UserRepository $users,
#[Log('daily')] protected LoggerInterface $log,
#[RequestAttribute('organization')] protected Organization $organization,
#[RouteParameter] protected Photo $photo,
#[Tag('reports')] protected iterable $reports,
) {
// ...
}
}RouteParameter 屬性會解析與變數名稱比對相符的路由參數。如果需要,你也可以明確指定路由參數名稱:#[RouteParameter('photo')]。
RequestAttribute 屬性會解析當前請求的 屬性包 (attribute bag) 中儲存在指定鍵名下的值:#[RequestAttribute('organization')]。
此外,Laravel 還提供了 CurrentUser 屬性,用於將當前通過認證的使用者注入到指定的路由或類別中:
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
Route::get('/user', function (#[CurrentUser] User $user) {
return $user;
})->middleware('auth');定義自訂屬性
你可以透過實作 Illuminate\Contracts\Container\ContextualAttribute 契約(Contracts)來建立自訂的情境屬性。容器會呼叫屬性的 resolve 方法,該方法應該要解析出並回傳注入到使用該屬性之類別中的值。在以下範例中,我們將重新實作 Laravel 內建的 Config 屬性:
<?php
namespace App\Attributes;
use Attribute;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Container\ContextualAttribute;
use ReflectionParameter;
#[Attribute(Attribute::TARGET_PARAMETER)]
class Config implements ContextualAttribute
{
/**
* Create a new attribute instance.
*/
public function __construct(public string $key, public mixed $default = null)
{
}
/**
* Resolve the configuration value.
*
* @param self $attribute
* @param \Illuminate\Contracts\Container\Container $container
* @param \ReflectionParameter $parameter
* @return mixed
*/
public static function resolve(self $attribute, Container $container, ReflectionParameter $parameter)
{
return $container->make('config')->get($attribute->key, $attribute->default);
}
}綁定基本型別
有時候,你可能有一個類別除了接收注入的類別外,還需要注入一個基本型別的值(例如整數)。你可以輕鬆地使用情境綁定來注入該類別所需的任何數值:
use App\Http\Controllers\UserController;
$this->app->when(UserController::class)
->needs('$variableName')
->give($value);有時候,某個類別可能會依賴一個包含已標記 (tagged)實例的陣列。使用 giveTagged 方法,你可以輕鬆地注入帶有該標籤的所有容器綁定:
$this->app->when(ReportAggregator::class)
->needs('$reports')
->giveTagged('reports');如果你需要從應用程式的設定檔中注入一個值,你可以使用 giveConfig 方法:
$this->app->when(ReportAggregator::class)
->needs('$timezone')
->giveConfig('app.timezone');綁定型別可變參數
偶爾,你可能會有一個類別,透過可變建構子引數 (variadic constructor argument) 來接收指定型別的物件陣列:
<?php
use App\Models\Filter;
use App\Services\Logger;
class Firewall
{
/**
* The filter instances.
*
* @var array
*/
protected $filters;
/**
* Create a new class instance.
*/
public function __construct(
protected Logger $logger,
Filter ...$filters,
) {
$this->filters = $filters;
}
}使用情境綁定,你可以傳遞一個閉包給 give 方法來解析此依賴關係,該閉包需回傳由解析後的 Filter 實例組成的陣列:
$this->app->when(Firewall::class)
->needs(Filter::class)
->give(function (Application $app) {
return [
$app->make(NullFilter::class),
$app->make(ProfanityFilter::class),
$app->make(TooLongFilter::class),
];
});為了方便起見,當 Firewall 需要 Filter 實例時,你也可以直接提供一個類別名稱陣列,讓容器自動解析:
$this->app->when(Firewall::class)
->needs(Filter::class)
->give([
NullFilter::class,
ProfanityFilter::class,
TooLongFilter::class,
]);可變參數標籤依賴
有時候類別會有一個型別提示為指定類別的可變參數依賴(Report ...$reports)。使用 needs 與 giveTagged 方法,你可以輕鬆地為該依賴注入帶有該標籤的所有容器綁定:
$this->app->when(ReportAggregator::class)
->needs(Report::class)
->giveTagged('reports');標記
偶爾,你可能需要解析某個「類別種類」的所有綁定。例如,你可能正在建構一個報表分析器 (report analyzer),它會接收包含許多不同 Report 介面實作的陣列。在註冊了 Report 的實作後,你可以使用 tag 方法為它們分配一個標籤:
$this->app->bind(CpuReport::class, function () {
// ...
});
$this->app->bind(MemoryReport::class, function () {
// ...
});
$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');一旦服務被標記後,你就可以透過容器的 tagged 方法輕鬆解析它們全部:
$this->app->bind(ReportAnalyzer::class, function (Application $app) {
return new ReportAnalyzer($app->tagged('reports'));
});擴充綁定
extend 方法允許對已解析的服務進行修改。例如,當一個服務被解析時,您可以執行額外的程式碼來裝飾(Decorate)或設定該服務。extend 方法接受兩個引數:您要擴充的服務類別,以及一個應回傳修改後服務的閉包。該閉包會接收正在被解析的服務與容器實例:
$this->app->extend(Service::class, function (Service $service, Application $app) {
return new DecoratedService($service);
});解析
make 方法
你可以使用 make 方法從容器中解析出類別實例。make 方法接受你希望解析的類別或介面名稱:
use App\Services\Transistor;
$transistor = $this->app->make(Transistor::class);若類別的部分依賴無法透過容器解析,你可以將這些依賴以關聯陣列的方式傳入 makeWith 方法來進行注入。例如,我們可以手動傳入 Transistor 服務所需的建構函式引數 $id:
use App\Services\Transistor;
$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);可以使用 bound 方法來確認某個類別或介面是否已在容器中明確綁定:
if ($this->app->bound(Transistor::class)) {
// ...
}如果你處於服務提供者之外,且該程式碼位置無法存取 $app 變數,則可以使用 App Facade 或 app 輔助函式來從容器中解析類別實例:
use App\Services\Transistor;
use Illuminate\Support\Facades\App;
$transistor = App::make(Transistor::class);
$transistor = app(Transistor::class);如果你希望將 Laravel 容器實例本身注入到由容器解析的類別中,可以在該類別的建構函式中型別提示 Illuminate\Container\Container 類別:
use Illuminate\Container\Container;
/**
* Create a new class instance.
*/
public function __construct(
protected Container $container,
) {}自動注入
另外且非常重要的一點是,你可以在由容器解析的類別建構函式中型別提示其依賴,這些類別包含控制器、事件監聽器、中介層等等。此外,你也可以在佇列任務的 handle 方法中型別提示依賴。在實務上,這正是絕大多數物件透過容器解析的方式。
例如,你可以在控制器的建構函式中型別提示應用程式所定義的服務。該服務將會自動被解析並注入至該類別中:
<?php
namespace App\Http\Controllers;
use App\Services\AppleMusic;
class PodcastController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
protected AppleMusic $apple,
) {}
/**
* Show information about the given podcast.
*/
public function show(string $id): Podcast
{
return $this->apple->findPodcast($id);
}
}方法呼叫與注入
有時候,你可能希望呼叫物件實例上的某個方法,同時讓容器自動注入該方法的依賴。例如,假設有以下類別:
<?php
namespace App;
use App\Services\AppleMusic;
class PodcastStats
{
/**
* Generate a new podcast stats report.
*/
public function generate(AppleMusic $apple): array
{
return [
// ...
];
}
}你可以像這樣透過容器呼叫 generate 方法:
use App\PodcastStats;
use Illuminate\Support\Facades\App;
$stats = App::call([new PodcastStats, 'generate']);call 方法接受任何 PHP callable。容器的 call 方法甚至可用於呼叫 Closure,並自動注入其依賴:
use App\Services\AppleMusic;
use Illuminate\Support\Facades\App;
$result = App::call(function (AppleMusic $apple) {
// ...
});容器事件
服務容器每次解析物件時都會觸發一個事件。你可以使用 resolving 方法來監聽此事件:
use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
$this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) {
// Called when container resolves objects of type "Transistor"...
});
$this->app->resolving(function (mixed $object, Application $app) {
// Called when container resolves object of any type...
});如你所見,正在被解析的物件將會被傳遞給回呼函式,讓你在該物件提供給使用者之前,先為該物件設定任何額外屬性。
重新綁定
rebinding 方法讓你能夠監聽服務何時被重新綁定至容器,也就是在初始綁定之後再次註冊或覆蓋。當你需要每次更新特定綁定時一併更新依賴或修改行為,這會非常有用:
use App\Contracts\PodcastPublisher;
use App\Services\SpotifyPublisher;
use App\Services\TransistorPublisher;
use Illuminate\Contracts\Foundation\Application;
$this->app->bind(PodcastPublisher::class, SpotifyPublisher::class);
$this->app->rebinding(
PodcastPublisher::class,
function (Application $app, PodcastPublisher $newInstance) {
//
},
);
// New binding will trigger rebinding closure...
$this->app->bind(PodcastPublisher::class, TransistorPublisher::class);PSR-11
Laravel 的服務容器實作了 PSR-11 介面。因此,你可以型別提示 PSR-11 容器介面來取得 Laravel 容器實例:
use App\Services\Transistor;
use Psr\Container\ContainerInterface;
Route::get('/', function (ContainerInterface $container) {
$service = $container->get(Transistor::class);
// ...
});如果給定的識別碼無法解析,則會拋出例外。若識別碼從未綁定過,該例外將會是 Psr\Container\NotFoundExceptionInterface 的實例。若識別碼已綁定但無法解析,則會拋出 Psr\Container\ContainerExceptionInterface 的實例。