果冻想
认真玩技术的地方

Laravel初级教程之服务容器

简介

学习Laravel时,大家都在强调服务容器是Laravel的核心,既然是核心,那咱就有必要好好的学习一下。

作为Laravel框架的核心,那就先说说什么是服务容器。见名知意,服务容器就是一个存放服务的地方,当我们需要某个服务的时候,我们就可以从这个容器中取出我们需要的服务。这是最通俗易懂的方式。用更专业一点的术语来说,服务容器是这样的:

Laravel服务容器是一个用于管理类依赖和执行依赖注入的强大工具。

说到管理类依赖和执行依赖注入,搞过Spring开发,或者ASP.NET MVC开发的朋友,应该都熟悉,这些概念在这些年特别火。因为好用,所以火。现在这把火烧到了PHP这里。而说到这里,我们又不得不说两个特别重要的概念,也是面试的时候经常问的概念:IoC(控制反转)与DI(依赖注入)。

IoC(控制反转)与DI(依赖注入)

IoC(控制反转)与DI(依赖注入)是现在特别流行的概念,也是目前降低软件开发复杂度;提升模块低耦合、高内聚所使用的一种设计模式。

控制反转:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源;
依赖注入:应用程序依赖容器创建并注入它所需要的外部资源。

上面的概念很好理解。但是,真正面对大型工程,大片代码的时候,由于对原理的理解不是很透彻,就可能迷失在代码中。下面就通过一些简单的代码来配合说明IoC(控制反转)与DI(依赖注入),也好让大家真正的明白其中的含义。

控制反转是用来进行对象解耦。通过借助第三方,将具有依赖关系的对象进行解耦;而这个第三方就是IoC容器。

比如全体齿轮的转动由一个对象来控制,如类B。如下图所示:

对象耦合

引入了IoC容器后,对象A、B、C、D之间没有了依赖关系,全体齿轮转动的控制权交给IoC容器。这时候齿轮转动控制权不属于任何对象,而属于IoC容器,所以控制权反转了,从某个对象转到了IoC容器。

IoC容器

对于依赖注入,首先依赖就是指一种关系,如果在类A中创建了类B的实例,我们就可以说类A依赖类B。例如:

<?php
class B {
    public function func() {
        print_r(__CLASS__);
    }
}

class A {
    private $b;

    public function A() {
        $this->b = new B();
    }

    public function func() {
        $this->b->func();
    }
}

而使用这种方式,则会出现以下问题:

  • 问题1:如果现在要改变类B的创建方式,如需要用new B(String name)初始化B,需要修改类A中的源代码;
  • 问题2:如果想测试不同B对象对A的影响很困难,因为B的初始化被写死在了A的构造函数中;
  • 问题3:如果要对类B的实例进行调试时,就必须在类A中对类B的实例进行测试,增加了测试难度和复杂度;因为当出现问题时,不知道是类A的问题 还是类B的问题。

基于此,提出以下的解决方案:

class A {
    private $b;

    public function A(B $bObj) {
        $this->b = $bObj;
    }

    public function func() {
        $this->b->func();
    }
}

将B对象实例作为类A的构造器参数进行传入,在调用类A构造器之前,类B实例已经被初始化好了。像这种非自己主动初始化依赖,而通过外部传入依赖对象的方式,就很完美的解决了上述的问题,这就是依赖注入。

总结完了IoC(控制反转)与DI(依赖注入),很好的理解完之后,再来看看Laravel中如何使用服务容器。在Laravel中,服务容器主要承担两个作用:

  • 服务绑定;
  • 服务解析。

下面就从这两个大的方面来细细的说说Laravel是如何完成服务绑定和服务解析的。

服务绑定

一个类要被容器所能够提取,必须要先注册至这个容器。既然Laravel称这个容器叫做服务容器,那么我们需要某个服务,就得先注册、绑定这个服务到容器,那么提供服务并绑定服务至容器的东西,就是服务提供者(Service Provider),关于服务提供者的内容,我在下一篇文章中会细说;现在,我们记住一点即可:几乎所有的服务容器绑定都是在服务提供者中完成。

注:如果一个类没有基于任何接口那么就没有必要将其绑定到容器。容器并不需要被告知如何构建对象,因为它会使用PHP的反射服务自动解析出具体的对象。

一般在服务提供者中进行服务容器绑定都是通过以下几种方法:

  • bind绑定
    在一个服务提供者中,可以通过$this->app变量访问容器,然后使用bind方法注册一个绑定,该方法需要两个参数,第一个参数是我们想要注册的类名或接口名称,第二个参数是返回类实例的闭包:

    $this->app->bind('HelpSpot\API', function ($app) {
        return new HelpSpot\API();
    });
    
  • singleton单例绑定
    singleton方法绑定一个只需要解析一次的类或接口到容器,然后接下来对容器的调用将会返回同一个实例:

    $this->app->singleton('HelpSpot\API', function ($app) {
        return new HelpSpot\API();
    });
    

    对于全局需要使用同一个实例的场景,可以使用该方式来实现。

  • instance实例绑定
    我们还可以使用instance方法绑定一个已存在的对象实例到容器,随后调用容器将总是返回给定的实例:

    $api = new HelpSpot\API();
    $this->app->instance('HelpSpot\Api', $api);
    
  • 原始值绑定
    有时,我们的类不仅需要注入类,还需要注入一些原始数据,如一个整数。此时,可以很容易地通过情景绑定注入需要的任何值:

    $this->app->when('App\Http\Controllers\UserController')
    ->needs('$variableName')
        ->give($value);
    
  • 接口到实现的绑定
    服务容器的一个非常强大的功能是其绑定接口到实现。我们假设有一个EventPusher接口及其实现类RedisEventPusher,编写完该接口的RedisEventPusher实现后,就可以将其注册到服务容器:

    $this->app->bind(
        'App\Contracts\EventPusher', 
        'App\Services\RedisEventPusher'
    );
    

    这段代码告诉容器当一个类需要EventPusher的实现时将会注入RedisEventPusher,现在我们可以在构造器或者任何其它通过服务容器注入依赖的地方进行EventPusher接口的依赖注入:

    use App\Contracts\EventPusher;
    
    /**
     * 创建一个新的类实例
     *
     * @param  EventPusher  $pusher
     * @return void
     */
    public function __construct(EventPusher $pusher){
        $this->pusher = $pusher;
    }
    
  • 上下文绑定
    有时侯我们可能有两个类使用同一个接口,但我们希望在每个类中注入不同实现,例如,两个控制器依赖Illuminate\Contracts\Filesystem\Filesystem接口的不同实现。Laravel为此定义了简单、平滑的接口:

    use Illuminate\Support\Facades\Storage;
    use App\Http\Controllers\VideoController;
    use App\Http\Controllers\PhotoControllers;
    use Illuminate\Contracts\Filesystem\Filesystem;
    
    $this->app->when(PhotoController::class)
        ->needs(Filesystem::class)
        ->give(function () {
            return Storage::disk('local');
        });
    
    $this->app->when(VideoController::class)
        ->needs(Filesystem::class)
        ->give(function () {
            return Storage::disk('s3');
        });
    

服务解析

服务注册、绑定完成后,我们就可以根据我们的需要从IoC容器中“提取”对应的服务了。

一般情况下,我们会使用以下几种方式从IoC容器中获取服务。

  • make解析
    使用make方法,该方法接收我们想要解析的类名或接口名作为参数:

    $fooBar = $this->app->make('HelpSpot\API');
    
  • resolve解析
    有的时候,我们所处的代码位置无法访问$app变量,这个时候可以使用辅助函数resolve

    $api = resolve('HelpSpot\API');
    
  • 自动注入
    自动注入是我们使用最多的,我们可以简单的通过在类的构造函数中对依赖进行类型提示来从容器中解析对象。在实践中,这是大多数对象从容器中解析的方式。

    <?php
    namespace App\Http\Controllers;
    use App\Users\Repository as UserRepository;
    
    class UserController extends Controller{
      /**
      * 用户仓库实例
      */
      protected $users;
    
      /**
      * 创建一个控制器实例
      *
      * @param UserRepository $users 自动注入
      * @return void
      */
      public function __construct(UserRepository $users)
      {
          $this->users = $users;
      }
    }
    

容器事件

每当服务容器解析一个对象时就会触发一个事件。我们可以使用resolving方法监听这个事件:

$this->app->resolving(function ($object, $app) {
    // 解析任何类型的对象时都会调用该方法...
});

$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
    // 解析「HelpSpot\API」类型的对象时调用...
});

正如如你所见,被解析的对象会被传递至回调中,让我们可以在对象被传递到消费者前可以设置任何额外属性到对象上。

总结

作为Laravel中最核心的东西,我们当然有必要花费更多的力气去解读。这篇文章虽是流水文,但是对于入门者来说,通过这篇文章理解IoC(控制反转)与DI(依赖注入)已经够了,以及如何绑定和解析服务,这篇文章也足以。至于服务绑定和服务解析后面的东西,入门的时候,你可以不去了解;等你告别Laravel菜鸟的时候,再去看看更深层次的东西,那个时候你会有更多的收获。

每天都在学习,每天都在进步。但是我发现我现在牛逼到受公司的领导排挤了。哎~~~

果冻想——一个原创技术文章分享网站。

2018年3月22日 于内蒙古呼和浩特。

参考

为了完成这篇文章,参考了网络上以下文章:

未经允许不得转载:果冻想 » Laravel初级教程之服务容器
网站维护离不开您的支持,您可以赞助本站,谢谢支持
×

感谢您的支持,我们会一直保持!

扫码支持
请土豪扫码随意打赏

打开支付宝扫一扫,即可进行扫码打赏哦

分享从这里开始,精彩与您同在

赞助本站
关注微信公众号
关注微信公众号和果冻一起分享你的疑惑与心得。
分享到: 更多 (0)

玩技术,我们是认真的

联系我们关于果冻