Laravel Container (容器) 深刻理解 (下)

本文大部分翻譯自 DAVE JAMES MILLER 的 《Laravel’s Dependency Injection Container in Depth》php

上文介紹了 Dependency Injection Containers (容器) 的基本概念,如今接着深刻講解 LaravelContainerhtml

Laravel 中實現的 Inversion of Control (IoC) / Dependency Injection (DI) Container 很是強悍,但文檔中很低調的沒有細講它。laravel

本文中示例基於 Laravel 5.5 ,其它版本差很少。git

準備工做

1.Dependency Injection

關於 DI 請看這篇 《Laravel Dependency Injection (依賴注入) 概念詳解》,這裏再也不贅述。github

2. 初識 Container

Laravel 中有一大堆訪問 Container 實例的姿式,好比最簡單的:數組

$container = app();

但咱們仍是先關注下 Container 類自己。緩存

Laravel 官方文檔中通常使用 $this->app 代替 $container。它是 Application 類的實例,而 Application 類繼承自 Container 類。bash

3. 在 Laravel 以外使用 Illuminate\Container

若是在 Laravel 以外session

mkdir container && cd container
composer require illuminate/container
// 新建一個 container.php,文件名隨便取
<?php
include './vendor/autoload.php';

use Illuminate\Container\Container;
$container = Container::getInstance();

Container 的技能們

技能Q. 基本用法,用type hint (類型提示) 注入 依賴:

只須要在本身類的構造函數中使用 type hint 就實現 DI閉包

class MyClass
{
    private $dependency;

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

接下來用 Containermake 方法來代替 new MyClass:

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

Container 會自動實例化依賴的對象,因此它等同於:

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

若是 AnotherClass 也有 依賴,那麼 Container 會遞歸注入它所需的依賴。

Container 使用 Reflection (反射) 來找到並實例化構造函數參數中的那些類,實現起來並不複雜,之後的文章裏再介紹。

實戰

下面是 PHP-DI 文檔 中的一個例子,它分離了「用戶註冊」和「發郵件」的過程:

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)
    {
        // 建立用戶帳戶
        // ...

        // 給用戶的郵箱發個 「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!');

技能W. Binding Interfaces to Implementations (綁定接口到實現)

Container 能夠輕鬆地寫一個接口,而後在運行時實例化一個具體的實例。 首先定義接口:

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

而後聲明實現這些接口的具體類。下面這個類不但實現了一個接口,還依賴了實現另外一個接口的類實例:

class MyClass implements MyInterface
{
    private $dependency;

    // 依賴了一個實現 AnotherInterface 接口的類的實例
    public function __construct(AnotherInterface $dependency)
    {
        $this->dependency = $dependency;
    }
}

如今用 Containerbind() 方法來讓每一個 接口 和實現它的類一一對應起來:

$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"。

實戰

下面是可封裝的 Cache 層:

interface 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();

這裏用 Redis 作緩存,若是改用其餘緩存,只要把 RedisCache 換成別的就好了,easy!

技能E:Binding Abstract & Concret Classes (綁定抽象類和具體類):

綁定還能夠用在抽象類:

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

或者繼承的類中:

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

技能R:自定義綁定

若是類須要一些附加的配置項,能夠把 bind() 方法中的第二個參數換成 Closure (閉包函數)

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

閉包也可用於定製 具體類 的實例化方式:

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

技能T:Resolving Callbacks (回調)

可用 resolveing() 方法來註冊一個 callback (回調函數),而不是直接覆蓋掉以前的 綁定。 這個函數會在綁定的類解析完成以後調用。

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

若是有一大堆 callbacks,他們所有都會被調用。對於 接口抽象類 也能夠這麼用:

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

diao 的是,還能夠註冊成「什麼類解析完以後都調用」:

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

但這個估計只有 loggingdebugging 纔會用到。

技能Y:Extending a Class (擴展一個類)

使用 extend() 方法,能夠封裝一個類而後返回一個不一樣的對象 (裝飾模式):

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

注意:這兩個類要實現相同的 接口,否則用類型提示的時候會出錯:

interface Getable
{
    public function get();
}
class APIClient implements Getable
{
    public function get()
    {
        return 'yes!';
    }
}
class APIClientDecorator implements Getable
{
    private $client;

    public function __construct(APIClient $client)
    {
        $this->client = $client;
    }

    public function get()
    {
        return 'no!';
    }
}
class User
{
    private $client;

    public function __construct(Getable $client)
    {
        $this->client = $client;
    }
}
$container->extend(APIClient::class, function ($client, Container $container) {
    return new APIClientDecorator($client);
});
//
$container->bind(Getable::class, APIClient::class);

// 此時 $instance 的 $client 屬性已是 APIClentDecorator 類型了
$instance = $container->make(User::class);

技能U:單例

使用 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);

技能I:Arbitrary Binding Names (任意綁定名稱)

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);

技能O:保存任何值

Container 還能夠用來保存任何值,例如 configuration 數據:

$container->instance('database.name', 'testdb');
$db_name = $container->make('database.name');

它支持數組訪問語法,這樣用起來更天然:

$container['database.name'] = 'testdb';
$db_name = $container['database.name'];

這是由於 Container 實現了 PHP 的 ArrayAccess 接口。

當處理 Closure 綁定的時候,你會發現這個方式很是好用:

$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 用了。

數組訪問語法還能夠代替 make() 來實例化對象:

$db = $container['database'];

技能P:Dependency Injection for Functions & Methods (給函數或方法注入依賴)

除了給構造函數注入依賴,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']);
// 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]);

均可以注入。

技能A: 調用實例方法的快捷方式

使用 ClassName@methodName 語法能夠快捷調用實例中的方法:

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

由於Container 被用來實例化類。意味着:

  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 用這種方式來處理 event handlers :

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

// 至關於:
$container->call('MyEventHandler@handle', $parameters);

技能S:Method Call Bindings (方法調用綁定)

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 :-(']);

注意:這種方式不是 Container 接口 的一部分,只有在它的實現類 Container 纔有。在這個 PR` 裏能夠看到它加了什麼以及爲何參數被忽略。

技能D:Contextual Bindings (上下文綁定)

有時候你想在不一樣的地方給接口不一樣的實現。這裏有 Laravel 文檔 裏的一個例子:

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

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

如今 PhotoControllerVideoController 都依賴了 Filesystem 接口,可是收到了不一樣的實例。

能夠像 bind() 那樣,給 give() 傳閉包:

->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');

技能F:Binding Parameters to Primitives (綁定初始數據)

當有一個類不只須要接受一個注入類,還須要注入一個基本值(好比整數)。
還能夠經過將變量名稱 (而不是接口) 傳遞給 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'];
    });

技能G: Tagging (標記)

Container 能夠用來「標記」有關係的綁定:

$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']);

技能H:Rebinding (從新綁定)

這個功能不多用到,能夠跳過,僅供參考。

在綁定或實例被使用以後又發生了變化,將調用一個 rebinding 方法。 下例中, Auth 使用 Session 類後,Session 類將被替換,此時須要通知 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

Rebinding 的更多信息能夠看這兩個連接:
https://stackoverflow.com/questions/38974593/laravels-ioc-container-rebinding-abstract-types
https://code.tutsplus.com/tutorials/digging-in-to-laravels-ioc-container--cms-22167

還有一個 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;
});

注意:這種方式不是 Container 接口 的一部分,只有在它的實現類 Container 纔有。

技能J:Overriding Constructor Parameters (重寫構造函數參數)

makeWith 方法容許將附加參數傳遞給構造函數。它忽略任何現有的實例或單例,能夠用於建立具備不一樣參數的類的多個實例,同時仍然注入依賴關係:

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

注意:Laravel 5.3 及如下使用 make($class, $parameters)Laravel 5.4 中移除了此方法,可是在 5.4.16 之後又從新加回來了 makeWith()

技能K:其它

這涵蓋了我認爲有用的全部方法,但僅僅是簡介,否則這篇文章就寫不完了。。。

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() 功能相似,差異在於只有在現有綁定不存在的狀況下才註冊綁定。 它通常被用在 package 中註冊一個可被用戶重寫的默認綁定。

$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() 以後它會被重置:

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

factory()

factory() 方法返回一個不須要參數並調用 make() 的閉包。

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

這個東西我也不知道有什麼用。。。

wrap()

wrap 方法包裝一個閉包,以便在執行時依賴關係被注入。 它接受一個數組參數; 返回的閉包不帶參數:

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

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

$username = $usernameGetter();

我也不知道它有啥用,由於返回的閉包沒帶回參數。。。

注意:這個方法不是 Container 接口` 的一部分,只有在它的實現類 Container 纔有。

afterResolving()

afterResolving() 方法做用與 resolving() 徹底相同,不一樣之處是 調用 「resolving」回調以後再調用 「afterResolving」回調。
不知道何時會用到它。。。

最後再附幾個

isShared() – 肯定一個給定的類型是一個 singleton/instance
isAlias() – 肯定給定的字符串是不是已註冊的 別名
hasMethodBinding() - 肯定容器是否具備給定的 method binding
getBindings() - 取回全部已註冊綁定的原始數組
getAlias($abstract) - 獲取基礎類/綁定名稱的別名
forgetInstance($abstract) - 清除單個實例對象
forgetInstances() - 清除全部實例對象
flush() - 清除全部綁定和實例,有效地重置容器
setInstance() - 替換 getInstance() 使用的實例 (提示:使用 setInstance(null)來清除它,這樣下一次它將生成一個新的實例)

注意:這些方法不是 Container 接口 的一部分,只有在它的實現類 Container 纔有。

原創。 全部 Laravel 文章均已收錄至 laravel-tips 項目。

相關文章
相關標籤/搜索