张大帅 2016-08-03 14:12:55 10771次浏览 8条评论 27 10 0

突然发现昨天写的文章放进了版本1.0里去了,今天给挪了出来。很久之前就对源码产生了很大的兴趣,想着有那么一天能够把Yii的框架读下来,但是基于众多事情,这个事情一直被搁浅。最近对代码进行了一些简单的阅读,了解了Yii框架大概是怎么跑起来的,今天做个分享给大家,如果有什么不对,咱们共同讨论。
因为Yii的安装版本有很多,在这里,我用composer安装的是basic版本,接下来就按照basic版本进行分析。之前看过@lizhi353 写的一篇文章http://www.yiichina.com/code/546 写的很不错,但是还不是特别的详细,然后论坛里还有几篇也写得不错,在此不列出来了,其实要想了解框架的基本原理,最基本的一个概念必须要弄懂,否则就会不知道文件怎么加载进来的,命名空间怎么找到相应的类的,这个概念就是依赖注入和容器,这里有几篇文章写的相当不错,不了解的人可以看下这两篇文章http://www.yiichina.com/tutorial/112http://www.yiichina.com/topic/6445。第一篇文章,我们先把整个流程走下来,之后我们在对各个环节的细节方面进行详细的源码分析。很多内容官方文档里已经说得很清楚了,之前没看过源码的时候,看到了官方文档中介绍的应用结构流程,当时只是大概了解,后来看完源码之后,加深了这方面的印象。

02094807585_thumb.png

按照这个流程,咱们现在开始。

因为我用的basic版本,那么第一步就是找到入口脚本@approot/web/index.php

<?php

// comment out the following two lines when deployed to production
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');

require(__DIR__ . '/../vendor/autoload.php');
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');

$config = require(__DIR__ . '/../config/web.php');

(new yii\web\Application($config))->run();

第二行中,可以根据自己的环境,配置为'dev','prod','test'模式(开发环境,生产环境,测试环境)
接下来进入

require(__DIR__ . '/../vendor/autoload.php');

中,会看到代码

<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer' . '/autoload_real.php';

return ComposerAutoloaderInitc69cf032229bc724438b0b4df6d6342d::getLoader();

会调用autoload_real.php中的getLoader()函数,然后在进入到@vendor/composer/autoload_real.php文件中。

spl_autoload_register(array('ComposerAutoloaderInitc69cf032229bc724438b0b4df6d6342d', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInitc69cf032229bc724438b0b4df6d6342d', 'loadClassLoader'));

这三行中,会将loadClassLoader函数进行注册,之后将require DIR . '/ClassLoader.php';文件require进来,等于说加载了一个$loader=new \Composer\Autoload\ClassLoader();的类,之后会看到接下来的几行。

$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
    $loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
    $loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
    $loader->addClassMap($classMap);
}

进行了这几行的运行之后,等于说$composer类实体里面几个属性有了一些默认的值,列表如下:

prefixLengthsPsr4:
{
    "y": {
        "yii\\swiftmailer\\": 16,
        "yii\\gii\\": 8,
        "yii\\faker\\": 10,
        "yii\\debug\\": 10,
        "yii\\composer\\": 13,
        "yii\\codeception\\": 16,
        "yii\\bootstrap\\": 14,
        "yii\\": 4
    },
    "k": {
        "kartik\\grid\\": 12,
        "kartik\\base\\": 12
    },
    "c": {
        "cebe\\markdown\\": 14
    },
    "F": {
        "Faker\\": 6
    }
}
prefixDirsPsr4:
{
    "yii\\swiftmailer\\": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/yiisoft\/yii2-swiftmailer"],
    "yii\\gii\\": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/yiisoft\/yii2-gii"],
    "yii\\faker\\": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/yiisoft\/yii2-faker"],
    "yii\\debug\\": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/yiisoft\/yii2-debug"],
    "yii\\composer\\": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/yiisoft\/yii2-composer"],
    "yii\\codeception\\": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/yiisoft\/yii2-codeception"],
    "yii\\bootstrap\\": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/yiisoft\/yii2-bootstrap"],
    "yii\\": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/yiisoft\/yii2"],
    "kartik\\grid\\": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/kartik-v\/yii2-grid"],
    "kartik\\base\\": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/kartik-v\/yii2-krajee-base"],
    "cebe\\markdown\\": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/cebe\/markdown"],
    "Faker\\": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/fzaninotto\/faker\/src\/Faker"]
}

fallbackDirsPsr4:
[]

prefixesPsr0:
{
    "H": {
        "HTMLPurifier": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/ezyang\/htmlpurifier\/library"]
    },
    "D": {
        "Diff": ["D:\\xampp\\htdocs\\Yii2Test\\vendor\/phpspec\/php-diff\/lib"]
    }
}

fallbackDirsPsr0:
[]

useIncludePath:
false

classMap:
[]

之后调用$loader->register(true);

public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}

public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);
        return true;
    }
}

相当于如果需要new一下某个命名空间的类的时候,系统会先通过loadClass函数查找该类(spl_autoload_register函数的作用以及PSR标准规范,请各位自己查询)。
那么为什么会先将vender包里的这些文件,进行预加载呢,猜测就是为了节省时间。在prefixLengthsPsr4这个属性里,结果已经按照加载那几个文件进行首字母和前缀长度进行了分组,这样在查找某个类的时候,先去获得命名空间类的首字母,然后定位到前缀的长度,之后从prefixDirsPsr4数组里找到该文件的绝对路径并包含进来。

private function findFileWithExtension($class, $ext)
    {
        // PSR-4 lookup
        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;

        $first = $class[0];
        if (isset($this->prefixLengthsPsr4[$first])) {
            foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
                if (0 === strpos($class, $prefix)) {
                    foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
                            return $file;
                        }
                    }
                }
            }
        }

同理PSR0类似。这样的话,就可以预加载了vendor包里的类。至此,开始提到的require(DIR . '/../vendor/autoload.php');基本完成。
接下来进行index.php中的require(DIR . '/../vendor/yiisoft/yii2/Yii.php');代码分析。

<?php

require(__DIR__ . '/BaseYii.php');

class Yii extends \yii\BaseYii
{
}

spl_autoload_register(['Yii', 'autoload'], true, true);
Yii::$classMap = require(__DIR__ . '/classes.php');
Yii::$container = new yii\di\Container();

可以看到代码class Yii extends \yii\BaseYii是继承于BaseYii的。(注意,此时系统还不会自己按照命名空间类的规则去查找某个文件夹下的问下,接下来会分析)
之后spl_autoload_register(['Yii', 'autoload'], true, true);对Yii中的autoload函数进行注册,因为这个函数是注册,所以用到的时候才会触发这个函数。所以接下来往下分析,Yii::$classMap = require(DIR . '/classes.php');,会看到,这个文件里的数组很庞大,对,就是这样,就像前面介绍的vendor/autoload.php一样,为了类的查找时间,系统已经将常用到的类的路径预装进来。接下来是Yii::$container = new yii\di\Container();,就是这个容器解决了很多的问题,如系统不会重复加载一个类实例,会自动查找依赖关系等等。借用这个机制,就可以找到相应的类,关于依赖注入后续文章再进行详细说明。
就像之前提到的vendor/autoload.php文件中一样,Yii::autoload也会进行一些类的预装。接口就是通过BaseYii.php中的autoload函数。

public static function autoload($className)
    {
        if (isset(static::$classMap[$className])) {
            $classFile = static::$classMap[$className];
            if ($classFile[0] === '@') {
                $classFile = static::getAlias($classFile);
            }
        } elseif (strpos($className, '\\') !== false) {
            $classFile = static::getAlias('@' . str_replace('\\', '/', $className) . '.php', false);
            if ($classFile === false || !is_file($classFile)) {
                return;
            }
        } else {
            return;
        }

        include($classFile);

        if (YII_DEBUG && !class_exists($className, false) && !interface_exists($className, false) && !trait_exists($className, false)) {
            throw new UnknownClassException("Unable to find '$className' in file: $classFile. Namespace missing?");
        }
    }

关于static::这种延迟加载的问题,这里不做描述,读者可以自己去查找相关概念。
代码中解释到如果在Yii::classMap中(也就是classes.php),如果找到该类,就直接inlude,如果没有的话,就去aliases数组中查找,如果还是查找不到的话,那容器就派上用场了。容器里也找不到,那就只能抛出错误了。
回头重新看入口文件index.php,其实

require(__DIR__ . '/../vendor/autoload.php');
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');

$config = require(__DIR__ . '/../config/web.php');

这几行没有涉及到教程里提到的加载应用配置,至此只是解决了类的自动加载问题。因为YII框架是个非常庞大的框架,里面牵扯太多的文件和路径,不先解决这个问题,只是简单的require就能把人累死。
接下来才是程序加载配置并运行的过程,这个我放在第二篇里面去写,敬请期待。

觉得很赞
您需要登录后才可以评论。登录 | 立即注册