热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Laravel核心——IoC服务容器

Laravel核心——IoC服务容器

服务容器

本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/laravel-source-analysis
在说 Ioc 容器之前,我们需要了解什么是 Ioc 容器。

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

在理解这句话之前,我们需要先了解一下服务容器的来龙去脉: laravel神奇的服务容器。这篇博客告诉我们,服务容器就是工厂模式的升级版,对于传统的工厂模式来说,虽然解耦了对象和外部资源之间的关系,但是工厂和外部资源之间却存在了耦和。而服务容器在为对象创建了外部资源的同时,又与外部资源没有任何关系,这个就是 Ioc 容器。

所谓的依赖注入和控制反转: 依赖注入和控制反转,就是

只要不是由内部生产(比如初始化、构造函数 __construct 中通过工厂方法、自行手动 new 的),而是由外部以参数或其他形式注入的,都属于依赖注入(DI)

也就是说:

依赖注入是从应用程序的角度在描述,可以把依赖注入描述完整点:应用程序依赖容器创建并注入它所需要的外部资源;

控制反转是从容器的角度在描述,描述完整点:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。

Laravel中的服务容器


Laravel服务容器主要承担两个作用:绑定与解析,服务容器的结构如下:

Laravel 核心——IoC 服务容器

绑定


所谓的绑定就是将接口与实现建立对应关系。几乎所有的服务容器绑定都是在服务提供者中完成,也就是在服务提供者中绑定。

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

也就是说,如果需要依赖注入的外部资源如果没有接口,那么就不需要绑定,直接利用服务容器进行解析就可以了,服务容器会根据类名利用反射对其进行自动构造。

bind绑定

绑定有多种方法,首先最常用的是bind函数的绑定:

  • 绑定自身

    $this->app->bind('App\Services\RedisEventPusher', null);
  • 绑定闭包

    $this->app->bind('HelpSpot\API', function ($app) {
    return new HelpSpot\API();
    });//闭包直接提供实现方式
    
    $this->app->bind('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
    });//需要依赖注入
  • 绑定接口

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

这三种绑定方式中,第一种绑定自身一般用于绑定单例。

singleton绑定

singleton 方法绑定一个只需要解析一次的类或接口到容器,然后接下来对容器的调用将会返回同一个实例:

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

instance绑定

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

  $api = new HelpSpot\API(new HttpClient);
  $this->app->instance('HelpSpot\Api', $api);

Context绑定

有时侯我们可能有两个类使用同一个接口,但我们希望在每个类中注入不同实现,例如,两个控制器依赖 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(StorageController::class)
          ->needs(Filesystem::class)
          ->give(function () {
            Storage::class
          });//提供类名

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
             return new Storage();
          });//提供实现方式

$this->app->when(VideoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
            return new Storage($app->make(Disk::class));
          });//需要依赖注入

原始值绑定

我们可能有一个接收注入类的类,同时需要注入一个原生的数值比如整型,可以结合上下文轻松注入这个类需要的任何值:

$this->app->when('App\Http\Controllers\UserController')
          ->needs('$variableName')
          ->give($value);

数组绑定

app()['service'] = function(){
    return new Service();
};

标签绑定

少数情况下,我们需要解析特定分类下的所有绑定,例如,你正在构建一个接收多个不同 Report 接口实现的报告聚合器,在注册完 Report 实现之后,可以通过 tag 方法给它们分配一个标签:

$this->app->bind('SpeedReport', function () {
  //
});

$this->app->bind('MemoryReport', function () {
  //
});

$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');

这些服务被打上标签后,可以通过 tagged 方法来轻松解析它们:

$this->app->bind('ReportAggregator', function ($app) {
    return new ReportAggregator($app->tagged('reports'));
});

extend扩展

extend是在当原来的类被注册或者实例化出来后,可以对其进行扩展:

public function testExtendInstancesArePreserved()
{
    $cOntainer= new Container;
    $container->bind('foo', function () {
        $obj = new StdClass;
        $obj->foo = 'bar';

        return $obj;
    });

    $obj = new StdClass;
    $obj->foo = 'foo';
    $container->instance('foo', $obj);

    $container->extend('foo', function ($obj, $container) {
        $obj->bar = 'baz';
        return $obj;
    });

    $container->extend('foo', function ($obj, $container) {
        $obj->baz = 'foo';
        return $obj;
    });

    $this->assertEquals('foo', $container->make('foo')->foo);
    $this->assertEquals('baz', $container->make('foo')->bar);
    $this->assertEquals('foo', $container->make('foo')->baz);
}

Rebounds与Rebinding

绑定是针对接口的,是为接口提供实现方式的方法。我们可以对接口在不同的时间段里提供不同的实现方法,一般来说,对同一个接口提供新的实现方法后,不会对已经实例化的对象产生任何影响。但是在一些场景下,在提供新的接口实现后,我们希望对已经实例化的对象重新做一些改变,这个就是 rebinding 函数的用途。
下面就是一个例子:

abstract class Car
{
    public function __construct(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }

    public function refuel($litres)
    {
        return $litres * $this->fuel->getPrice();
    }

    public function setFuel(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }

}

class JeepWrangler extends Car
{
  //
}

interface Fuel
{
    public function getPrice();
}

class Petrol implements Fuel
{
    public function getPrice()
    {
        return 130.7;
    }
}

我们在服务容器中是这样对car接口和fuel接口绑定的:

$this->app->bind('fuel', function ($app) {
    return new Petrol;
});

$this->app->bind('car', function ($app) {
    return new JeepWrangler($app['fuel']);
});

$this->app->make('car');

如果car被服务容器解析实例化成对象之后,有人修改了 fuel 接口的实现,从 Petrol 改为 PremiumPetrol:

$this->app->bind('fuel', function ($app) {
    return new PremiumPetrol;
});

由于 car 已经被实例化,那么这个接口实现的改变并不会影响到 car 的实现,假若我们想要 car 的成员变量 fuel 随着 fuel 接口的变化而变化,我们就需要一个回调函数,每当对 fuel 接口实现进行改变的时候,都要对 car 的 fuel 变量进行更新,这就是 rebinding 的用途:

$this->app->bindShared('car', function ($app) {
    return new JeepWrangler($app->rebinding('fuel', function ($app, $fuel) {
        $app['car']->setFuel($fuel);
    }));
});

服务别名


什么是服务别名

在说服务容器的解析之前,需要先说说服务的别名。什么是服务别名呢?不同于上一个博客中提到的 Facade 门面的别名(在 config/app 中定义),这里的别名服务绑定名称的别名。通过服务绑定的别名,在解析服务的时候,跟不使用别名的效果一致。别名的作用也是为了同时支持全类型的服务绑定名称以及简短的服务绑定名称考虑的。

通俗的讲,假如我们想要创建 auth 服务,我们既可以这样写:

$this->app->make('auth')

又可以写成:

$this->app->make('\Illuminate\Auth\AuthManager::class')

还可以写成

$this->app->make('\Illuminate\Contracts\Auth\Factory::class')

后面两个服务的名字都是 auth 的别名,使用别名和使用 auth 的效果是相同的。

服务别名的递归

需要注意的是别名是可以递归的:

app()->alias('service', 'alias_a');
app()->alias('alias_a', 'alias_b');
app()-alias('alias_b', 'alias_c');

会得到:

'alias_a' => 'service'
'alias_b' => 'alias_a'
'alias_c' => 'alias_b'

服务别名的实现

那么这些别名是如何加载到服务容器里面的呢?实际上,服务容器里面有个 aliases 数组:

$aliases = [
  'app' => [\Illuminate\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class],
  'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],
  'auth.driver' => [\Illuminate\Contracts\Auth\Guard::class],
  'blade.compiler' => [\Illuminate\View\Compilers\BladeCompiler::class],
  'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
...
]

而服务容器的初始化的过程中,会运行一个函数:

public function registerCoreContainerAliases()
{
  foreach ($aliases as $key => $aliases) {
    foreach ($aliases as $alias) {
      $this->alias($key, $alias);
    }
  }
}

public function alias($abstract, $alias)
{
  $this->aliases[$alias] = $abstract;

  $this->abstractAliases[$abstract][] = $alias;
}

加载后,服务容器的aliases和abstractAliases数组:

$aliases = [
  'Illuminate\Foundation\Application' = "app"
  'Illuminate\Contracts\Container\Container' = "app"
  'Illuminate\Contracts\Foundation\Application' = "app"
  'Illuminate\Auth\AuthManager' = "auth"
  'Illuminate\Contracts\Auth\Factory' = "auth"
  'Illuminate\Contracts\Auth\Guard' = "auth.driver"
  'Illuminate\View\Compilers\BladeCompiler' = "blade.compiler"
  'Illuminate\Cache\CacheManager' = "cache"
  'Illuminate\Contracts\Cache\Factory' = "cache"
  ...
]
$abstractAliases = [
  app = {array} [3]
  0 = "Illuminate\Foundation\Application"
  1 = "Illuminate\Contracts\Container\Container"
  2 = "Illuminate\Contracts\Foundation\Application"
  auth = {array} [2]
  0 = "Illuminate\Auth\AuthManager"
  1 = "Illuminate\Contracts\Auth\Factory"
  auth.driver = {array} [1]
  0 = "Illuminate\Contracts\Auth\Guard"
  blade.compiler = {array} [1]
  0 = "Illuminate\View\Compilers\BladeCompiler"
  cache = {array} [2]
  0 = "Illuminate\Cache\CacheManager"
  1 = "Illuminate\Contracts\Cache\Factory"
  ...
]

服务解析


make 解析

有很多方式可以从容器中解析对象,首先,你可以使用 make 方法,该方法接收你想要解析的类名或接口名作为参数:

  $fooBar = $this->app->make('HelpSpot\API');

如果你所在的代码位置访问不了 $app 变量,可以使用辅助函数resolve:

$api = resolve('HelpSpot\API');

自动注入

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

call 方法注入

make 解析是服务容器进行解析构建类对象时所用的方法,在实际应用中,还有另外一个需求,那就是当前已经获取了一个类对象,我们想要调用它的一个方法函数,这时发现这个方法中参数众多,如果一个个的 make 会比较繁琐,这个时候就要用到 call 解析了。我们可以看这个例子:

class TaskRepository{

    public function testContainerCall(User $user,Task $task){
        $task->where('user_id','!=',0)
            ->orderBy('created_at')
            ->get();

        $user->orderBy('created_at')
            ->get();
    }

    public static function testContainerCallStatic(User $user,Task $task){
        $task->where('user_id','!=',0)
            ->orderBy('created_at')
            ->get();

        $user->orderBy('created_at')
            ->get();
    }

    public function testCallback(){
        echo 'call callback successfully!';
    }

    public function testDefaultMethod(){
        echo 'default Method successfully!';
    }
}

静态方法注入

服务容器的 call 解析主要依靠 call_user_func_array() 函数,关于这个函数可以查看 Laravel学习笔记之Callback Type - 来生做个漫画家,这个函数对类中的静态函数和非静态函数有一些区别,对于静态函数来说:

class ContainerCallTest
{
    public function testContainerCallStatic(){
        App::call(TaskRepository::class.'@testContainerCallStatic');
        App::call(TaskRepository::class.'::testContainerCallStatic');
        App::call([TaskRepository::class,'testContainerCallStatic']);
    }
}

服务容器调用类的静态方法有三种,注意第三种使用数组的形式,数组中可以直接传类名 TaskRepository::class;

非静态方法注入

对于类的非静态方法:

class ContainerCallTest
{
    public function testContainerCall(){
        $taskRepo = new TaskRepository();
        App::call(TaskRepository::class.'@testContainerCall');
        App::call([$taskRepo,'testContainerCall']);
    }
}

我们可以看到非静态方法只有两种调用方式,而且第二种数组传递的参数是类对象,原因就是 call_user_func_array函数的限制,对于非静态方法只能传递对象。

bindmethod 方法绑定

服务容器还有一个 bindmethod 的方法,可以绑定类的一个方法到自定义的函数:

public function testContainCallMethodBind(){

    App::bindMethod(TaskRepository::class.'@testContainerCallStatic',function () {
         $taskRepo = new TaskRepository();
         $taskRepo->testCallback();
    });

    App::call(TaskRepository::class.'@testContainerCallStatic');
    App::call(TaskRepository::class.'::testContainerCallStatic');
    App::call([TaskRepository::class,'testContainerCallStatic']);

    App::bindMethod(TaskRepository::class.'@testContainerCall',function (TaskRepository $taskRepo) { $taskRepo->testCallback(); });

    $taskRepo = new TaskRepository();
    App::call(TaskRepository::class.'@testContainerCall');
    App::call([$taskRepo,'testContainerCall']);
}

从结果上看,bindmethod 不会对静态的第二种解析方法( :: 解析方式)起作用,对于其他方式都会调用绑定的函数。

默认函数注入
public function testContainCallDefultMethod(){

    App::call(TaskRepository::class,[],'testContainerCall');

    App::call(TaskRepository::class,[],'testContainerCallStatic');

    App::bindMethod(TaskRepository::class.'@testContainerCallStatic',function () {
        $taskRepo = new TaskRepository();
        $taskRepo->testCallback();
    });

    App::bindMethod(TaskRepository::class.'@testContainerCall',function (TaskRepository $taskRepo) {  $taskRepo->testCallback(); });

    App::call(TaskRepository::class,[],'testContainerCall');

    App::call(TaskRepository::class,[],'testContainerCallStatic');

}

值得注意的是,这种默认函数注入的方法使得非静态的方法也可以利用类名去调用,并不需要对象。默认函数注入也回受到 bindmethod 函数的影响。

数组解析

  app()['service'];

app($service)的形式

  app('service');

  

服务容器事件


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

$this->app->resolving(function ($object, $app) {
  // 解析任何类型的对象时都会调用该方法...
});
$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
  // 解析「HelpSpot\API」类型的对象时调用...
});
$this->app->afterResolving(function ($object, $app) {
  // 解析任何类型的对象后都会调用该方法...
});
$this->app->afterResolving(HelpSpot\API::class, function ($api, $app) {
  // 解析「HelpSpot\API」类型的对象后调用...
});

服务容器每次解析对象的时候,都会调用这些通过 resolving 和 afterResolving 函数传入的闭包函数,也就是触发这些事件。
注意:如果是单例,则只在解析时会触发一次

Written with StackEdit.


以上所述就是小编给大家介绍的《Laravel 核心——IoC 服务容器》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 我们 的支持!


推荐阅读
  • 浏览器中的异常检测算法及其在深度学习中的应用
    本文介绍了在浏览器中进行异常检测的算法,包括统计学方法和机器学习方法,并探讨了异常检测在深度学习中的应用。异常检测在金融领域的信用卡欺诈、企业安全领域的非法入侵、IT运维中的设备维护时间点预测等方面具有广泛的应用。通过使用TensorFlow.js进行异常检测,可以实现对单变量和多变量异常的检测。统计学方法通过估计数据的分布概率来计算数据点的异常概率,而机器学习方法则通过训练数据来建立异常检测模型。 ... [详细]
  • PHP中的单例模式与静态变量的区别及使用方法
    本文介绍了PHP中的单例模式与静态变量的区别及使用方法。在PHP中,静态变量的存活周期仅仅是每次PHP的会话周期,与Java、C++不同。静态变量在PHP中的作用域仅限于当前文件内,在函数或类中可以传递变量。本文还通过示例代码解释了静态变量在函数和类中的使用方法,并说明了静态变量的生命周期与结构体的生命周期相关联。同时,本文还介绍了静态变量在类中的使用方法,并通过示例代码展示了如何在类中使用静态变量。 ... [详细]
  • 本文介绍了绕过WAF的XSS检测机制的方法,包括确定payload结构、测试和混淆。同时提出了一种构建XSS payload的方法,该payload与安全机制使用的正则表达式不匹配。通过清理用户输入、转义输出、使用文档对象模型(DOM)接收器和源、实施适当的跨域资源共享(CORS)策略和其他安全策略,可以有效阻止XSS漏洞。但是,WAF或自定义过滤器仍然被广泛使用来增加安全性。本文的方法可以绕过这种安全机制,构建与正则表达式不匹配的XSS payload。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了使用postman进行接口测试的方法,以测试用户管理模块为例。首先需要下载并安装postman,然后创建基本的请求并填写用户名密码进行登录测试。接下来可以进行用户查询和新增的测试。在新增时,可以进行异常测试,包括用户名超长和输入特殊字符的情况。通过测试发现后台没有对参数长度和特殊字符进行检查和过滤。 ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • Webmin远程命令执行漏洞复现及防护方法
    本文介绍了Webmin远程命令执行漏洞CVE-2019-15107的漏洞详情和复现方法,同时提供了防护方法。漏洞存在于Webmin的找回密码页面中,攻击者无需权限即可注入命令并执行任意系统命令。文章还提供了相关参考链接和搭建靶场的步骤。此外,还指出了参考链接中的数据包不准确的问题,并解释了漏洞触发的条件。最后,给出了防护方法以避免受到该漏洞的攻击。 ... [详细]
  • 模板引擎StringTemplate的使用方法和特点
    本文介绍了模板引擎StringTemplate的使用方法和特点,包括强制Model和View的分离、Lazy-Evaluation、Recursive enable等。同时,还介绍了StringTemplate语法中的属性和普通字符的使用方法,并提供了向模板填充属性的示例代码。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • Activiti7流程定义开发笔记
    本文介绍了Activiti7流程定义的开发笔记,包括流程定义的概念、使用activiti-explorer和activiti-eclipse-designer进行建模的方式,以及生成流程图的方法。还介绍了流程定义部署的概念和步骤,包括将bpmn和png文件添加部署到activiti数据库中的方法,以及使用ZIP包进行部署的方式。同时还提到了activiti.cfg.xml文件的作用。 ... [详细]
  • ECMA262规定typeof操作符的返回值和instanceof的使用方法
    本文介绍了ECMA262规定的typeof操作符对不同类型的变量的返回值,以及instanceof操作符的使用方法。同时还提到了在不同浏览器中对正则表达式应用typeof操作符的返回值的差异。 ... [详细]
  • 本文分析了Wince程序内存和存储内存的分布及作用。Wince内存包括系统内存、对象存储和程序内存,其中系统内存占用了一部分SDRAM,而剩下的30M为程序内存和存储内存。对象存储是嵌入式wince操作系统中的一个新概念,常用于消费电子设备中。此外,文章还介绍了主电源和后备电池在操作系统中的作用。 ... [详细]
author-avatar
前所未闻啊_549
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有