yii2 随笔(七)依赖注入——(3)yii2的依赖注入

yii2的依赖注入的核心代码在 yii\di,在这个包(文件夹)下面有3个文件,分别是Container.php(容器),Instance.php(实例),ServiceLocator(服务定位器),现在我们讨论一下前两个,服务定位器可以理解一个服务的注册表,这个不影响我们讨论依赖注入,它也是依赖注入的一种应用。
我们还是从代码开始讲解yii2是怎么使用依赖注入的。

// yii\base\application
//这个是yii2的依赖注入使用入口,参数的解释请参考源码,这里不多解释
public static function createObject($type, array $params = [])
{
    if (is_string($type)) {//type 是字符串的话,它就把type当做一个对象的“原材料”,直接把它传给容器并通过容器得到想要的对象。
        return static::$container->get($type, $params);
    } elseif (is_array($type) && isset($type['class'])) {//type 是数组,并且有class的键,经过简单处理后,得到对象的“原材料”,然后把得到的“原材料”传给容器并通过容器得到想要的对象。
        $class = $type['class'];
        unset($type['class']);
        return static::$container->get($class, $params, $type);
    } elseif (is_callable($type, true)) {//如果type是可调用的结构,就直接调用
        return call_user_func($type, $params);
    } elseif (is_array($type)) {//如果type是array,并且没有'class'的键值,那么就抛出异常
        throw new InvalidConfigException('Object configuration must be an array containing a "class" element.');
    } else {//其他情况,均抛出另一个异常,说type不支持的配置类型
        throw new InvalidConfigException("Unsupported configuration type: " . gettype($type));
    }
}

通过阅读上面代码,Yii::createObject()是把合格的“原材料”,交给“容器($container)”,来生成目标对象的,那么容器就是我们“依赖注入”生产对象的地方。那么$container是什么时候引入的呢(注意这里用的是 static::$container, 而不是 self::$container)?还记得在首页导入yii框架时的语句么?

//导入yii框架
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');

代码如下

//引入基本的yii框架
require(__DIR__ . '/BaseYii.php');
//只是做了继承,这里给我们留了二次开发的余地,虽然很少能用到
class Yii extends \yii\BaseYii
{
}
//设置自动加载
spl_autoload_register(['Yii', 'autoload'], true, true);
//注册 classMap
Yii::$classMap = require(__DIR__ . '/classes.php');
//注册容器
Yii::$container = new yii\di\Container();

你看的没错!就是最后一句话,yii2 把 yii\di\Container 的实现拿给自己使用。接下来,我们讨论一下容器是怎么实现的?
接着上面的 static::$container->get() 的方法,在讲解get方法之前,我们要先了解一下容器的几个属性,这将有助于理解get的实现

$_singletons; // 单例数组,它的键值是类的名字,如果生成的对象是单例,则把他保存到这个数组里,值为null的话,表示它还没有被实例化
$_definitions;// 定义数组,它的键值是类的名字,值是生成这个类所需的“原材料”,在set 或 setSingleton的时候写入
$_params; // 参数,它的键值是类的名字,值是生成这个类所需的额外的“原材料”,在set 或 setSingleton的时候写入
$_reflections; //反射,它的键值是类的名字,值是要生成的对象的反射句柄,在生成对象的时候写入
$_dependencies;//依赖,它的键值是类的名字,值是要生成对象前的一些必备“原材料”,在生成对象的时候,通过反射函数得到。

ok,如果你够细心地话,理解了上面的几个属性,估计你就对yii2的容器有个大概的了解了,这里还是从get开始。

public function get($class, $params = [], $config = [])
{
    if (isset($this->_singletons[$class])) {//查看将要生成的对象是否在单例里,如果是,则直接返回
        // singleton
        return $this->_singletons[$class];
    } elseif (!isset($this->_definitions[$class])) {//如果没有要生成类的定义,则直接生成,yii2自身大部分走的是这部分,并没有事先在容器里注册什么,那么配置文件是在哪里注册呢?还记的文章最开始的时候的"服务定位器"么?我们在服务定位器里讲看到这些。
        return $this->build($class, $params, $config);
    }
    //如果已经定义了这个类,则取出这个类的定义
    $definition = $this->_definitions[$class];

    if (is_callable($definition, true)) {//如果定义是可调用的结构
        //先整合一下参数,和$_params里是否有这个类的参数,如果有则和传入的参数以传入覆盖定义的方式整和在一起
        //然后再检查整合后的参数是否符合依赖,就是说是否有必填的参数,如果有直接抛出异常,否则返回参数。检查依赖的时候,需要判断是否为实例(Instance),如果是,则要实现实例。注意:这里出现了Instance。
        $params = $this->resolveDependencies($this->mergeParams($class, $params));
        //把参数专递给可调用结果,返回结果
        $object = call_user_func($definition, $this, $params, $config);
    } elseif (is_array($definition)) {//如果定义是一个数组
        //把代表要生成的class取出
        $concrete = $definition['class'];
        //注销这个键值
        unset($definition['class']);
        //把定义 和 配置整合成新的定义
        $config = array_merge($definition, $config);
        //整合参数
        $params = $this->mergeParams($class, $params);
        //如果传入的$class 和 定义里的class完全一样,则直接生成,build第一个参数确保为真实的类名,而传入的$type可能是别名
        if ($concrete === $class) {
            $object = $this->build($class, $params, $config);
        } else {//如果是别名,则回调自己,生成对象,因为这时的类也有可能是别名
            $object = $this->get($concrete, $params, $config);
        }
    } elseif (is_object($definition)) {//如果定义是一个对象,则代表这个类是个单例,保存到单例里,并返回这个单例,这里要自己动脑想一下,为什么是个对象就是单例?只可意会不可言传,主要是我也组织不好语言怎么解释它。
        return $this->_singletons[$class] = $definition;
    } else {//什么都不是则抛出异常
        throw new InvalidConfigException("Unexpected object definition type: " . gettype($definition));
    }
    //判断这个类的名字是否在单例里,如果在,则把生成的对象放到单例里
    if (array_key_exists($class, $this->_singletons)) {
        // singleton
        $this->_singletons[$class] = $object;
    }
    //返回生成的对象
    return $object;
}

研究到这里,我们发现 get 函数仅仅是个“入口”而已,主要的功能在build里

//创建对象
protected function build($class, $params, $config)
{
    //通过类名得到反射句柄,和依赖(依赖就是所需参数)
    //所以前面提到,传输buile的第一个参数必须为有效的“类名”否则,会直接报错
    list ($reflection, $dependencies) = $this->getDependencies($class);
    //把依赖和参数配置,因为依赖可能有默认参数,这里覆盖默认参数
    foreach ($params as $index => $param) {
        $dependencies[$index] = $param;
    }
    //确保依赖没问题,所有原材料是否都ok了,否则抛出异常
    $dependencies = $this->resolveDependencies($dependencies, $reflection);
    if (empty($config)) {//如果config为空,则返回目标对象
        return $reflection->newInstanceArgs($dependencies);
    }
    
    if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {//如果目标对象是 Configurable的接口
        // set $config as the last parameter (existing one will be overwritten)
        $dependencies[count($dependencies) - 1] = $config;
        return $reflection->newInstanceArgs($dependencies);
    } else {//其他的情况下
        $object = $reflection->newInstanceArgs($dependencies);
        foreach ($config as $name => $value) {
            $object->$name = $value;
        }
        return $object;
    }
}

好了,build到这里就结束了,下面我们一起看看容器是怎么得到反射句柄和依赖关系的

protected function getDependencies($class)
{
    if (isset($this->_reflections[$class])) {//是否已经解析过目标对象了
        return [$this->_reflections[$class], $this->_dependencies[$class]];
    }
   
    $dependencies = [];//初始化依赖数组
    $reflection = new ReflectionClass($class);//得到目标对象的反射,请参考php手册

    $constructor = $reflection->getConstructor();//得到目标对象的构造函数
    if ($constructor !== null) {//如果目标对象有构造函数,则说明他有依赖
        //解析所有的参数,注意得到参数的顺序是从左到右的,确保依赖时也是按照这个顺序执行
        foreach ($constructor->getParameters() as $param) {
            if ($param->isDefaultValueAvailable()) {//如果参数的默认值可用
                $dependencies[] = $param->getDefaultValue();//把默认值放到依赖里
            } else {//如果是其他的
                $c = $param->getClass();//得到参数的类型,如果参数的类型不是某类,是基本类型的话,则返回null
                //如果,是基本类型,则生成null的实例,如果不是基本类型,则生成该类名的实例。注意:这里用到了实例(Instance)
                $dependencies[] = Instance::of($c === null ? null : $c->getName());
            }
        }
    }
    //把引用保存起来,以便下次直接使用
    $this->_reflections[$class] = $reflection;
    //把依赖存起来,以便下次直接使用
    $this->_dependencies[$class] = $dependencies;
    //返回结果
    return [$reflection, $dependencies];
}

下面我们来看看容器是怎么确保依赖关系的

protected function resolveDependencies($dependencies, $reflection = null)
{
    //拿到依赖关系
    foreach ($dependencies as $index => $dependency) {
        //如果依赖是一个实例,因为经过处理的依赖,都是Instance的对象
        if ($dependency instanceof Instance) {
            if ($dependency->id !== null) {//这个实例有id,则通过这个id生成这个对象,并且代替原来的参数
                $dependencies[$index] = $this->get($dependency->id);
            } elseif ($reflection !== null) {//如果反射句柄不为空,注意这个函数是protected 类型的,所以只有本类或者本类的衍生类可访问,但是本类里只有两个地方用到了,一个是 get 的时候,如果目标对象是可调用的结果(is_callable),那么$reflection===null,另外一个build的时候,$reflection不为空,这个时候代表目标对象有一个必须参数,但是还不是一个实例(Instance的对象),这个时候代表缺乏必须的“原材料”抛出异常
                //则拿到响应的必填参数名字,并且抛出异常
                $name = $reflection->getConstructor()->getParameters()[$index]->getName();
                $class = $reflection->getName();
                throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\".");
            }
        }
    }

    //确保了所有的依赖后,返回所有依赖,如果目标是is_callable($definition, true),则不会抛出异常,仅仅把Instance类型的参数实例化出来。
    return $dependencies;
}

看到这里,我们就可以了解了yii2是怎么使用容器实现“依赖注入”了,那么有个问题,闭包的依赖怎么保证呢?我想是因为yii2认为闭包的存在解决的是局限性的问题,不存在依赖性,或者依赖是交给开发者自行解决的。另外yii2的容器,如果参数是闭包的话,就会出现错误,因为对闭包的依赖,解析闭包参数的时候,会得到$dependencies[] = Instance::of($c === null ? null : $c->getName());得到的就是一个 Closure 的实例,而后面 实例化这个实例的时候,就会出现问题了,所以用yii2的容器实现对象的时候,被实现的对象不能包含闭包参数,如果有闭包参数,则一定要有默认值,或者人为保证会传入这个闭包参数,绕过自动生成的语句。
ok容器的主要函数就有这些了,其他方法,set,setSingleton,has,hasSingleton,clear一看就知道什么意思,另外这些方法基本上没有在框架中使用(可以在这些函数写exit,看看你的页面会不会空白),或者你用容器自己生成一些东西的话,可以自行查看这些函数的用法。
最后,我们来看看Instance到底扮演了什么角色

//yii\di\Instance
//很诧异吧,就是实例化一个自己,注意这个自己是 static,以后你可能需要用到这个地方
public static function of($id)
{
    return new static($id);
}

那么这个函数的构造函数呢?

//禁止外部实例化
protected function __construct($id)
{
    //赋值id
    $this->id = $id;
}

在容器中,就用到了Instance的这两个方法,说明Instance在实例中,只是确保了依赖的可用性。此外Instance还提供了其他的函数,其中 get 得到的是当前Instance所对应的id的实例化对象,另外,还有一个静态函数ensure

//确保 $reference 是 $type类型的,如果不是则抛出异常
//在框架中多次用到,请自行查找
//另外,如果$type==null的时候,他也可以当做依赖注入的入口,使用方法请自行查看源码,到现在你应该可以自己看懂这些代码了。
public static function ensure($reference, $type = null, $container = null)
{
    //...
}
发表在 yii2 | 标签为 , , , | 2条评论

yii2 随笔(七)依赖注入——(2)php依赖注入的简单实现

前面我们知道了问题所在,那么PHP是怎么解决这个办法的呢?因为是yii2,所以我们用yii2的方式简单介绍一下,我把yii2的核心代码整合了一下,来说php实现依赖注入的过程。
在使用过yii2开发的同学,对Yii::createObject不陌生,他就是依赖注入的一个“入口”,我把它提出来改一下:

//$p 可以想象的yii2的配置文件,如$p = ['class'=>'sdk/Test', 'file'=>'xxxx'];
// 'class' 是将要实现的类,‘file’是将要实现的对象的属性
function createObject($p){
    $class = $p['class'];
    unset($p['class']);
    $obj = new $class;//require 使用的是 autoload 实现的
    foreach($p as $f => $v){
        $class->$f = $v;    
    }

    return $obj;
}

当然,yii2的createObject远远不止这些,我写成这么简单只是为了说明php实现“依赖注入”的一种方式,当然它是非常简陋的(它不具备最基本的容错功能,而且不具备依赖注入的全部功能,比如参数管理、兼容闭包等,等我们真正看完yii2 DI实现的时候,这些都会浮现出来)。

发表在 yii2 | 标签为 , , , | 留下评论

yii2 随笔(七)依赖注入——(1)什么是依赖注入

在学yii2之前,就已经知道了“依赖注入”的这个名词,也尝试了解过,但是都是一直云里雾里的,可能是一直没有机会真实的使用过,所以了解的不太彻底,这次学习yii2顺便把这个所谓“高大上”给搞定!!!哈哈

在开始“依赖注入”之前,我们先说说设计模式,我觉的从这方面入手可能理解的更快一些。
在最开始学程序时,我们学的是“过程式”编程,比如说实现某些功能,从上到下一句一句的来实现,顶多是把某些通用的功能整理出来当做一些通用包(函数)。再然后我们了解到“面向对象”,随之而来的就是如何应用“对象”的观念来设计出更好的程序(各种设计模式),而“依赖注入”也是一种设计模式的实现。它的实现解决了我们实现功能时的对“前后依赖”或者说是“上下文的依赖”的关注。这句话听起来比较难懂,下面简单解释一下。
怎么理解“前后依赖”
比如说我们要分别制造一辆“宝马”和一辆“保时捷”,在制造宝马时,要严格的遵守下面的顺序(1、2可以不固定顺序):
1. 找到宝马工厂,(require或include)
2. 准备好制造宝马前的准备(初始化对象的参数,如果参数是对象的话,可能同样需要准备一些参数。。。)
3. 必须1和2 都准备完全了之后,才可以生成“宝马”(new BaoMa($params1,$params2….)),就是1 和 2 必须在3之前。
制造“保时捷”要同样的过程,当然,制造其他的车的时候,也都要走一遍这个过程。这就是“前后依赖”的问题。有没有一种办法可以把实现车的这个过程单独整理出来实现通用呢?也就是说先把实现过程准备好,然后再提供“原材料”,要实现什么东西,就要看你提供的什么“原材料”(依赖注入)。

发表在 yii2 | 标签为 , , , | 一条评论

php isset 和 array_key_exists 检查数组中是否存在某值的区别

先看看下面这段代码,你觉得结果是什么呢?

//定义一个数组, > 5.4
$arr = ['a' => null];
var_dump(isset($arr), array_key_exists('a', $arr));

———————下面是结果—————————

/*
 * 输出
 *bool(false)
 *bool(true)
 */

//isset 的用法是: 检测变量是否设置,并且不是 NULL。所以会有上面的输出
//如果判断数组中是否存在某个键值,还是乖乖的用 array_key_exists 吧,不然就是给自己挖了一个巨隐藏的坑啊!!!
发表在 php 优化 | 留下评论

yii2 随笔(六)利用事件触发修改请求参

yii2 可以使用事件机制来触发特殊的处理,实现了代码扩展,可以理解为在yii2内核层已经埋下了隐藏的“钩子”,用于后期扩展,比如 yii\base\application::run() 方法就埋下了两个“钩子”

public function run()  
    {  
        try {  
  
            $this->state = self::STATE_BEFORE_REQUEST;  
            //触发第一个事件(钩子) ,在执行主体之前方面修改一些参数  
            $this->trigger(self::EVENT_BEFORE_REQUEST);  
  
            $this->state = self::STATE_HANDLING_REQUEST;  
            $response = $this->handleRequest($this->getRequest());  
            //触发第二个事件(“钩子”),用于处理运行主体之后,触发一些逻辑  
            $this->state = self::STATE_AFTER_REQUEST;  
            $this->trigger(self::EVENT_AFTER_REQUEST);  
  
            $this->state = self::STATE_SENDING_RESPONSE;  
            $response->send();  
  
            $this->state = self::STATE_END;  
  
            return $response->exitStatus;  
  
        } catch (ExitException $e) {  
  
            $this->end($e->statusCode, isset($response) ? $response : null);  
            return $e->statusCode;  
  
        }  
    } 

我们利用第一个事件,触发修改路由控制,仅作测试使用
1. 在 config/web.php 中添加配置

//添加一个路径别名,可以理解为注册一个新的包为sdk,包的路径为 指定的文件夹。  
yii::setAlias('sdk', dirname(__DIR__).'/sdk');  
//注册一个名为test 的组件,并指定组件所对应的对象,yii2 使用 createObject 方法自动创建这个对象。其他的键可以代表 test 组件的属性值(如果有的话)。  
$config['components']['test'] => ['class'=>'sdk\Test'];  
//test 组件自动加载  
$config['bootstrap'][] = 'test';  

2. 在根目录(和config同级目录)创建sdk目录,在sdk目录下创建Test.php,代码如下:

<?php
namespace sdk;  
  
use Yii;  
//注意大小写,应完全与文件名一样,首字母大写是yii2的规范。  
//这里继承Object是因为配合yii2的运行方式,使用init()函数作为代码的初识话函数,当然仅仅做本此事需求的话,可以写在构造函数里,而不用继承任何基类  
class Test extends yii\base\Object{  
    //构造函数时运行  
    public function init(){  
        //在App里面注册一个处理事件,该事件对应上面提到的yii\base\Application::run()里的第一个事件  
        Yii::$app->on(yii\base\Application::EVENT_BEFORE_REQUEST, function($event){  
            //得到request 对象  
            $request = Yii::$app->getRequest();  
            //检查该对象源码,发现该对象可以修改的内容均为 set.... 开头的,这里用setPathInfo(),设置路由控制器的(用于指定解析本次处理的控制器)。  
            $request->setPathInfo('message/pros');  
        });  
    }  
}  
发表在 yii2 | 留下评论

yii2随笔(五):mysql 防注入 实现 mysql_real_escape_string 功能

用yii2写程序时,一般的简单的sql用yii2封装好的DB库http://www.yiichina.com/doc/guide/2.0/db-dao#w0-6,但是如果不得不写比较复杂的sql,或者你的sql功力足够强大,那么你更愿意自己写sql,是在不再想研究 yii2 封装的DB,总感觉用起来麻烦,那么放sql注入是很重要的,yii2使用的是 Yii::$app->db->quoteValue() (等价 PDO::quote) ,用例如下:

//..else code
$where['channel'] = Yii::$app->db->quoteValue($channel);
//..else code
$all = Yii::$app->db->createCommand($sql)->queryAll();
发表在 yii2 | 留下评论

yii2 随笔(四):文件日志监控

日志使用方式:http://www.yiifans.com/yii2/guide/runtime-logging.html
使用时注意日志的权限
基本配置如下:

[  
    'bootstrap' => ['log'], // ensure logger gets loaded before application starts  
    'components' => [  
        'log' => [  
            'targets' => [  
                'file' => [  
                    'class' => 'yii\log\FileTarget',  //日志处理类
                    'levels' => ['trace', 'info'],  //trace,info 级别的日志写入到文件
                    'categories' => ['yii\*'],  // 指所有以 yii\ 开头的类别
                ],  
                'email' => [  
                    'class' => 'yii\log\EmailTarget',  //日志处理类
                    'levels' => ['error', 'warning'],  //error,warning级别的日志发送邮件
                    'message' => [  
                        'to' => ['admin@techbrood.com', 'support@techbrood.com'],  
                        'subject' => 'New example.com log message',  
                    ],  
                ],  
            ],  
        ],  
    ],  
]  

基本使用方法如下:

//还有其他参数,请参考手册或者源码
\Yii::info('Hello, I am a test log message');
\Yii::error('Hello, I am a test log message');
\Yii::trace('Hello, I am a test log message');
\Yii::warning('Hello, I am a test log message');
发表在 yii2 | 留下评论

yii随笔(三) 开启友好路径

概述:实现 localhost/site/login 访问 localhost/index.php?r=site/login
1. web server 配置
a. nginx 配置
location / {

try_files $uri $uri/ /index.php?$args;
}

b. apache 配置

DocumentRoot “webroot”


RewriteEngine on

# If a directory or a file exists, use the request directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Otherwise forward the request to index.php
RewriteRule . index.php

# …other settings…

2. 在配置文件中的 components 里 添加如下(配置urlManager)
‘urlManager’ =>[
‘class’ => ‘yii\web\UrlManager’,
‘showScriptName’ => false,
‘enablePrettyUrl’ => true,
],

发表在 yii2 | 留下评论

yii 随笔(二):创建第一个用户(yii2脚本程序初识)

1. 进入到 webroot/advanced (yii安装目录)

2.创建数据库, 配置数据库
配置文件路径为:webroot/advanced/common/config/main-local.php

3. 进入到 webroot/advanced目录, 执行 yii migrate 命令(migrate命令是yii自带的数据库转移工具,可对数据库进行备份升级等操作)
该命令实质执行的数据文件是:webroot/advanced/console/migrations/*.php
该命令的执行控制器文件是:webroot/advanced/vendor/yiisoft/yii2/console/controllers/MigrateController.php

4. 现在有了现在数据库里有了 user表,现在建立一个可登陆的用户

5. 用命令行创建password
1). 创建新的命令行控制器

webroot/advanced/console/controllers/testController.php

2). 在新的命令行控制器写下

/**
 * 主命令
 * 本例预计执行示例:在yii根目录执行 yii test(或者 yii test/tes) 
 */

//不可少
namespace console\controllers;
//不可少
use Yii;
//控制器的名字首字母必须为大写,继承 console 控制器
class TestController extends \yii\console\Controller{
    //默认执行的子命令,首字母必须为小写
    public $defaultAction = 'tes';
    
    //执行的子命令
    public function actionTes(){
        echo Yii::$app->getSecurity()->generatePasswordHash('123123');//123123 是需要被设置的密码
        return 0;
    }
}

发表在 yii2 | 留下评论

yii 随笔(一):初始化环境

1. 下载高级模板包(我选择的是解压安装)
http://www.yiiframework.com/download/

2. 解压安装包(我下载的是https://github.com/yiisoft/yii2/releases/download/2.0.6/yii-advanced-app-2.0.6.tgz),到 webroot/advanced

3. 进到 webroot/advanced 执行 init,跟着提示走

4. 开启apache(关于安装,配置略)

5.访问前台 http://localhost/advanced/frontend/web/

发表在 yii2 | 留下评论