【译】深入研究Laravel的依赖注入容器
原文地址Laravel's Dependency Injection Container in Depth 下面是中文翻译 Laravel拥有强大的控制反转(IoC)/依赖注入(DI) 容器。不幸的是官方文档并没有涵盖所有可用的功能,因此,我决定尝试写文档为自己记录一下。以下是基于Laravel 5.4.26,其他版本可能有所不同。 依赖注入简介我不会尝试在这里解释DI/IOC背后的原理,如果你不熟悉它们,你可能需要去阅读由Fabien Potencier(Symfony框架作者)创建的什么是依赖注入 访问容器在Laravel中有几种访问Container实例的方法,但最简单的方法是调用 $container = app(); 我今天不会描述其他方式,而是我想专注于Container类本身。 注意: 如果你读了官方文档,它使用 (在Laravel应用程序中,它实际上是Container的一个子类,称为Application这就是为什么称为助手 在Laravel外使用 IlluminateContainer要在Laravel外使用Container,请安装它 然后: use IlluminateContainerContainer; $container = Container::getInstance(); 基本用法最简单的用法是用你想注入的类键入你的类的构造函数: class MyClass { private $dependency; public function __construct(AnotherClass $dependency) { $this->dependency = $dependency; } } 然后 $instance = $container->make(MyClass::class); 容器会自动实例化依赖关系,所以这在功能上等同于: $instance = new MyClass(new AnotherClass()); (除了 实例以下是一个基于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; } } 然后使用 $container->bind(MyInterface::class,MyClass::class); $container->bind(AnotherInterface::class,AnotherClass::class); 最后通过将接口名代替类名去传递给 $instance = $container->make(MyInterface::class); 注意: 如果你忘记去绑定一个接口,你将会得到一个稍微神秘的致命错误: Fatal error: Uncaught ReflectionException: Class MyInterface does not exist 这是因为容器会尝试实例化interface ( 实例下面是一个实用的例子,一个可交换的缓存层 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); 自定义绑定如果该类需要额外的配置,你可以传递一个闭包来代替类名作为 $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; }); 解决回调你可以使用 $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) { // ... }); 扩展一个类或者你可以使用 $container->extend(APIClient::class,Container $container) { return new APIClientDecorator($client); }); 结果对象仍然应该实现相同的接口,否则使用类型提示会出错。 单例(Singletons)在使用自动绑定和 $container->singleton(Cache::class,RedisCache::class); 或者使用一个闭包: $container->singleton(Database::class,function (Container $container) { return new MySQLDatabase('localhost','testdb','user','pass'); }); 要让一个具体的类成为实例,请传递该类且不需要传递第二个参数: $container->singleton(MySQLDatabase::class); 在不同情况下,单例对象将在第一次需要时创建,然后在随后每次需要时重用。如果你已经有一个实例,你想重用使用 $container->instance(Container::class,$container); 任意绑定名称你可以使用任意字符串而不是使用一个 $container->bind('database',MySQLDatabase::class); $db = $container->make('database'); 要同时支持类/接口,请使用 $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: 在实例化对象的时候,也可以使用数组语法代替 $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]); 调用实例方法的快捷方式有一个快捷方式来实例化一个类并一次调用一个方法,使用 $container->call('PostController@index'); $container->call('PostController@show',['id' => 4]); 该容器用于实例化类,即:
例如: 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); 方法调用绑定
$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']); 但是,任何多余传递给 $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都可以依赖文件系统接口,但是每个接口都会接受到不同的实现,你也可以像使用 $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'); 将参数绑定到原函数你也可以通过传递变量名称给 $container ->when(MySQLDatabase::class) ->needs('$username') ->give(DB_USER); 你可以使用闭包来延迟检索值直到需要用到它: $container ->when(MySQLDatabase::class) ->needs('$username') ->give(function () { return config('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(); }
$container->tag([MyPlugin::class,AnotherPlugin::class],'plugin'); $container->tag(MyPlugin::class,['plugin','plugin.admin']); 重新绑定_Note: 这个更高级一点,但是很少用到,可以跳过它 打工绑定或者实例已经被使用后, $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 (有关重新绑定的更多信息,请查看 这里 和 这里.) 刷新还有一种更便捷的方法来处理这种模式,通过 $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,'setSession')); return $auth; }); (我个人觉得这个语法更令人困惑,并且更喜欢上面的更详细的版本) Note: 这些方法不是 Container interface的一部分,只是具体的Container class. 重写构造函数参数该 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 以及以下版本中,它很简单 其他方法这里涵盖了我认为有用的所有方法,但只是为了整理一些内容。下面这些是对其余共用方法的总结: bound()如果类或名称使用 if (! $container->bound('database.user')) { // ... } 你还可以使用数组语法和 if (! isset($container['database.user'])) { // ... } 它可以使用 unset($container['database.user']); var_dump($container->bound('database.user')); // false bindIf()
$container->bindIf(Loader::class,FallbackLoader::class); 没有 $container->bindIf(Loader::class,FallbackLoader::class,true); 或者全部写出来: if (! $container->bound(Loader::class)) { $container->singleton(Loader::class,FallbackLoader::class); } resolved()如果一个类已经被解析, var_dump($container->resolved(Database::class)); // false $container->make(Database::class); var_dump($container->resolved(Database::class)); // true 我不确定他有什么用处,如果使用 unset($container[Database::class]); var_dump($container->resolved(Database::class)); // false factory()该 $dbFactory = $container->factory(Database::class); $db = $dbFactory(); 我不确定他有什么用处 wrap()该 $cacheGetter = function (Cache $cache,$key) { return $cache->get($key); }; $usernameGetter = $container->wrap($cacheGetter,['username']); $username = $usernameGetter(); 我不确定他有什么用处,因为闭包不需要参数 Note: 此方法不是Container interface的一部分,只是具体的 Container class. afterResolving()
最后
_Note: 最后一节的方法都不是 Container interface.的一部分 本文最初发布于2017年6月15日的DaveJamesMiller.com (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |