Laravel: Bộ chứa dịch vụ (Service Container)

Giới thiệu

Vùng chứa dịch vụ Laravel là một công cụ mạnh mẽ để quản lý các lớp phụ thuộc và thực hiện việc chèn lớp phụ thuộc. Dependency injection là một cụm từ ưa thích về cơ bản có nghĩa là: các phụ thuộc của lớp được “đưa” vào lớp thông qua hàm tạo hoặc trong một số trường hợp là các phương thức “setter”.

Hãy xem một ví dụ đơn giản:

<?php

namespace

App\Http\Controllers;

use

App\Http\Controllers\Controller;

use

App\Repositories\UserRepository;

use

App\Models\User;

class

UserController

extends

Controller

{

/** * The user repository implementation. * * @var UserRepository */

protected

$users

;

/** * Create a new controller instance. * * @param UserRepository $users * @return void */

public

function

__construct

(UserRepository

$users

) {

$this

->

users

=

$users

; }

/** * Show the profile for the given user. * * @param int $id * @return Response */

public

function

show

(

$id

) {

$user

=

$this

->

users

->

find

(

$id

);

return

view

(

'user.profile'

, [

'user'

=>

$user

]); } }

Trong ví dụ này, UserController cần truy xuất người dùng từ một nguồn dữ liệu. Vì vậy, chúng ta sẽ đưa vào một dịch vụ có thể truy xuất người dùng. Trong bối cảnh này, UserRepository của ta rất có thể sử dụng Eloquent để lấy thông tin người dùng từ cơ sở dữ liệu. Tuy nhiên, vì kho lưu trữ được đưa vào, chúng ta có thể dễ dàng hoán đổi nó bằng một triển khai khác. Chúng ta cũng có thể dễ dàng “mô phỏng” hoặc tạo một triển khai giả UserRepository khi kiểm tra ứng dụng của chúng ta.

Hiểu biết sâu sắc về vùng chứa dịch vụ Laravel là điều cần thiết để xây dựng một ứng dụng lớn, mạnh mẽ, cũng như để đóng góp vào chính lõi Laravel.

Độ phân giải cấu hình bằng không

Nếu một lớp không có phụ thuộc hoặc chỉ phụ thuộc vào các lớp cụ thể khác (không phải giao diện), thì vùng chứa không cần được hướng dẫn về cách giải quyết lớp đó. Ví dụ: bạn có thể đặt mã sau trong file routes/web.php:

<?php

class

Service

{

//

} Route::

get

(

'/'

,

function

(Service

$service

) {

die

(

get_class

(

$service

)); });

Trong ví dụ này, việc đánh vào route / của ứng dụng của bạn sẽ tự động giải quyết lớp Service và đưa nó vào trình xử lý route của bạn. Như vậy thì đang có thay đổi. Nó có nghĩa là bạn có thể phát triển ứng dụng của mình và tận dụng lợi thế của việc đưa vào phụ thuộc mà không phải lo lắng về các tệp cấu hình bị cồng kềnh.

Rất may, nhiều lớp bạn sẽ viết khi xây dựng ứng dụng Laravel tự động nhận các phần phụ thuộc của chúng thông qua vùng chứa, bao gồm bộ điều khiển, trình nghe sự kiện, middleware, v.v. Ngoài ra, bạn có thể nhập gợi ý phụ thuộc trong phương thức handle của công việc được xếp hàng đợi. Một khi bạn thấy được sức mạnh của việc đưa vào phụ thuộc cấu hình tự động và zero, bạn sẽ cảm thấy không thể phát triển nếu không có nó.

Khi nào sử dụng container

Nhờ độ phân giải cấu hình zero, bạn thường sẽ nhập gợi ý phụ thuộc vào các route, bộ điều khiển, trình nghe sự kiện và các nơi khác mà không cần tương tác thủ công với vùng chứa. Ví dụ: bạn có thể nhập gợi ý đối tượng Illuminate\Http\Request trên định nghĩa route của mình để bạn có thể dễ dàng truy cập yêu cầu hiện tại. Mặc dù chúng ta không bao giờ phải tương tác với vùng chứa để viết mã này, nhưng nó đang quản lý việc đưa các phụ thuộc sau vào hậu trường:

use

Illuminate\Http\Request; Route::

get

(

'/'

,

function

(Request

$request

) {

// ...

});

Trong nhiều trường hợp, nhờ việc đưa vào các phụ thuộc tự động và facade, bạn có thể xây dựng các ứng dụng Laravel mà không bao giờ tự ràng buộc hoặc giải quyết bất cứ điều gì từ container. Vì vậy, khi nào bạn sẽ tương tác thủ công với vùng chứa? Hãy xem xét hai tình huống.

Đầu tiên, nếu bạn viết một lớp triển khai một giao diện và bạn muốn gõ-gợi ý giao diện đó trên một route hoặc phương thức tạo lớp, bạn phải cho vùng chứa biết cách giải quyết giao diện đó. Thứ hai, nếu bạn đang viết một gói Laravel mà bạn định chia sẻ với các nhà phát triển Laravel khác, bạn có thể cần phải ràng buộc các dịch vụ của gói vào vùng chứa.

Ràng buộc

Kiến thức cơ bản về ràng buộc

Ràng buộc đơn

Hầu hết tất cả các ràng buộc vùng chứa dịch vụ của bạn sẽ được đăng ký trong các nhà cung cấp dịch vụ, vì vậy hầu hết các ví dụ này sẽ chứng minh việc sử dụng vùng chứa trong ngữ cảnh đó.

Trong một nhà cung cấp dịch vụ, bạn luôn có quyền truy cập vào vùng chứa thông qua thuộc tính $this->app. Chúng ta có thể đăng ký một ràng buộc bằng cách sử dụng phương thức bind, chuyển lớp hoặc tên giao diện mà chúng ta muốn đăng ký cùng với một bao đóng trả về một thể hiện của lớp:

use

App\Services\Transistor;

use

App\Services\PodcastParser;

$this

->

app

->

bind

(Transistor::

class

,

function

(

$app

) {

return

new

Transistor

(

$app

->

make

(PodcastParser::

class

)); });

Lưu ý rằng chúng ta nhận chính vùng chứa như một đối số cho trình phân giải. Sau đó, chúng ta có thể sử dụng vùng chứa để giải quyết các phụ thuộc phụ của đối tượng mà chúng ta đang xây dựng.

Như đã đề cập, bạn thường sẽ tương tác với vùng chứa bên trong các nhà cung cấp dịch vụ; tuy nhiên, nếu bạn muốn tương tác với vùng chứa bên ngoài nhà cung cấp dịch vụ, bạn có thể làm như vậy thông qua facade App:

use

App\Services\Transistor;

use

Illuminate\Support\Facades\App; App::

bind

(Transistor::

class

,

function

(

$app

) {

// ...

});

Không cần ràng buộc các lớp vào vùng chứa nếu chúng không phụ thuộc vào bất kỳ giao diện nào. Vùng chứa không cần phải được hướng dẫn về cách xây dựng các đối tượng này, vì nó có thể tự động phân giải các đối tượng này bằng cách sử dụng phản chiếu (reflection).

Ràng buộc một Singleton

Phương thức singleton liên kết với một lớp hoặc giao diện vào container mà chỉ nên được giải quyết một lúc. Khi một liên kết singleton được giải quyết, cùng một cá thể đối tượng sẽ được trả về trong các lần gọi tiếp theo vào vùng chứa:

use

App\Services\Transistor;

use

App\Services\PodcastParser;

$this

->

app

->

singleton

(Transistor::

class

,

function

(

$app

) {

return

new

Transistor

(

$app

->

make

(PodcastParser::

class

)); });

Ràng buộc Singletons có phạm vi

Phương thức scoped liên kết với một lớp hoặc giao diện vào container mà chỉ nên được giải quyết một lần trong vòng đời / yêu cầu Laravel đã cho. Mặc dù phương thức này tương tự như phương thức singleton, nhưng các phiên bản được đăng ký bằng phương thức scoped sẽ được xóa bất cứ khi nào ứng dụng Laravel bắt đầu một “vòng đời” mới, chẳng hạn như khi một nhân viên Laravel Octane xử lý một yêu cầu mới hoặc khi một nhân viên hàng đợi Laravel xử lý một công việc mới:

use

App\Services\Transistor;

use

App\Services\PodcastParser;

$this

->

app

->

scoped

(Transistor::

class

,

function

(

$app

) {

return

new

Transistor

(

$app

->

make

(PodcastParser::

class

)); });

Ràng buộc các đối tượng

Bạn cũng có thể liên kết một cá thể đối tượng hiện có vào vùng chứa bằng phương thức instance. Phiên bản đã cho sẽ luôn được trả về trong các lần gọi tiếp theo vào vùng chứa:

use

App\Services\Transistor;

use

App\Services\PodcastParser;

$service

=

new

Transistor

(

new

PodcastParser

);

$this

->

app

->

instance

(Transistor::

class

,

$service

);

Ràng buộc các giao diện với các triển khai

Một tính năng rất mạnh của vùng chứa dịch vụ là khả năng liên kết một giao diện với một triển khai nhất định. Ví dụ, giả sử chúng ta có một giao diện là EventPusher và một triển khai là RedisEventPusher. Khi chúng ta đã mã hóa triển khai RedisEventPusher của giao diện này, chúng ta có thể đăng ký nó với vùng chứa dịch vụ như sau:

use

App\Contracts\EventPusher;

use

App\Services\RedisEventPusher;

$this

->

app

->

bind

(EventPusher::

class

, RedisEventPusher::

class

);

Câu lệnh này cho vùng chứa biết rằng nó sẽ đưa vào RedisEventPusher khi một lớp cần triển khai EventPusher. Bây giờ chúng ta có thể gõ-gợi ý giao diện EventPusher trong phương thức khởi tạo của một lớp được phân giải bởi vùng chứa. Hãy nhớ rằng bộ điều khiển, trình nghe sự kiện, phần mềm trung gian và nhiều loại lớp khác trong các ứng dụng Laravel luôn được giải quyết bằng cách sử dụng vùng chứa:

use

App\Contracts\EventPusher;

/** * Create a new class instance. * * @param \App\Contracts\EventPusher $pusher * @return void */

public

function

__construct

(EventPusher

$pusher

) {

$this

->

pusher

=

$pusher

; }

Ràng buộc các ngữ cảnh

Đôi khi bạn có thể có hai lớp sử dụng cùng một giao diện, nhưng bạn muốn đưa các triển khai khác nhau vào mỗi lớp. Ví dụ, hai bộ điều khiển có thể phụ thuộc vào các triển khai khác nhau của hợp đồng (contract) Illuminate\Contracts\Filesystem\Filesystem. Laravel cung cấp một giao diện đơn giản, mượt để xác định hành vi này:

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'

); });

Ràng buộc nguyên thủy

Đôi khi bạn có thể có một lớp nhận một số lớp được chèn vào, nhưng cũng cần một giá trị nguyên thủy được chèn vào chẳng hạn như một số nguyên. Bạn có thể dễ dàng sử dụng liên kết theo ngữ cảnh để đưa vào bất kỳ giá trị nào mà lớp của bạn có thể cần:

$this

->

app

->

when

(

'App\Http\Controllers\UserController'

) ->

needs

(

'$variableName'

) ->

give

(

$value

);

Đôi khi một lớp có thể phụ thuộc vào một mảng các cá thể được gắn thẻ. Sử dụng phương thức giveTagged bạn có thể dễ dàng chèn tất cả các liên kết vùng chứa với thẻ đó:

$this

->

app

->

when

(ReportAggregator::

class

) ->

needs

(

'$reports'

) ->

giveTagged

(

'reports'

);

Nếu bạn cần nhập giá trị từ một trong các tệp cấu hình của ứng dụng, bạn có thể sử dụng giveConfigphương pháp:

$this

->

app

->

when

(ReportAggregator::

class

) ->

needs

(

'$timezone'

) ->

giveConfig

(

'app.timezone'

);

Đa dạng hóa ràng buộc đã nhập

Đôi khi, bạn có thể có một lớp nhận một mảng các đối tượng đã nhập bằng cách sử dụng một đối số phương thức khởi tạo khác nhau:

<?php

use

App\Models\Filter;

use

App\Services\Logger;

class

Firewall

{

/** * The logger instance. * * @var \App\Services\Logger */

protected

$logger

;

/** * The filter instances. * * @var array */

protected

$filters

;

/** * Create a new class instance. * * @param \App\Services\Logger $logger * @param array $filters * @return void */

public

function

__construct

(Logger

$logger

, Filter ...

$filters

) {

$this

->

logger

=

$logger

;

$this

->

filters

=

$filters

; } }

Sử dụng ràng buộc theo ngữ cảnh, bạn có thể giải quyết sự phụ thuộc này bằng cách cung cấp phương thức give với một bao đóng trả về một mảng các thể hiện Filter đã giải quyết :

$this

->

app

->

when

(Firewall::

class

) ->

needs

(Filter::

class

) ->

give

(

function

(

$app

) {

return

[

$app

->

make

(NullFilter::

class

),

$app

->

make

(ProfanityFilter::

class

),

$app

->

make

(TooLongFilter::

class

), ]; });

Để thuận tiện, bạn cũng có thể chỉ cung cấp một mảng tên lớp để vùng chứa giải quyết bất cứ khi nào Firewall cần các thể hiện Filter:

$this

->

app

->

when

(Firewall::

class

) ->

needs

(Filter::

class

) ->

give

([ NullFilter::

class

, ProfanityFilter::

class

, TooLongFilter::

class

, ]);

Các phụ thuộc vào thẻ đa dạng

Đôi khi một lớp có thể có một phụ thuộc khác nhau được gợi ý kiểu như một lớp nhất định (Report ...$reports). Sử dụng các phương thức needs và giveTagged bạn có thể dễ dàng chèn tất cả các liên kết vùng chứa với thẻ đó cho phần phụ thuộc đã cho:

$this

->

app

->

when

(ReportAggregator::

class

) ->

needs

(Report::

class

) ->

giveTagged

(

'reports'

);

Gắn thẻ

Đôi khi, bạn có thể cần phải giải quyết tất cả một “danh mục” ràng buộc nhất định. Ví dụ: có lẽ bạn đang xây dựng một trình phân tích báo cáo nhận được một loạt các triển khai giao diện Report khác nhau. Sau khi đăng ký triển khai Report, bạn có thể gán thẻ cho chúng bằng phương thức tag:

$this

->

app

->

bind

(CpuReport::

class

,

function

() {

//

});

$this

->

app

->

bind

(MemoryReport::

class

,

function

() {

//

});

$this

->

app

->

tag

([CpuReport::

class

, MemoryReport::

class

],

'reports'

);

Khi các dịch vụ đã được gắn thẻ, bạn có thể dễ dàng giải quyết tất cả chúng thông qua phương thức tagged của vùng chứa:

$this

->

app

->

bind

(ReportAnalyzer::

class

,

function

(

$app

) {

return

new

ReportAnalyzer

(

$app

->

tagged

(

'reports'

)); });

Mở rộng ràng buộc

Phương thức extend cho phép việc sửa đổi các dịch vụ giải quyết. Ví dụ: khi một dịch vụ được giải quyết, bạn có thể chạy mã bổ sung để trang trí hoặc định cấu hình dịch vụ. Phương thức extend chấp nhận một closure, mà nên trả về dịch vụ sửa đổi, như là đối số duy nhất của nó. Closure sẽ nhận được dịch vụ đang được giải quyết và thể hiện của vùng chứa:

$this

->

app

->

extend

(Service::

class

,

function

(

$service

,

$app

) {

return

new

DecoratedService

(

$service

); });

Giải quyết

Phương thức make

Bạn có thể sử dụng phương thức make để giải quyết một cá thể lớp từ vùng chứa. Phương thức make chấp nhận tên của lớp hoặc giao diện bạn muốn quyết tâm:

use

App\Services\Transistor;

$transistor

=

$this

->

app

->

make

(Transistor::

class

);

Nếu một số phụ thuộc lớp của bạn không thể giải quyết được thông qua vùng chứa, bạn có thể chèn chúng bằng cách chuyển chúng dưới dạng một mảng liên kết vào phương thức makeWith. Ví dụ: chúng ta có thể truyền thủ công đối số phương thức tạo $id theo yêu cầu của dịch vụ Transistor:

use

App\Services\Transistor;

$transistor

=

$this

->

app

->

makeWith

(Transistor::

class

, [

'id'

=>

1

]);

Nếu bạn ở bên ngoài nhà cung cấp dịch vụ ở vị trí mã của bạn không có quyền truy cập vào biến $app, bạn có thể sử dụng facade App để giải quyết một đối tượng lớp từ vùng chứa:

use

App\Services\Transistor;

use

Illuminate\Support\Facades\App;

$transistor

= App::

make

(Transistor::

class

);

Nếu bạn muốn bản thân thể hiện vùng chứa Laravel được chèn vào một lớp đang được vùng chứa phân giải, bạn có thể nhập-gợi ý lớp Illuminate\Container\Container trên phương thức tạo của lớp:

use

Illuminate\Container\Container;

/** * Create a new class instance. * * @param \Illuminate\Container\Container $container * @return void */

public

function

__construct

(Container

$container

) {

$this

->

container

=

$container

; }

Ngoài ra, và quan trọng là bạn có thể nhập gợi ý sự phụ thuộc trong phương thức khởi tạo của một lớp được phân giải bởi vùng chứa, bao gồm controller, trình nghe sự kiện, middleware và hơn thế nữa. Ngoài ra, bạn có thể nhập gợi ý phụ thuộc trong phương thức handle của công việc được xếp hàng đợi. Trong thực tế, đây là cách hầu hết các đối tượng của bạn sẽ được giải quyết bởi vùng chứa.

Ví dụ: bạn có thể gõ-gợi ý một kho lưu trữ được xác định bởi ứng dụng của bạn trong phương thức khởi tạo của bộ điều khiển. Kho lưu trữ sẽ tự động được giải quyết và đưa vào lớp:

<?php

namespace

App\Http\Controllers;

use

App\Repositories\UserRepository;

class

UserController

extends

Controller

{

/** * The user repository instance. * * @var \App\Repositories\UserRepository */

protected

$users

;

/** * Create a new controller instance. * * @param \App\Repositories\UserRepository $users * @return void */

public

function

__construct

(UserRepository

$users

) {

$this

->

users

=

$users

; }

/** * Show the user with the given ID. * * @param int $id * @return \Illuminate\Http\Response */

public

function

show

(

$id

) {

//

} }

Gọi & Chèn phương thức

Đôi khi bạn có thể muốn gọi một phương thức trên một cá thể đối tượng trong khi vẫn cho phép vùng chứa tự động đưa vào các phụ thuộc của phương thức đó. Ví dụ, cho lớp sau:

<?php

namespace

App;

use

App\Repositories\UserRepository;

class

UserReport

{

/** * Generate a new user report. * * @param \App\Repositories\UserRepository $repository * @return array */

public

function

generate

(UserRepository

$repository

) {

// ...

} }

Bạn có thể gọi phương thức generate thông qua vùng chứa như sau:

use

App\UserReport;

use

Illuminate\Support\Facades\App;

$report

= App::

call

([

new

UserReport

,

'generate'

]);

Phương thức call chấp nhận bất kỳ lời gọi PHP nào. Phương thức call của vùng chứa thậm chí có thể được sử dụng để gọi một closure trong khi tự động đưa vào các phụ thuộc của nó:

use

App\Repositories\UserRepository;

use

Illuminate\Support\Facades\App;

$result

= App::

call

(

function

(UserRepository

$repository

) {

// ...

});

Sự kiện vùng chứa

Vùng chứa dịch vụ kích hoạt một sự kiện mỗi khi nó giải quyết một đối tượng. Bạn có thể nghe sự kiện này bằng phương thức resolving:

use

App\Services\Transistor;

$this

->

app

->

resolving

(Transistor::

class

,

function

(

$transistor

,

$app

) {

// Called when container resolves objects of type "Transistor"...

});

$this

->

app

->

resolving

(

function

(

$object

,

$app

) {

// Called when container resolves object of any type...

});

Như bạn có thể thấy, đối tượng đang được giải quyết sẽ được chuyển đến lệnh gọi lại, cho phép bạn thiết lập bất kỳ thuộc tính bổ sung nào trên đối tượng trước khi nó được trao cho người tiêu dùng của nó.

Vùng chứa dịch vụ của Laravel thực hiện giao diện PSR-11. Do đó, bạn có thể gõ gợi ý giao diện vùng chứa PSR-11 để có được một phiên bản của vùng chứa Laravel:

use

App\Services\Transistor;

use

Psr\Container\ContainerInterface; Route::

get

(

'/'

,

function

(ContainerInterface

$container

) {

$service

=

$container

->

get

(Transistor::

class

);

//

});

Một ngoại lệ được đưa ra nếu không thể giải quyết được số nhận dạng đã cho. Ngoại lệ sẽ là một đối tượng Psr\Container\NotFoundExceptionInterface nếu số nhận dạng không thể bị ràng buộc. Nếu mã định danh đã bị ràng buộc nhưng không thể được giải quyết, một phiên bản của Psr\Container\ContainerExceptionInterface sẽ được ném ra.