详解PHP的Yii框架中组件行为的属性注入和方法注入


Posted in PHP onMarch 18, 2016

行为的属性和方法注入原理

上面我们了解到了行为的用意在于将自身的属性和方法注入给所依附的类。 那么Yii中是如何将一个行为 yii\base\Behavior 的属性和方法, 注入到一个 yii\base\Component 中的呢? 对于属性而言,是通过 __get() 和 __set() 魔术方法来实现的。 对于方法,是通过 __call() 方法。

属性的注入

以读取为例,如果访问 $Component->property1 ,Yii在幕后干了些什么呢? 这个看看 yii\base\Component::__get()

public function __get($name)
{
  $getter = 'get' . $name;
  if (method_exists($this, $getter)) {
    return $this->$getter();
  } else {
    // 注意这个 else 分支的内容,正是与 yii\base\Object::__get() 的
    // 不同之处
    $this->ensureBehaviors();
    foreach ($this->_behaviors as $behavior) {
      if ($behavior->canGetProperty($name)) {

        // 属性在行为中须为 public。否则不可能通过下面的形式访问呀。
        return $behavior->$name;
      }
    }
  }
  if (method_exists($this, 'set' . $name)) {
    throw new InvalidCallException('Getting write-only property: ' .
      get_class($this) . '::' . $name);
  } else {
    throw new UnknownPropertyException('Getting unknown property: ' .
      get_class($this) . '::' . $name);
  }
}

重点来看 yii\base\Compoent::__get() 与 yii\base\Object::__get() 的不同之处。 就是在于对于未定义getter函数之后的处理, yii\base\Object 是直接抛出异常, 告诉你想要访问的属性不存在之类。 但是 yii\base\Component 则是在不存在getter之后,还要看看是不是注入的行为的属性:

首先,调用了 $this->ensureBehaviors() 。这个方法已经在前面讲过了,主要是确保行为已经绑定。
在确保行为已经绑定后,开始遍历 $this->_behaviors 。 Yii将类所有绑定的行为都保存在 yii\base\Compoent::$_behaviors[] 数组中。
最后,通过行为的 canGetProperty() 判断这个属性, 是否是所绑定行为的可读属性,如果是,就返回这个行为的这个属性 $behavior->name 。 完成属性的读取。 至于 canGetProperty() 已经在 :ref::property 部分已经简单讲过了, 后面还会有针对性地一个介绍。
对于setter,代码类似,这里就不占用篇幅了。

方法的注入

与属性的注入通过 __get() __set() 魔术方法类似, Yii通过 __call() 魔术方法实现对行为中方法的注入:

public function __call($name, $params)
{
  $this->ensureBehaviors();
  foreach ($this->_behaviors as $object) {
    if ($object->hasMethod($name)) {
      return call_user_func_array([$object, $name], $params);
    }
  }
  throw new UnknownMethodException('Calling unknown method: ' .
    get_class($this) . "::$name()");
}

从上面的代码中可以看出,Yii还是先是调用了 $this->ensureBehaviors() 确保行为已经绑定。

然后,也是遍历 yii\base\Component::$_behaviros[] 数组。 通过 hasMethod() 方法判断方法是否存在。 如果所绑定的行为中要调用的方法存在,则使用PHP的 call_user_func_array() 调用之。 至于 hasMethod() 方法,我们后面再讲。

注入属性与方法的访问控制

在前面我们针对行为中public和private、protected的成员在所绑定的类中是否可访问举出了具体例子。 这里我们从代码层面解析原因。

在上面的内容,我们知道,一个属性可不可访问,主要看行为的 canGetProperty() 和 canSetProperty() 。 而一个方法可不可调用,主要看行为的 hasMethod() 。 由于 yii\base\Behavior 继承自我们的老朋友 yii\base\Object ,所以上面提到的三个判断方法, 事实上代码都在 Object 中。我们一个一个来看:

public function canGetProperty($name, $checkVars = true)
{
  return method_exists($this, 'get' . $name) || $checkVars &&
    property_exists($this, $name);
}

public function canSetProperty($name, $checkVars = true)
{
  return method_exists($this, 'set' . $name) || $checkVars &&
    property_exists($this, $name);
}

public function hasMethod($name)
{
  return method_exists($this, $name);
}

这三个方法真的谈不上复杂。对此,我们可以得出以下结论:

当向Component绑定的行为读取(写入)一个属性时,如果行为为该属性定义了一个getter (setter),则可以访问。 或者,如果行为确实具有该成员变量即可通过上面的判断,此时,该成员变量可为 public, private, protected。 但最终只有 public 的成员变量才能正确访问。原因在上面讲注入的原理时已经交待了。
当调用Component绑定的行为的一个方法时,如果行为已经定义了该方法,即可通过上面的判断。 此时,这个方法可以为 public, private, protected。 但最终只有 public 的方法才能正确调用。如果你理解了上一款的原因,那么这里也就理解了。

依赖注入容器
依赖注入(Dependency Injection,DI)容器就是一个对象,它知道怎样初始化并配置对象及其依赖的所有对象。Martin 的文章 已经解释了 DI 容器为什么很有用。这里我们主要讲解 Yii 提供的 DI 容器的使用方法。

依赖注入

Yii 通过 yii\di\Container 类提供 DI 容器特性。它支持如下几种类型的依赖注入:

  • 构造方法注入;
  • Setter 和属性注入;
  • PHP 回调注入.
  • 构造方法注入

在参数类型提示的帮助下,DI 容器实现了构造方法注入。当容器被用于创建一个新对象时,类型提示会告诉它要依赖什么类或接口。容器会尝试获取它所依赖的类或接口的实例,然后通过构造器将其注入新的对象。例如:

class Foo
{
  public function __construct(Bar $bar)
  {
  }
}

$foo = $container->get('Foo');
// 上面的代码等价于:
$bar = new Bar;
$foo = new Foo($bar);

Setter 和属性注入

Setter 和属性注入是通过配置提供支持的。当注册一个依赖或创建一个新对象时,你可以提供一个配置,该配置会提供给容器用于通过相应的 Setter 或属性注入依赖。例如:

use yii\base\Object;

class Foo extends Object
{
  public $bar;

  private $_qux;

  public function getQux()
  {
    return $this->_qux;
  }

  public function setQux(Qux $qux)
  {
    $this->_qux = $qux;
  }
}

$container->get('Foo', [], [
  'bar' => $container->get('Bar'),
  'qux' => $container->get('Qux'),
]);

PHP 回调注入

这种情况下,容器将使用一个注册过的 PHP 回调创建一个类的新实例。回调负责解决依赖并将其恰当地注入新创建的对象。例如:

$container->set('Foo', function () {
  return new Foo(new Bar);
});

$foo = $container->get('Foo');

注册依赖关系

可以用 yii\di\Container::set() 注册依赖关系。注册会用到一个依赖关系名称和一个依赖关系的定义。依赖关系名称可以是一个类名,一个接口名或一个别名。依赖关系的定义可以是一个类名,一个配置数组,或者一个 PHP 回调。

$container = new \yii\di\Container;

// 注册一个同类名一样的依赖关系,这个可以省略。
$container->set('yii\db\Connection');

// 注册一个接口
// 当一个类依赖这个接口时,相应的类会被初始化作为依赖对象。
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');

// 注册一个别名。
// 你可以使用 $container->get('foo') 创建一个 Connection 实例
$container->set('foo', 'yii\db\Connection');

// 通过配置注册一个类
// 通过 get() 初始化时,配置将会被使用。
$container->set('yii\db\Connection', [
  'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
  'username' => 'root',
  'password' => '',
  'charset' => 'utf8',
]);

// 通过类的配置注册一个别名
// 这种情况下,需要通过一个 “class” 元素指定这个类
$container->set('db', [
  'class' => 'yii\db\Connection',
  'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
  'username' => 'root',
  'password' => '',
  'charset' => 'utf8',
]);

// 注册一个 PHP 回调
// 每次调用 $container->get('db') 时,回调函数都会被执行。
$container->set('db', function ($container, $params, $config) {
  return new \yii\db\Connection($config);
});

// 注册一个组件实例
// $container->get('pageCache') 每次被调用时都会返回同一个实例。
$container->set('pageCache', new FileCache);

Tip: 如果依赖关系名称和依赖关系的定义相同,则不需要通过 DI 容器注册该依赖关系。
通过 set() 注册的依赖关系,在每次使用时都会产生一个新实例。可以使用 yii\di\Container::setSingleton() 注册一个单例的依赖关系:

$container->setSingleton('yii\db\Connection', [
  'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
  'username' => 'root',
  'password' => '',
  'charset' => 'utf8',
]);

解决依赖关系

注册依赖关系后,就可以使用 DI 容器创建新对象了。容器会自动解决依赖关系,将依赖实例化并注入新创建的对象。依赖关系的解决是递归的,如果一个依赖关系中还有其他依赖关系,则这些依赖关系都会被自动解决。

可以使用 yii\di\Container::get() 创建新的对象。该方法接收一个依赖关系名称,它可以是一个类名,一个接口名或一个别名。依赖关系名或许是通过 set() 或 setSingleton() 注册的。你可以随意地提供一个类的构造器参数列表和一个configuration 用于配置新创建的对象。例如:

// "db" 是前面定义过的一个别名
$db = $container->get('db');

// 等价于: $engine = new \app\components\SearchEngine($apiKey, ['type' => 1]);
$engine = $container->get('app\components\SearchEngine', [$apiKey], ['type' => 1]);

代码背后,DI 容器做了比创建对象多的多的工作。容器首先将检查类的构造方法,找出依赖的类或接口名,然后自动递归解决这些依赖关系。

如下代码展示了一个更复杂的示例。UserLister 类依赖一个实现了 UserFinderInterface 接口的对象;UserFinder 类实现了这个接口,并依赖于一个 Connection 对象。所有这些依赖关系都是通过类构造器参数的类型提示定义的。通过属性依赖关系的注册,DI 容器可以自动解决这些依赖关系并能通过一个简单的 get('userLister') 调用创建一个新的 UserLister 实例。

namespace app\models;

use yii\base\Object;
use yii\db\Connection;
use yii\di\Container;

interface UserFinderInterface
{
  function findUser();
}

class UserFinder extends Object implements UserFinderInterface
{
  public $db;

  public function __construct(Connection $db, $config = [])
  {
    $this->db = $db;
    parent::__construct($config);
  }

  public function findUser()
  {
  }
}

class UserLister extends Object
{
  public $finder;

  public function __construct(UserFinderInterface $finder, $config = [])
  {
    $this->finder = $finder;
    parent::__construct($config);
  }
}

$container = new Container;
$container->set('yii\db\Connection', [
  'dsn' => '...',
]);
$container->set('app\models\UserFinderInterface', [
  'class' => 'app\models\UserFinder',
]);
$container->set('userLister', 'app\models\UserLister');

$lister = $container->get('userLister');

// 等价于:

$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);

实践中的运用

当在应用程序的入口脚本中引入 Yii.php 文件时,Yii 就创建了一个 DI 容器。这个 DI 容器可以通过 Yii::$container 访问。当调用 Yii::createObject() 时,此方法实际上会调用这个容器的 yii\di\Container::get() 方法创建新对象。如上所述,DI 容器会自动解决依赖关系(如果有)并将其注入新创建的对象中。因为 Yii 在其多数核心代码中都使用了 Yii::createObject() 创建新对象,所以你可以通过 Yii::$container 全局性地自定义这些对象。

例如,你可以全局性自定义 yii\widgets\LinkPager 中分页按钮的默认数量:

\Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]);

这样如果你通过如下代码在一个视图里使用这个挂件,它的 maxButtonCount 属性就会被初始化为 5 而不是类中定义的默认值 10。

echo \yii\widgets\LinkPager::widget();

然而你依然可以覆盖通过 DI 容器设置的值:

echo \yii\widgets\LinkPager::widget(['maxButtonCount' => 20]);

另一个例子是借用 DI 容器中自动构造方法注入带来的好处。假设你的控制器类依赖一些其他对象,例如一个旅馆预订服务。你可以通过一个构造器参数声明依赖关系,然后让 DI 容器帮你自动解决这个依赖关系。

namespace app\controllers;

use yii\web\Controller;
use app\components\BookingInterface;

class HotelController extends Controller
{
  protected $bookingService;

  public function __construct($id, $module, BookingInterface $bookingService, $config = [])
  {
    $this->bookingService = $bookingService;
    parent::__construct($id, $module, $config);
  }
}

如果你从浏览器中访问这个控制器,你将看到一个报错信息,提醒你 BookingInterface 无法被实例化。这是因为你需要告诉 DI 容器怎样处理这个依赖关系。

\Yii::$container->set('app\components\BookingInterface', 'app\components\BookingService');
现在如果你再次访问这个控制器,一个 app\components\BookingService 的实例就会被创建并被作为第三个参数注入到控制器的构造器中。

什么时候注册依赖关系

由于依赖关系在创建新对象时需要解决,因此它们的注册应该尽早完成。如下是推荐的实践:

如果你是一个应用程序的开发者,你可以在应用程序的入口脚本或者被入口脚本引入的脚本中注册依赖关系。
如果你是一个可再分发扩展的开发者,你可以将依赖关系注册到扩展的引导类中。
总结

依赖注入和服务定位器都是流行的设计模式,它们使你可以用充分解耦且更利于测试的风格构建软件。强烈推荐你阅读 Martin 的文章,对依赖注入和服务定位器有个更深入的理解。

Yii 在依赖住入(DI)容器之上实现了它的服务定位器。当一个服务定位器尝试创建一个新的对象实例时,它会把调用转发到 DI 容器。后者将会像前文所述那样自动解决依赖关系。

PHP 相关文章推荐
example1.php
Oct 09 PHP
删除数组元素实用的PHP数组函数
Aug 18 PHP
PhpMyAdmin中无法导入sql文件的解决办法
Jan 08 PHP
关于PHPDocument 代码注释规范的总结
Jun 25 PHP
php ckeditor上传图片文件名乱码解决方法
Nov 15 PHP
php绘制一条弧线的方法
Jan 24 PHP
php文件操作之小型留言本实例
Jun 20 PHP
关于扩展 Laravel 默认 Session 中间件导致的 Session 写入失效问题分析
Jan 08 PHP
php实现批量删除挂马文件及批量替换页面内容完整实例
Jul 08 PHP
PHP针对中英文混合字符串长度判断及截取方法示例
Mar 31 PHP
使用laravel根据用户类型来显示或隐藏字段
Oct 17 PHP
open_basedir restriction in effect. 原因与解决方法
Mar 14 PHP
PHP的Yii框架中移除组件所绑定的行为的方法
Mar 18 #PHP
PHP的Yii框架中行为的定义与绑定方法讲解
Mar 18 #PHP
详解在PHP的Yii框架中使用行为Behaviors的方法
Mar 18 #PHP
深入讲解PHP的Yii框架中的属性(Property)
Mar 18 #PHP
Symfony2函数用法实例分析
Mar 18 #PHP
Symfony2联合查询实现方法
Mar 18 #PHP
Symfony2使用Doctrine进行数据库查询方法实例总结
Mar 18 #PHP
You might like
php 数组的合并、拆分、区别取值函数集
2010/02/15 PHP
DEDE采集大师官方留后门的删除办法
2011/01/08 PHP
PHP mysql与mysqli事务使用说明 分享
2013/08/17 PHP
php使用curl检测网页是否被百度收录的示例分享
2014/01/31 PHP
Prototype RegExp对象 学习
2009/07/19 Javascript
JS控制显示隐藏兼容问题(IE6、IE7、IE8)
2010/04/01 Javascript
jQuery hover 延时器实现代码
2011/03/12 Javascript
图片上传插件jquery.uploadify详解
2013/11/15 Javascript
javascript实现简易计算器的代码
2016/05/31 Javascript
微信小程序 swiper组件轮播图详解及实例
2016/11/16 Javascript
Bootstrap CSS布局之按钮
2016/12/17 Javascript
Vue导出json数据到Excel电子表格的示例
2017/12/04 Javascript
VUE解决 v-html不能触发点击事件的问题
2019/10/28 Javascript
Vue中使用better-scroll实现轮播图组件
2020/03/07 Javascript
Vue循环中多个input绑定指定v-model实例
2020/08/31 Javascript
解决Ant Design Modal内嵌Form表单initialValue值不动态更新问题
2020/10/29 Javascript
Python中关于使用模块的基础知识
2015/05/24 Python
Python中的FTP通信模块ftplib的用法整理
2016/07/08 Python
python中import学习备忘笔记
2017/01/24 Python
Java编程迭代地删除文件夹及其下的所有文件实例
2018/02/10 Python
利用Python如何实现数据驱动的接口自动化测试
2018/05/11 Python
对python同一个文件夹里面不同.py文件的交叉引用方法详解
2018/12/15 Python
Python 用matplotlib画以时间日期为x轴的图像
2019/08/06 Python
Python figure参数及subplot子图绘制代码
2020/04/18 Python
利用pandas向一个csv文件追加写入数据的实现示例
2020/04/23 Python
实例讲解Python 迭代器与生成器
2020/07/08 Python
波兰快递服务:Globkurier.pl
2019/11/08 全球购物
思想政治教育专业个人求职信范文
2013/12/20 职场文书
结婚保证书范文
2014/04/29 职场文书
大二学生学年自我鉴定
2014/09/12 职场文书
小学班主任评语
2014/12/29 职场文书
ktv服务员岗位职责
2015/02/09 职场文书
2015年法院工作总结范文
2015/04/28 职场文书
学校勤俭节约倡议书
2015/04/29 职场文书
六年级上册《闻官军收河南河北》的教学设计
2019/11/15 职场文书
PostgreSQL数据库去除重复数据和运算符的基本查询操作
2022/04/12 PostgreSQL