上文介绍了 Dependency Injection Containers (容器)
的基本概念,现在接着深入讲解 Laravel
的 Container
。Laravel
中实现的 Inversion of Control (IoC) / Dependency Injection (DI) Container
非常强悍,但文档中很低调的没有细讲它。
本文中示例基于 Laravel 5.5 ,其它版本差不多。
准备工作
1.Dependency Injection
关于 DI
请看这篇 《Laravel Dependency Injection (依赖注入) 概念详解》,这里不再赘述。
2. 初识 Container
Laravel
中有一大堆访问 Container
实例的姿势,比如最简单的:
$container = app();
但我们还是先关注下 Container
类本身。
Laravel 官方文档中一般使用 $this->app 代替 $container。它是 Application 类的实例,而 Application 类继承自 Container 类。
3. 在 Laravel 之外使用 Illuminate\Container
如果在 Laravel
之外
mkdir container && cd container composer require illuminate/container
// 新建一个 container.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; } }
接下来用 Container
的 make
方法来代替 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; } }
现在用 Container
的 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"。
实战
下面是可封装的 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) { // ... });
但这个估计只有 logging
和 debugging
才会用到。
技能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
被用来实例化类。意味着:
-
- 依赖
被注入进构造函数(或者方法); - 如果需要复用实例,可以定义为单例;
- 可以用接口或任何名称来代替具体类。
所以这样调用也可以生效:
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);
现在 PhotoController
和 VideoController
都依赖了 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/instanceisAlias()
– 确定给定的字符串是否是已注册的 别名
hasMethodBinding()
- 确定容器是否具有给定的 method binding
getBindings()
- 取回所有已注册绑定的原始数组getAlias($abstract)
- 获取基础类/绑定名称的别名forgetInstance($abstract)
- 清除单个实例对象forgetInstances()
- 清除所有实例对象flush()
- 清除所有绑定和实例,有效地重置容器setInstance()
- 替换 getInstance()
使用的实例 (提示:使用 setInstance(null)来清除它,这样下一次它将生成一个新的实例)
注意:这些方法不是 Container 接口 的一部分,只有在它的实现类 Container 才有。