• 深入讲解 Laravel 的 IoC 服务容器


    众所周知,Laravel 控制反转 (IoC) / 依赖注入 (DI) 的功能非常强大。遗憾的是, 官方文档 并没有详细讲解它的所有功能,所以我决定自己实践一下,并整理成文。下面的代码是基于 Laravel 5.4.26 的,其他版本可能会有所不同。

    了解依赖注入

    我在这里不会详细讲解依赖注入/控制反转的原则 - 如果你对此还不是很了解,建议阅读 Fabien Potencier (Symfony 框架的创始人)的 What is Dependency Injection?

    访问容器

    通过 Laravel 访问 Container 实例的方式有很多种,最简单的就是调用辅助函数 app()

    1 $container = app();

    为了突出重点 Container 类,这里就不赘述其他方式了。

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

    (* 在 Laravel 应用中,Application 实际上是 Container 的一个子类 ( 这也说明了辅助函数 app() 的由来 ),不过在这篇文章中我还是将重点讲解 Container 类的方法。)

    在 Laravel 之外使用 IlluminateContainer

    想要不基于 Laravel 使用 Container,安装 然后:

    use IlluminateContainerContainer;
    
    $container = Container::getInstance();

    基础用法

    最简单的用法是通过构造函数注入依赖类。

    1 class MyClass
    2 {
    3     private $dependency;
    4 
    5     public function __construct(AnotherClass $dependency)
    6     {
    7         $this->dependency = $dependency;
    8     }
    9 }

    使用 Container 的 make() 方法实例化 MyClass 类:

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

    container 会自动实例化依赖类,所以上面代码实现的功能就相当于:

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

    ( 假设 AnotherClass 还有需要依赖的类 - 在这种情况下,Container 会递归式地实例化所有的依赖。)

    实战

     phper在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货需要的可以免费分享给大家需要的(点击→)我的官方群677079770

    下面是一些基于 PHP-DI 文档 的例子 - 将发送邮件与用户注册的代码解耦:

     1 class Mailer
     2 {
     3     public function mail($recipient, $content)
     4     {
     5         // 发送邮件
     6         // ...
     7     }
     8 }
     9 class UserManager
    10 {
    11     private $mailer;
    12 
    13     public function __construct(Mailer $mailer)
    14     {
    15         $this->mailer = $mailer;
    16     }
    17 
    18     public function register($email, $password)
    19     {
    20         // 创建用户账号
    21         // ...
    22 
    23         // 给用户发送问候邮件
    24         $this->mailer->mail($email, 'Hello and welcome!');
    25     }
    26 }
    27 use IlluminateContainerContainer;
    28 
    29 $container = Container::getInstance();
    30 
    31 $userManager = $container->make(UserManager::class);
    32 $userManager->register('dave@davejamesmiller.com', 'MySuperSecurePassword!');

    绑定接口与具体实现

    通过 Container 类,我们可以轻松实现从接口到具体类到实例的过程。首先定义接口:

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

    声明实现接口的具体类,具体类还可以依赖其他接口( 或者是像上个例子中的具体类 ):

    class MyClass implements MyInterface
    1 {
    2     private $dependency;
    3 
    4     public function __construct(AnotherInterface $dependency)
    5     {
    6         $this->dependency = $dependency;
    7     }
    8 }
    
    

    然后使用 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),这本身在语法上就是错误的。

    实战

    可更换的缓存层:

     1 interface Cache
     2 {
     3     public function get($key);
     4     public function put($key, $value);
     5 }
     6 class RedisCache implements Cache
     7 {
     8     public function get($key) { /* ... */ }
     9     public function put($key, $value) { /* ... */ }
    10 }
    11 class Worker
    12 {
    13     private $cache;
    14 
    15     public function __construct(Cache $cache)
    16     {
    17         $this->cache = $cache;
    18     }
    19 
    20     public function result()
    21     {
    22         // 应用缓存
    23         $result = $this->cache->get('worker');
    24 
    25         if ($result === null) {
    26             $result = do_something_slow();
    27 
    28             $this->cache->put('worker', $result);
    29         }
    30 
    31         return $result;
    32     }
    33 }
    34 use IlluminateContainerContainer;
    35 
    36 $container = Container::getInstance();
    37 $container->bind(Cache::class, RedisCache::class);
    38 
    39 $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 的实例作为参数,如果需要的话,还可以用它来实例化其他类:

    1 $container->bind(Logger::class, function (Container $container) {
    2     $filesystem = $container->make(Filesystem::class);
    3 
    4     return new FileLogger($filesystem, 'logs/error.log');
    5 });

    还可以通过闭包函数自定义要如何实例化某个类:

    1 $container->bind(GitHubClient::class, function (Container $container) {
    2     $client = new GitHubClient;
    3     $client->setEnterpriseUrl(GITHUB_HOST);
    4     return $client;
    5 });

    解析回调函数

    可以使用 resolving() 方法来注册一个回调函数,当绑定被解析的时候,就调用这个回调函数:

    1 $container->resolving(GitHubClient::class, function ($client, Container $container) {
    2     $client->setEnterpriseUrl(GITHUB_HOST);
    3 });

    所有的注册的回调函数都会被调用。这种方法也适用于接口和抽象类:

     1 $container->resolving(Logger::class, function (Logger $logger) {
     2     $logger->setLevel('debug');
     3 });
     4 
     5 $container->resolving(FileLogger::class, function (FileLogger $logger) {
     6     $logger->setFilename('logs/debug.log');
     7 });
     8 
     9 $container->bind(Logger::class, FileLogger::class);
    10 
    11 $logger = $container->make(Logger::class);

    还可以注册一个任何类被解析时都会被调用的回调函数 - 但是我想这可能仅适用于登录和调试:

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

    扩展类

    你还可以使用 extend() 方法把一个类与另一个类的实例进行绑定:

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

    这里返回的另外一个类应该也实现了同样的接口,否则会报错。

    单例绑定

    只要使用 bind() 方法进行绑定,每次用的时候,就会创建一个新的实例( 闭包函数就会被调用一次)。为了共用一个实例,可以使用 singleton() 方法来代替 bind() 方法:

    $container->singleton(Cache::class, RedisCache::class);

    或者是闭包:

    1 $container->singleton(Database::class, function (Container $container) {
    2     return new MySQLDatabase('localhost', 'testdb', 'user', 'pass');
    3 });

    为一个具体类创建单例,就只传这个类作为唯一的参数:

    $container->singleton(MySQLDatabase::class);

    在以上的每种情况下,单例对象都是一次创建,反复使用。如果想要复用的实例已经生成了,则可以使用 instance() 方法。例如,Laravel 就是用这种方式来确保Container 的实例有且仅有一个的:

    $container->instance(Container::class, $container);

    自定义绑定的名称

    其实,你可以使用任意字符串作为绑定的名称,而不一定非要用类名或者接口名 - 但是这样做的弊端就是不能使用类名实例化了,而只能使用 make() 方法:

    $container->bind('database', MySQLDatabase::class);
    
    $db = $container->make('database');

    为了同时支持类和接口,并且简化类名的写法,可以使用 alias() 方法:

    1 $container->singleton(Cache::class, RedisCache::class);
    2 $container->alias(Cache::class, 'cache');
    3 
    4 $cache1 = $container->make(Cache::class);
    5 $cache2 = $container->make('cache');
    6 
    7 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');

    除了依赖类,还可以传其他参数:

    1 function show_product(Cache $cache, $id, $tab = 'details') { /* ... */ }
    2 
    3 // show_product($cache, 1)
    4 $container->call('show_product', [1]);
    5 $container->call('show_product', ['id' => 1]);
    6 
    7 // show_product($cache, 1, 'spec')
    8 $container->call('show_product', [1, 'spec']);
    9 $container->call('show_product', ['id' => 1, 'tab' => 'spec']);

    可用于任何可调用的方法:

    闭包

    1 $closure = function (Cache $cache) { /* ... */ };
    2 
    3 $container->call($closure);

    静态方法

    1 class SomeClass
    2 {
    3     public static function staticMethod(Cache $cache) { /* ... */ }
    4 }
    5 $container->call(['SomeClass', 'staticMethod']);
    6 // 或者:
    7 $container->call('SomeClass::staticMethod');

    普通方法

    1 class PostController
    2 {
    3     public function index(Cache $cache) { /* ... */ }
    4     public function show(Cache $cache, $id) { /* ... */ }
    5 }
    6 $controller = $container->make(PostController::class);
    7 
    8 $container->call([$controller, 'index']);
    9 $container->call([$controller, 'show'], ['id' => 1]);

    调用实例方法的快捷方式

    通过这种语法结构 ClassName@methodName,就 可以达到实例化一个类并调用其方法的目:

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

    容器用于实例化类,这意味着:

    1. 依赖项被注入构造函数(以及方法)。
    2. 如果希望重用这个类,则可以将该类定义为单例类。
    3. 你可以使用接口或任意名称,而不是具体的类。

    例如,这将会启作用:

    1 class PostController
    2 {
    3     public function __construct(Request $request) { /* ... */ }
    4     public function index(Cache $cache) { /* ... */ }
    5 }
    6 $container->singleton('post', PostController::class);
    7 $container->call('post@index');

    最后,你可以将「默认方法」作为第三个参数。如果第一个参数是一个没有指定方法的类名,则将调用默认的方法。 Laravel 使用 事件处理 来实现:

    1 $container->call(MyEventHandler::class, $parameters, 'handle');
    2 
    3 // Equivalent to:
    4 $container->call('MyEventHandler@handle', $parameters);

    方法调用绑定

    可以使用 bindMethod() 方法重写方法调用,例如传递其他参数:

    1 $container->bindMethod('PostController@index', function ($controller, $container) {
    2     $posts = get_posts(...);
    3 
    4     return $controller->index($posts);
    5 });

    所有这些都会奏效,调用闭包而不是的原始方法:

    1 $container->call('PostController@index');
    2 $container->call('PostController', [], 'index');
    3 $container->call([new PostController, 'index']);

    但是, call() 的任何附加参数都不会传递到闭包中,因此不能使用它们。

    1 $container->call('PostController@index', ['Not used :-(']);

    注意: 这个方法不属于 容器接口, 只是具体的 容器类. 参考 提交的 PR 了解为什么忽略参数。

    上下文绑定

    有时候,你希望在不同的地方使用接口的不同实现。下面是来自 Laravel 文档 中的一个例子:

    1 $container
    2     ->when(PhotoController::class)
    3     ->needs(Filesystem::class)
    4     ->give(LocalFilesystem::class);
    5 
    6 $container
    7     ->when(VideoController::class)
    8     ->needs(Filesystem::class)
    9     ->give(S3Filesystem::class);

    现在, PhotoController 和 VideoController 都可以依赖于文件系统接口,但是每个都将接收不同的实现。你还可以为 give() 使用闭包,就像使用 bind() 一样:

    1 $container
    2     ->when(VideoController::class)
    3     ->needs(Filesystem::class)
    4     ->give(function () {
    5         return Storage::disk('s3');
    6     });

    或者命名依赖项:

    1 $container->instance('s3', $s3Filesystem);
    2 
    3 $container
    4     ->when(VideoController::class)
    5     ->needs(Filesystem::class)
    6     ->give('s3');

    将参数绑定基本类型

    你还可以通过将变量名称传递给 needs()(而不是接口)并将值传递给 give() 来绑定基本类型(字符串,整数等):

    1 $container
    2     ->when(MySQLDatabase::class)
    3     ->needs('$username')
    4     ->give(DB_USER);

    您可以使用闭包来延迟检索值,直到需要它:

    1 $container
    2     ->when(MySQLDatabase::class)
    3     ->needs('$username')
    4     ->give(function () {
    5         return config('database.user');
    6     });

    在这里你不能传递一个类或一个命名的依赖项(例如 give('database.user'))因为它将作为文字值返回 - 为此你必须使用一个闭包:

    1 $container
    2     ->when(MySQLDatabase::class)
    3     ->needs('$username')
    4     ->give(function (Container $container) {
    5         return $container['database.user'];
    6     });

    标记

    你可以使用容器 tag 来绑定相关标记:

    $container->tag(MyPlugin::class, 'plugin');
    $container->tag(AnotherPlugin::class, 'plugin');

    然后将所有标记的实例检索为数组:

    1 foreach ($container->tagged('plugin') as $plugin) {
    2     $plugin->init();
    3 }

    tag() 的参数都接受数组:

    1 $container->tag([MyPlugin::class, AnotherPlugin::class], 'plugin');
    2 $container->tag(MyPlugin::class, ['plugin', 'plugin.admin']);

    重新绑定

    *Note: 这是一个更高级的,只是很少需要-请随意跳过它! *

    在绑定或实例已经被使用后需要更改时,可以调用 rebinding() 回调 - 例如,此 Session 类在被 Auth 类使用后被替换,因此需要通知 Auth 类变化:

     1 $container->singleton(Auth::class, function (Container $container) {
     2     $auth = new Auth;
     3     $auth->setSession($container->make(Session::class));
     4 
     5     $container->rebinding(Session::class, function ($container, $session) use ($auth) {
     6         $auth->setSession($session);
     7     });
     8 
     9     return $auth;
    10 });
    11 
    12 $container->instance(Session::class, new Session(['username' => 'dave']));
    13 $auth = $container->make(Auth::class);
    14 echo $auth->username(); // dave
    15 $container->instance(Session::class, new Session(['username' => 'danny']));
    16 
    17 echo $auth->username(); // danny

    (有关重新绑定的更多信息, 看 这里这里.)

    refresh()

    还有一个快捷方法 refresh() 来处理这个常见模式:

    1 $container->singleton(Auth::class, function (Container $container) {
    2     $auth = new Auth;
    3     $auth->setSession($container->make(Session::class));
    4 
    5     $container->refresh(Session::class, $auth, 'setSession');
    6 
    7     return $auth;
    8 });

    它还返回现有实例或绑定(如果有的话),因此您可以这样做:

    1 // This only works if you call singleton() or bind() on the class
    2 $container->singleton(Session::class);
    3 
    4 $container->singleton(Auth::class, function (Container $container) {
    5     $auth = new Auth;
    6     $auth->setSession($container->refresh(Session::class, $auth, 'setSession'));
    7     return $auth;
    8 });

    (就个人而言,我发现这种语法更加混乱,并且更喜欢上面更详细的版本!)

    Note: 这些方法不属于 Container interface, 只有具体 Container class.

    覆盖构造函数参数

    makeWith() 方法允许你将其他参数传递给构造函数。 它忽略任何现有的实例或单例,并且在创建具有不同参数的类的多个实例时仍然有用,同时仍然注入依赖项:

    1 class Post
    2 {
    3     public function __construct(Database $db, int $id) { /* ... */ }
    4 }
    5 $post1 = $container->makeWith(Post::class, ['id' => 1]);
    6 $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。

    1 if (! $container->bound('database.user')) {
    2     // ...
    3 }
    4 
    5 还可以使用数组访问语法和 isset():
    6 
    7 if (! isset($container['database.user'])) {
    8     // ...
    9 }

    它可以用 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 方法接受一组参数, 返回的闭包没有参数:

    1 $cacheGetter = function (Cache $cache, $key) {
    2     return $cache->get($key);
    3 };
    4 
    5 $usernameGetter = $container->wrap($cacheGetter, ['username']);
    6 
    7 $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.

  • 相关阅读:
    在一个tomcat中配置多个tomcat服务器 111
    同一个tomcat部署多个项目11
    Tomcat部署多个项目及相关配置
    同一个tomcat部署多个项目
    Tomcat下部署多个项目
    Linux环境下在Tomcat上部署JavaWeb工程
    Linux命令详解之—pwd命令
    PWD
    C语言内存分配
    每天小练笔10-小和尚挑水(回溯法)
  • 原文地址:https://www.cnblogs.com/a609251438/p/11874024.html
Copyright © 2020-2023  润新知