Service Container

Giới thiệu

Laravel service container là một công cụ rất mạnh trong việc quản lý các dependencies và thực hiện xử lý dependency injection. Dependency injection là một cụm từ thể hiện ý như này: các dependencies của class được "injected" vào trong class thông qua hàm khởi tạo hoặc trong một số trường hợp là quả các phương thức "setter".

Hãy xem ví dụ đơn giản dưới đây:

<?php

namespace App\Http\Controllers;

use App\User;
use App\Repositories\UserRepository;
use App\Http\Controllers\Controller;

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, cái UserController cần nhận thông tin người dùng từ source. Vì vậy, chúng ta sẽ inject vào một service có thể để nhận thông tin người dùng. Trong hoàn cảnh này, UserRepository giống như Eloquent để lấy lại thông tin người dùng từ cơ sở dữ liệu. Tuy nhiên, khi repository được inject, chúng ta có thể dễ dàng trao đổi chúng với thằng khác. Chúng ta cũng dễ dàng "mock", hoặc tạo một hành động giả của UserRepository khi testing.

Sự hiểu biết sau về Laravel service container là rất cần thiết cho việc phát triển ứng dụng mạnh mẽ và lớn, cũng như đóng góp cho Laravel.

Binding

Binding Basics

Hầu hết tất cả các service container binding của bạn sẽ được đăng lý trong service providers, vì vậy, trong bối cảnh này hầu hết ví dụ này sẽ chứng minh cách sử dụng container.

Không cần phải bind class vào trong container nếu nó không phụ thuộc vào bất kỳ interface nào. Container không cần được hướng dẫn để xây dựng các đối tượng, vì nó có thể tự động sử lý các đối tượng này sử dụng reflection.

Ví dụ Binding

Bên trong một service provider, bạn luôn luôn có quyền truy cập vào trong container thông qua thuộc tính $this->app. Chúng ta có thể đăng kí liên kết sử dụng phương thức bind, và truyền vào tên của class hay interface mà chúng ta muốn đăng kí cùng với Closure thực hiện trả về thể hiện của class đó:

$this->app->bind('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

Chú ý là chúng ta nhận được container như một đối số truyền vào cho resolver. Sau đó thì chúng ta có thể thực hiện resolve các dependencies con của đối tượng mà đang được xây dựng.

Binding A Singleton

Phương thức singleton thực hiện liên kết một class hay interface vào container mà chỉ cần thực hiện duy nhất một lần, và sau đó cùng một đối tượng sẽ được trả về trong các lần gọi tiếp theo vào trong container.

$this->app->singleton('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

Binding Instances

Bạn cũng có thể liên kết một instance đang tồn tại vào trong container sử dụng phương thức instance. Instance đó sẽ luôn luôn được trả về trong các lần gọi sau vào container:

$api = new HelpSpot\API(new HttpClient);

$this->app->instance('HelpSpot\Api', $api);

Binding Primitives

Thỉnh thoảng bạn có một class nhật một vài injected class khác, nhưng cũng cần một inject giá trị nguyên thủy như một số nguyên. Bạn có thể dễ dàng sử dụng binding để inject bất kỳ giá trị nào vào trong class nếu cần:

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

Binding Interfaces vào Implementations

Một tính năng tuyệt vởi của service container là nó có khả năng bind một interface thành một implementation. Ví dụ, giả sử chúng ta có interface EventPusher và một implementation RedisEventPusher. Khi đã có code của implementation RedisEventPusher cho interface, chúng ta có thể đăng ký nó với service container như sau:

$this->app->bind(
    'App\Contracts\EventPusher',
    'App\Services\RedisEventPusher'
);

Lệnh đó sẽ bảo container luôn luôn inject RedisEventPusher khi một class nào đó cần một implementations từ interfaceEventPusher. Chúng ta có thể type-hint interface EventPusher interface trong một constructor, hay bất cứ vị trí nào mà dependencies có thể được inject bởi service container:

use App\Contracts\EventPusher;

/**
 * Create a new class instance.
 *
 * @param    EventPusher  $pusher
 * @return  void
 */
public function __construct(EventPusher $pusher)
{
    $this->pusher = $pusher;
}

Contextual Binding

Đôi khi bạn sẽ có hai classes triển khai từ cùng một interface nhưng bạn muốn inject các implementations khác nhau vào các class. Ví dụ, hai controllers có thể phụ thuộc vào implementations khác nhau của Illuminate\Contracts\Filesystem\Filesystem contract. Laravel cung cấp một interface đơn giản và liền mạch cho việc khai báo hành vi này:

use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when(VideoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

Tagging

Thỉnh thoảng, bạn cần giải quyết tất cả các "category" của binding. Ví dụ, bạn đang xây dụng một tập báo cáo mà sẽ nhận một mảng danh sách các implementations khác nhau của interface Report. Sau khi đăng ký Report implementations, bạn có thể gán chúng vào một tag sử dụng phương thức tag:

$this->app->bind('SpeedReport', function () {
    //
});

$this->app->bind('MemoryReport', function () {
    //
});

$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');

Khi service đã được tag, bạn có thể dễ dàng resolve chúng qua phương thức tagged:

$this->app->bind('ReportAggregator', function ($app) {
    return new ReportAggregator($app->tagged('reports'));
});

Resolving

Phương thức make

Bạn có thể sử dụng phương thức make để resolve một thể hiện class ra khỏi container. Phương thức make nhận tên class hay interface bạn muốn thực hiện resolve:

$api = $this->app->make('HelpSpot\API');

Nếu bạn đang ở ví trị mà code của bạn không truy cập được biến $app, bạn có thể sử dụng helper global resolve:

$api = resolve('HelpSpot\API');

Tự động Injection

Ngoài ra, và cũng quang trọng, bạn có thể đơn giản "type-hint" dependency vào trong hàm constructor của class nó sẽ được resolved bởi container, gồm controllers, event listeners, queue jobs, middleware, và còn nữa. Trong thực tế, đây là cách giải quyết đối tượng của bạn sẽ được giải quyết bởi container.

Ví dụ, bạn có thể type-hint một repository được định nghĩa bởi ứng dụng trong hàm khởi tạo constructor của controller. Repository này sẽ tự động được resolv và inject vào class:

<?php

namespace App\Http\Controllers;

use App\Users\Repository as UserRepository;

class UserController extends Controller
{
    /**
     * The user repository instance.
     */
    protected $users;

    /**
     * Create a new controller instance.
     *
     * @param    UserRepository  $users
     * @return  void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     * Show the user with the given ID.
     *
     * @param    int  $id
     * @return  Response
     */
    public function show($id)
    {
        //
    }
}

Container Events

Service container sẽ bắn ra các event mỗi khi nó thực hiện resolves một đối tượng. Bạn có thể listen các event qua phương thức resolving:

$this->app->resolving(function ($object, $app) {
    // Called when container resolves object of any type...
});

$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
    // Called when container resolves objects of type "HelpSpot\API"...
});

Như bạn có thể thấy, đối tượng đang được resolve sẽ truyền lại vào trong hàm callback, cho phép bạn thiết lập các thuộc tính bổ sung nào vào trong object trước khi được trả lại cho bên sử dụng nó.

Nguồn: https://laravel.com/docs/5.3/container