跳至內容

服務容器

簡介

Laravel 服務容器是一個強大的工具,用於管理類別的依賴關係並執行依賴注入。依賴注入是一個花俏的短語,基本上意思是:類別的依賴關係透過建構函式或某些情況下的「設定」方法「注入」到類別中。

讓我們來看一個簡單的例子

<?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 的服務。由於服務是注入的,因此我們可以在測試應用程式時輕鬆「模擬」或建立 AppleMusic 服務的虛擬實作。

深入了解 Laravel 服務容器對於建置強大、大型的應用程式以及為 Laravel 核心本身做出貢獻至關重要。

零配置解析

如果類別沒有依賴關係,或者只依賴於其他具體類別(而非介面),則無需指示容器如何解析該類別。例如,您可以將以下程式碼放在您的 routes/web.php 檔案中

<?php
 
class Service
{
// ...
}
 
Route::get('/', function (Service $service) {
die($service::class);
});

在這個範例中,點擊應用程式的 / 路由將自動解析 Service 類別並將其注入到路由的處理程式中。這是一個改變遊戲規則的功能。這表示您可以開發您的應用程式並利用依賴注入,而無需擔心程式碼膨脹的設定檔案。

值得慶幸的是,您在建置 Laravel 應用程式時編寫的許多類別都會透過容器自動接收它們的依賴關係,包括控制器事件監聽器中介層等等。此外,您可以在佇列任務handle 方法中指定依賴關係的型別提示。一旦您體驗到自動和零配置依賴注入的強大功能,就會覺得沒有它就無法進行開發。

何時使用容器

由於零配置解析,您通常會在路由、控制器、事件監聽器和其他地方指定依賴關係的型別提示,而無需手動與容器互動。例如,您可能會在路由定義中指定 Illuminate\Http\Request 物件的型別提示,以便您可以輕鬆存取目前的請求。即使我們從不需要與容器互動來編寫此程式碼,它也會在幕後管理這些依賴關係的注入

use Illuminate\Http\Request;
 
Route::get('/', function (Request $request) {
// ...
});

在許多情況下,由於自動依賴注入和門面,您可以建置 Laravel 應用程式,而無需手動綁定或從容器解析任何內容。那麼,您會在什麼時候手動與容器互動? 讓我們檢查兩種情況。

首先,如果您編寫了一個實作介面的類別,並且您希望在路由或類別建構函式中指定該介面的型別提示,則必須告訴容器如何解析該介面。其次,如果您正在編寫一個您計劃與其他 Laravel 開發人員分享的 Laravel 套件,您可能需要將套件的服務綁定到容器中。

綁定

綁定基礎

簡單綁定

幾乎所有您的服務容器綁定都會在服務提供者中註冊,因此這些範例中的大多數範例都會示範在該環境中使用容器。

在服務提供者中,您始終可以透過 $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 門面來執行

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));
});
lightbulb

如果類別不依賴任何介面,則無需將類別綁定到容器中。容器無需指示如何建構這些物件,因為它可以使用反射自動解析這些物件。

綁定單例模式

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));
});

綁定範圍內的單例模式

scoped 方法會將類別或介面綁定到容器中,使其在指定的 Laravel 請求或任務生命週期內只解析一次。雖然此方法與 singleton 方法類似,但使用 scoped 方法註冊的實例,會在 Laravel 應用程式啟動新的「生命週期」時被清除,例如當 Laravel Octane 工作程序處理新的請求,或當 Laravel 佇列工作程序處理新的任務時。

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));
});

綁定實例

您也可以使用 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 介面。請記住,Laravel 應用程式中的控制器、事件監聽器、中介軟體以及各種其他類型的類別,始終是使用容器解析的。

use App\Contracts\EventPusher;
 
/**
* Create a new class instance.
*/
public function __construct(
protected EventPusher $pusher,
) {}

上下文綁定

有時您可能有兩個類別使用相同的介面,但您希望將不同的實作注入到每個類別中。例如,兩個控制器可能依賴於 Illuminate\Contracts\Filesystem\Filesystem 合約的不同實作。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 提供了各種上下文綁定屬性,允許注入這些類型的值,而無需在您的服務提供者中手動定義上下文綁定。

例如,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 還提供 AuthCacheConfigDBLogRouteParameterTag 屬性。

<?php
 
namespace App\Http\Controllers;
 
use App\Models\Photo;
use Illuminate\Container\Attributes\Auth;
use Illuminate\Container\Attributes\Cache;
use Illuminate\Container\Attributes\Config;
use Illuminate\Container\Attributes\DB;
use Illuminate\Container\Attributes\Log;
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,
#[DB('mysql')] protected Connection $connection,
#[Log('daily')] protected LoggerInterface $log,
#[RouteParameter('photo')] protected Photo $photo,
#[Tag('reports')] protected iterable $reports,
)
{
// ...
}
}

此外,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 合約來建立自己的上下文屬性。容器將呼叫您的屬性的 resolve 方法,該方法應解析應注入到使用該屬性的類別中的值。在下面的範例中,我們將重新實作 Laravel 的內建 Config 屬性。

<?php
 
namespace App\Attributes;
 
use Illuminate\Contracts\Container\ContextualAttribute;
 
#[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
* @return mixed
*/
public static function resolve(self $attribute, Container $container)
{
return $container->make('config')->get($attribute->key, $attribute->default);
}
}

綁定基本型別

有時您可能有一個類別接收一些注入的類別,但也需要注入一個原始值(例如整數)。您可以輕鬆地使用上下文綁定來注入您的類別可能需要的任何值。

use App\Http\Controllers\UserController;
 
$this->app->when(UserController::class)
->needs('$variableName')
->give($value);

有時,類別可能依賴於 標記的實例陣列。使用 giveTagged 方法,您可以輕鬆注入該標籤的所有容器綁定。

$this->app->when(ReportAggregator::class)
->needs('$reports')
->giveTagged('reports');

如果您需要從您的應用程式設定檔中注入一個值,您可以使用 giveConfig 方法。

$this->app->when(ReportAggregator::class)
->needs('$timezone')
->giveConfig('app.timezone');

綁定型別化的可變參數

有時,您可能有一個類別透過可變參數建構子引數接收類型物件的陣列。

<?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)。使用 needsgiveTagged 方法,您可以輕鬆地為給定的依賴關係注入該標籤的所有容器綁定。

$this->app->when(ReportAggregator::class)
->needs(Report::class)
->giveTagged('reports');

標記

有時,您可能需要解析某個「類別」的所有綁定。例如,您可能正在建構一個報告分析器,該分析器接收許多不同 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 方法允許修改已解析的服務。例如,當解析服務時,您可以執行額外的程式碼來裝飾或配置服務。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 Facadesapp 輔助函數來從容器解析類別實例。

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 可呼叫的內容。容器的 call 方法甚至可以用來呼叫一個回調函數,同時自動注入其依賴關係。

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 的實例。