深刻講解 Laravel 的 IoC 服務容器

文章轉發自專業的Laravel開發者社區,原始連接:learnku.com/laravel/t/2…php

衆所周知,Laravel 控制反轉 (IoC) / 依賴注入 (DI) 的功能很是強大。遺憾的是, 官方文檔 並無詳細講解它的全部功能,因此我決定本身實踐一下,並整理成文。下面的代碼是基於 Laravel 5.4.26 的,其餘版本可能會有所不一樣。html

瞭解依賴注入

我在這裏不會詳細講解依賴注入/控制反轉的原則 - 若是你對此還不是很瞭解,建議閱讀 Fabien Potencier (Symfony 框架的創始人)的 What is Dependency Injection?laravel

訪問容器

經過 Laravel 訪問 Container 實例的方式有不少種,最簡單的就是調用輔助函數 app()git

$container = app();
複製代碼

爲了突出重點 Container 類,這裏就不贅述其餘方式了。github

注意:  官方文檔中使用的是  $this->app  而不是 $container數組

(* 在 Laravel 應用中,Application 其實是 Container 的一個子類 ( 這也說明了輔助函數 app() 的由來 ),不過在這篇文章中我仍是將重點講解 Container 類的方法。)緩存

在 Laravel 以外使用 Illuminate\Container

想要不基於 Laravel 使用 Container,安裝 而後:bash

use Illuminate\Container\Container;

$container = Container::getInstance();
複製代碼

基礎用法

最簡單的用法是經過構造函數注入依賴類。session

class MyClass
{
    private $dependency;

    public function __construct(AnotherClass $dependency)
    {
        $this->dependency = $dependency;
    }
}
複製代碼

使用 Container 的 make() 方法實例化 MyClass 類:閉包

$instance = $container->make(MyClass::class);
複製代碼

container 會自動實例化依賴類,因此上面代碼實現的功能就至關於:

$instance = new MyClass(new AnotherClass());
複製代碼

( 假設  AnotherClass 還有須要依賴的類 - 在這種狀況下,Container 會遞歸式地實例化全部的依賴。)

實戰

下面是一些基於 PHP-DI 文檔 的例子 - 將發送郵件與用戶註冊的代碼解耦:

class Mailer
{
    public function mail($recipient, $content)
    {
        // 發送郵件
        // ...
    }
}
複製代碼
class UserManager
{
    private $mailer;

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

    public function register($email, $password)
    {
        // 建立用戶帳號
        // ...

        // 給用戶發送問候郵件
        $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!');
複製代碼

綁定接口與具體實現

經過 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

複製代碼

這是由於 container 會嘗試實例化接口 ( 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()
    {
        // 應用緩存
        $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();
複製代碼

綁定抽象類與具體類

也能夠與抽象類進行綁定:

$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 類的實例的時候( 下面後講到如何經過 Singletons 建立一個能夠共享的實例),都要用到 Database 接口。咱們看到閉包函數接收了 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);
});
複製代碼

這裏返回的另一個類應該也實現了一樣的接口,不然會報錯。

單例綁定

只要使用 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 來存儲任何值 - 好比:配置數據:

$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 框架沒有用 container 來存儲配置文件,而是用了單獨的 Config 類 - 可是 PHP-DI 用了)

小貼士: 在實例化對象的時候,還能夠用數組的形式來代替 make() 方法:

$db = $container['database'];
複製代碼

經過方法 / 函數作依賴注入

到目前爲止,咱們已經看了不少經過構造函數進行依賴注入的例子,其實,Laravel 還支持對任何方法作依賴注入:

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']);
// 或者:
$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 :-(']);
複製代碼

注意: 這個方法不屬於 容器接口, 只是具體的 容器類. 參考 提交的 PR 瞭解爲何忽略參數。

上下文綁定

有時候,你但願在不一樣的地方使用接口的不一樣實現。下面是來自 Laravel 文檔 中的一個例子:

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

$container
    ->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give(S3Filesystem::class);
複製代碼

如今, PhotoController 和 VideoController 均可以依賴於文件系統接口,可是每一個都將接收不一樣的實現。你還能夠爲 give() 使用閉包,就像使用 bind() 同樣:

$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'];
    });
複製代碼

標記

你可使用容器 tag 來綁定相關標記:

$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()

還有一個快捷方法 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 被移除, 但後來 從新添加爲 makeWith() 在 5.4.16. 在Laravel 5.5中,它彷佛將恢復爲Laravel 5.3語法.

其餘方法

這涵蓋了我認爲有用的全部方法 - 但只是爲了解決問題,這裏是剩下的公共方法的摘要......

bound()

若是類或名稱已與 bind()singleton()instance() or 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() 徹底相同,只是在「解析」回調以後調用 「解析後」 回調。 我不肯定何時會有用...

最後...

  • isShared() - 肯定給定類型是否爲共享單例/實例
  • isAlias() - 肯定給定字符串是不是已註冊的別名
  • hasMethodBinding() - 肯定容器是否具備給定的方法綁定
  • getBindings() - 檢索全部已註冊綁定的原始數組
  • getAlias($abstract) - 解析基礎類/綁定名稱的別名
  • forgetInstance($abstract) - 清除單個實例對象
  • forgetInstances() - 清除全部實例對象
  • flush() - 清除全部綁定和實例,有效地重置容器
  • setInstance() - 替換 getInstance() 使用的實例(Tip:使用 setInstance(null) 清除它,因此下次它將生成一個新實例)

Note: 最後一節中沒有一個方法是其中的一部分 Container interface.

相關文章
相關標籤/搜索