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

原文地址

Laravel's Dependency Injection Container in Depthphp


下面是中文翻譯html


Laravel擁有強大的控制反轉(IoC)/依賴注入(DI) 容器。不幸的是官方文檔並無涵蓋全部可用的功能,所以,我決定嘗試寫文檔爲本身記錄一下。如下是基於Laravel 5.4.26,其餘版本可能有所不一樣。laravel

依賴注入簡介

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

訪問容器

在Laravel中有幾種訪問Container實例的方法,但最簡單的方法是調用app()helper方法:github

$container = app();

我今天不會描述其餘方式,而是我想專一於Container類自己。數據庫

注意: 若是你讀了官方文檔,它使用$this->app代替$container數組

(在Laravel應用程序中,它其實是Container的一個子類,稱爲Application這就是爲何稱爲助手app(),可是這篇文章,我只會描述Container方法)緩存

在Laravel外使用 IlluminateContainer

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

而後:閉包

use Illuminate\Container\Container;

$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 Illuminate\Container\Container;

$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 Illuminate\Container\Container;

$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(GitHub\Client::class, function (Container $container) {
    $client = new GitHub\Client;
    $client->setEnterpriseUrl(GITHUB_HOST);
    return $client;
});

解決回調

你可使用resolving()去註冊一個用於綁定完成後的回調函數:

$container->resolving(GitHub\Client::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

相關文章
相關標籤/搜索