【譯】深入研究Laravel的依賴注入容器

安小下發表於2019-02-16

原文地址

Laravel`s Dependency Injection Container in Depth


下面是中文翻譯


Laravel擁有強大的控制反轉(IoC)/依賴注入(DI) 容器。不幸的是官方文件並沒有涵蓋所有可用的功能,因此,我決定嘗試寫文件為自己記錄一下。以下是基於Laravel 5.4.26,其他版本可能有所不同。

依賴注入簡介

我不會嘗試在這裡解釋DI/IOC背後的原理,如果你不熟悉它們,你可能需要去閱讀由Fabien Potencier(Symfony框架作者)建立的什麼是依賴注入

訪問容器

在Laravel中有幾種訪問Container例項的方法,但最簡單的方法是呼叫app()helper方法:

$container = app();

我今天不會描述其他方式,而是我想專注於Container類本身。

注意: 如果你讀了官方文件,它使用$this->app代替$container

(在Laravel應用程式中,它實際上是Container的一個子類,稱為Application這就是為什麼稱為助手app(),但是這篇文章,我只會描述Container方法)

在Laravel外使用 IlluminateContainer

要在Laravel外使用Container,請安裝它

然後:

use IlluminateContainerContainer;

$container = Container::getInstance();

基本用法

最簡單的用法是用你想注入的類鍵入你的類的建構函式:

class MyClass
{
    private $dependency;

    public function __construct(AnotherClass $dependency)
    {
        $this->dependency = $dependency;
    }
}

然後new MyClass使用容器的make()方法。

$instance = $container->make(MyClass::class);

容器會自動例項化依賴關係,所以這在功能上等同於:

$instance = new MyClass(new AnotherClass());

(除了AnotherClass他自己的一些依賴關係,在這種情況下Container將遞迴例項化它們,直到沒有更多)

例項

以下是一個基於PHP-DI docs的更實用的示例,將郵件功能與使用者註冊分離:

class Mailer
{
    public function mail($recipient, $content)
    {
        // Send an email to the recipient
        // ...
    }
}
class UserManager
{
    private $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function register($email, $password)
    {
        // Create the user account
        // ...

        // Send the user an email to say hello!
        $this->mailer->mail($email, `Hello and welcome!`);
    }
}
use IlluminateContainerContainer;

$container = Container::getInstance();

$userManager = $container->make(UserManager::class);
$userManager->register(`dave@davejamesmiller.com`, `MySuperSecurePassword!`);

將介面(Interfaces)繫結到實現(Implementations)

Container可以很容易的編寫一個介面,然後在執行時例項化一個具體的實現,首先定義介面:

interface MyInterface { /* ... */ }
interface AnotherInterface { /* ... */ }

並宣告實現這些介面的具體類,他們可能依賴於其他介面(或以前的具體類)

class MyClass implements MyInterface
{
    private $dependency;

    public function __construct(AnotherInterface $dependency)
    {
        $this->dependency = $dependency;
    }
}

然後使用bind()去將每個介面對映到具體的類

$container->bind(MyInterface::class, MyClass::class);
$container->bind(AnotherInterface::class, AnotherClass::class);

最後通過將介面名代替類名去傳遞給make()

$instance = $container->make(MyInterface::class);

注意: 如果你忘記去繫結一個介面,你將會得到一個稍微神祕的致命錯誤:

Fatal error: Uncaught ReflectionException: Class MyInterface does not exist

這是因為容器會嘗試例項化interface (new MyInterface),而這不是一個有效的類。

例項

下面是一個實用的例子,一個可交換的快取層

interface Cache
{
    public function get($key);
    public function put($key, $value);
}
class RedisCache implements Cache
{
    public function get($key) { /* ... */ }
    public function put($key, $value) { /* ... */ }
}
class Worker
{
    private $cache;

    public function __construct(Cache $cache)
    {
        $this->cache = $cache;
    }

    public function result()
    {
        // Use the cache for something...
        $result = $this->cache->get(`worker`);

        if ($result === null) {
            $result = do_something_slow();

            $this->cache->put(`worker`, $result);
        }

        return $result;
    }
}
use IlluminateContainerContainer;

$container = Container::getInstance();
$container->bind(Cache::class, RedisCache::class);

$result = $container->make(Worker::class)->result();

繫結抽象類和具體類(Abstract & Concrete Classes)

Binding 也可以使用到 abstract 類:

$container->bind(MyAbstract::class, MyConcreteClass::class);

或者用一個子類替換一個具體的類:

$container->bind(MySQLDatabase::class, CustomMySQLDatabase::class);

自定義繫結

如果該類需要額外的配置,你可以傳遞一個閉包來代替類名作為bind()的第二個引數:

$container->bind(Database::class, function (Container $container) {
    return new MySQLDatabase(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS);
});

每次需要資料庫介面時,都會建立並使用一個新的MySQLDatabase例項,並使用指定的配置值。(要想共享單個例項,請參考下面的單例)閉包接收Container例項作為第一個引數,並且可以在需要時用於例項化其他類:

$container->bind(Logger::class, function (Container $container) {
    $filesystem = $container->make(Filesystem::class);

    return new FileLogger($filesystem, `logs/error.log`);
});

閉包也可以用來定製具體類如何例項化

$container->bind(GitHubClient::class, function (Container $container) {
    $client = new GitHubClient;
    $client->setEnterpriseUrl(GITHUB_HOST);
    return $client;
});

解決回撥

你可以使用resolving()去註冊一個用於繫結完成後的回撥函式:

$container->resolving(GitHubClient::class, function ($client, Container $container) {
    $client->setEnterpriseUrl(GITHUB_HOST);
});

如果有多個回撥,它們將全部被呼叫,它們也為介面和抽象類工作

$container->resolving(Logger::class, function (Logger $logger) {
    $logger->setLevel(`debug`);
});

$container->resolving(FileLogger::class, function (FileLogger $logger) {
    $logger->setFilename(`logs/debug.log`);
});

$container->bind(Logger::class, FileLogger::class);

$logger = $container->make(Logger::class);

也可以通過新增一個回撥來處理無論是哪個類被解析,總是呼叫該回撥函式。但是我認為他可能只能在日誌/除錯中使用:

$container->resolving(function ($object, Container $container) {
    // ...
});

擴充套件一個類

或者你可以使用extend()包裝類並返回一個不同的物件:

$container->extend(APIClient::class, function ($client, Container $container) {
    return new APIClientDecorator($client);
});

結果物件仍然應該實現相同的介面,否則使用型別提示會出錯。

單例(Singletons)

在使用自動繫結和bind()時,每次需要時都會建立一個新的例項(或者呼叫閉包)。想要共享一個例項,使用singleton() 代替 bind()

$container->singleton(Cache::class, RedisCache::class);

或者使用一個閉包:

$container->singleton(Database::class, function (Container $container) {
    return new MySQLDatabase(`localhost`, `testdb`, `user`, `pass`);
});

要讓一個具體的類成為例項,請傳遞該類且不需要傳遞第二個引數:

$container->singleton(MySQLDatabase::class);

在不同情況下,單例物件將在第一次需要時建立,然後在隨後每次需要時重用。如果你已經有一個例項,你想重用使用instance()方法代替。例如,Laravel使用它來確保無論什麼時候將單例項Container例項注入到類中都會返回它:

$container->instance(Container::class, $container);

任意繫結名稱

你可以使用任意字串而不是使用一個類/介面名稱,儘管你不能使用型別提示檢索它,但必須使用make()代替:

$container->bind(`database`, MySQLDatabase::class);

$db = $container->make(`database`);

要同時支援類/介面,請使用alias()

$container->singleton(Cache::class, RedisCache::class);
$container->alias(Cache::class, `cache`);

$cache1 = $container->make(Cache::class);
$cache2 = $container->make(`cache`);

assert($cache1 === $cache2);

儲存任意值

你也可以使用容器來儲存任意值,例如配置資料:

$container->instance(`database.name`, `testdb`);

$db_name = $container->make(`database.name`);

它支援陣列語法訪問,這使得他更自然:

$container[`database.name`] = `testdb`;

$db_name = $container[`database.name`];

當與閉包函式結合使用時,你可以看到為什麼這是有用的:

$container->singleton(`database`, function (Container $container) {
    return new MySQLDatabase(
        $container[`database.host`],
        $container[`database.name`],
        $container[`database.user`],
        $container[`database.pass`]
    );
});

(Laravel本是不使用容器進行配置,它使用一個單獨的Config類來代替,但是也是通過PHP-DI實現的)

Tip: 在例項化物件的時候,也可以使用陣列語法代替make():

$db = $container[`database`];

函式和方法(Functions & Methods)的依賴注入

到現在為止,我們已經看到了建構函式的依賴注入(DI),但是Laravel還支援任意函式的依賴注入(DI):

function do_something(Cache $cache) { /* ... */ }

$result = $container->call(`do_something`);

其他引數可以作為索引或關聯陣列傳遞:

function show_product(Cache $cache, $id, $tab = `details`) { /* ... */ }

// show_product($cache, 1)
$container->call(`show_product`, [1]);
$container->call(`show_product`, [`id` => 1]);

// show_product($cache, 1, `spec`)
$container->call(`show_product`, [1, `spec`]);
$container->call(`show_product`, [`id` => 1, `tab` => `spec`]);

這可以用於任意可呼叫的方法:

閉包

$closure = function (Cache $cache) { /* ... */ };

$container->call($closure);

靜態方法

class SomeClass
{
    public static function staticMethod(Cache $cache) { /* ... */ }
}
$container->call([`SomeClass`, `staticMethod`]);
// or:
$container->call(`SomeClass::staticMethod`);

例項方法

class PostController
{
    public function index(Cache $cache) { /* ... */ }
    public function show(Cache $cache, $id) { /* ... */ }
}
$controller = $container->make(PostController::class);

$container->call([$controller, `index`]);
$container->call([$controller, `show`], [`id` => 1]);

呼叫例項方法的快捷方式

有一個快捷方式來例項化一個類並一次呼叫一個方法,使用ClassName@methodName

$container->call(`PostController@index`);
$container->call(`PostController@show`, [`id` => 4]);

該容器用於例項化類,即:

  1. 依賴項注入到建構函式(以及方法)中。
  2. 如果你希望重用它,你可以將該類定義為單例。
  3. 你可以使用介面或任意名稱而不是具體類。

例如:

class PostController
{
    public function __construct(Request $request) { /* ... */ }
    public function index(Cache $cache) { /* ... */ }
}
$container->singleton(`post`, PostController::class);
$container->call(`post@index`);

最後,你可以傳遞一個“預設方法”作為第三個引數,如果第一個引數是沒有指定方法的類名,則會呼叫預設方法,Laravel使用它來實現事件處理

$container->call(MyEventHandler::class, $parameters, `handle`);

// Equivalent to:
$container->call(`MyEventHandler@handle`, $parameters);

方法呼叫繫結

bindMethod()方法可以用於重寫方法呼叫,例如傳遞其他引數:

$container->bindMethod(`PostController@index`, function ($controller, $container) {
    $posts = get_posts(...);

    return $controller->index($posts);
});

所有這些都可以通過使用閉包代替原始方法進行工作:

$container->call(`PostController@index`);
$container->call(`PostController`, [], `index`);
$container->call([new PostController, `index`]);

但是,任何多餘傳遞給call()的引數都不會傳遞到閉包中,因此無法使用他們。

$container->call(`PostController@index`, [`Not used :-(`]);

_Notes: 該方法不是 Container interface的一部分, 只適用於具體的 Container 類。為什麼忽略引數,請參閱PR

上下文繫結

有時候你想在不同的地方使用不同的介面實現,下面是Laravel 文件中的一個例子:

$container
    ->when(PhotoController::class)
    ->needs(Filesystem::class)
    ->give(LocalFilesystem::class);

$container
    ->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give(S3Filesystem::class);

現在,PhotoController和VideoController都可以依賴檔案系統介面,但是每個介面都會接受到不同的實現,你也可以像使用bind()一樣使用閉包give()

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

或者一個命名的依賴關係:

$container->instance(`s3`, $s3Filesystem);

$container
    ->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give(`s3`);

將引數繫結到原函式

你也可以通過傳遞變數名稱給needs()(而不是介面)和傳遞變數給give()來繫結原函式

$container
    ->when(MySQLDatabase::class)
    ->needs(`$username`)
    ->give(DB_USER);

你可以使用閉包來延遲檢索值直到需要用到它:

$container
    ->when(MySQLDatabase::class)
    ->needs(`$username`)
    ->give(function () {
        return config(`database.user`);
    });

在這裡,你不能傳遞一個類或者一個命名依賴(例如give(`database.user`)),因為它會作為一個字面值返回,要做到這一點,你將不得不使用閉包:

$container
    ->when(MySQLDatabase::class)
    ->needs(`$username`)
    ->give(function (Container $container) {
        return $container[`database.user`];
    });

做標記

你可以使用容器去“標記”相關的繫結:

$container->tag(MyPlugin::class, `plugin`);
$container->tag(AnotherPlugin::class, `plugin`);

然後以陣列方式檢索所有標記的例項:

foreach ($container->tagged(`plugin`) as $plugin) {
    $plugin->init();
}

tag()的兩個引數也可以傳遞陣列:

$container->tag([MyPlugin::class, AnotherPlugin::class], `plugin`);
$container->tag(MyPlugin::class, [`plugin`, `plugin.admin`]);

重新繫結

_Note: 這個更高階一點,但是很少用到,可以跳過它

打工繫結或者例項已經被使用後,rebinding()呼叫一個回撥函式。例如,這裡的session類在被Auth類使用後被替換,所以Auth需要被告知更改:

$container->singleton(Auth::class, function (Container $container) {
    $auth = new Auth;
    $auth->setSession($container->make(Session::class));

    $container->rebinding(Session::class, function ($container, $session) use ($auth) {
        $auth->setSession($session);
    });

    return $auth;
});

$container->instance(Session::class, new Session([`username` => `dave`]));
$auth = $container->make(Auth::class);
echo $auth->username(); // dave
$container->instance(Session::class, new Session([`username` => `danny`]));

echo $auth->username(); // danny

(有關重新繫結的更多資訊,請檢視 這裡這裡.)

重新整理

還有一種更便捷的方法來處理這種模式,通過refresh()

$container->singleton(Auth::class, function (Container $container) {
    $auth = new Auth;
    $auth->setSession($container->make(Session::class));

    $container->refresh(Session::class, $auth, `setSession`);

    return $auth;
});

它也返回現有的例項或繫結(如果有的話),所以你可以這樣做:

// This only works if you call singleton() or bind() on the class
$container->singleton(Session::class);

$container->singleton(Auth::class, function (Container $container) {
    $auth = new Auth;
    $auth->setSession($container->refresh(Session::class, $auth, `setSession`));
    return $auth;
});

(我個人覺得這個語法更令人困惑,並且更喜歡上面的更詳細的版本)

Note: 這些方法不是 Container interface的一部分, 只是具體的Container class.

重寫建構函式引數

makeWith()方法允許您將其他引數傳遞給建構函式,她忽略了任何現有的例項或單例,並且可以用於建立具有不同引數的類的多個例項,同時依然注入依賴關係:

class Post
{
    public function __construct(Database $db, int $id) { /* ... */ }
}
$post1 = $container->makeWith(Post::class, [`id` => 1]);
$post2 = $container->makeWith(Post::class, [`id` => 2]);

Note: 在 Laravel 5.3 以及以下版本中,它很簡單 make($class, $parameters), 但在 Laravel 5.4中被刪除, 但在5.4.16 被重新新增為 makeWith() 。 在Laravel 5.5 可能會 恢復到Laravel 5.3 語法.

其他方法

這裡涵蓋了我認為有用的所有方法,但只是為了整理一些內容。下面這些是對其餘共用方法的總結:

bound()

如果類或名稱使用bind(), singleton(), instance()alias()繫結,bound()將會返回true

if (! $container->bound(`database.user`)) {
    // ...
}

你還可以使用陣列語法和isset()訪問:

if (! isset($container[`database.user`])) {
    // ...
}

它可以使用unset()重置、刪除指定的繫結/例項/別名

unset($container[`database.user`]);
var_dump($container->bound(`database.user`)); // false

bindIf()

bindIf()bind()相同,除了他只在不存在繫結的情況下才回註冊繫結(請參見上面的bound()),它可以用於在包註冊中預設繫結,同事允許使用者覆蓋它:

$container->bindIf(Loader::class, FallbackLoader::class);

沒有singletonIf()方法,但是你可以使用bindIf($abstract, $concrete, true)實現它:

$container->bindIf(Loader::class, FallbackLoader::class, true);

或者全部寫出來:

if (! $container->bound(Loader::class)) {
    $container->singleton(Loader::class, FallbackLoader::class);
}

resolved()

如果一個類已經被解析,resolved()方法返回true

var_dump($container->resolved(Database::class)); // false
$container->make(Database::class);
var_dump($container->resolved(Database::class)); // true

我不確定他有什麼用處,如果使用unset()它會被重置(請看上面的bound()

unset($container[Database::class]);
var_dump($container->resolved(Database::class)); // false

factory()

factory()方法返回一個不帶引數和呼叫的閉包make()

$dbFactory = $container->factory(Database::class);

$db = $dbFactory();

我不確定他有什麼用處

wrap()

wrap()方法封裝了一個閉包,以便在其執行時註冊他的依賴關係,wrap方法接收一個陣列引數,返回的閉包不帶引數:

$cacheGetter = function (Cache $cache, $key) {
    return $cache->get($key);
};

$usernameGetter = $container->wrap($cacheGetter, [`username`]);

$username = $usernameGetter();

我不確定他有什麼用處,因為閉包不需要引數

Note: 此方法不是Container interface的一部分, 只是具體的 Container class.

afterResolving()

afterResolving()方法的作用和resolving()類似,不同的點是在resolving()回撥後呼叫afterResolving。我不確定何時會用到。。。

最後

  • isShared() – 確定給定型別是否是共享單例/例項
  • isAlias() – 確定給定的字串是否是已註冊的別名
  • hasMethodBinding() – 確定容器是否具有給定的方法繫結
  • getBindings() – 檢索所有註冊繫結的原始陣列
  • getAlias($abstract) – 解析底層類/繫結名稱的別名
  • forgetInstance($abstract) – 清除單個例項物件
  • forgetInstances() – 清除所有例項物件
  • flush() – 清除所有繫結和例項,有效的重置容器
  • setInstance() – 使用getInstance()替換使用的例項

_Note: 最後一節的方法都不是 Container interface.的一部分


本文最初釋出於2017年6月15日的DaveJamesMiller.com

相關文章