yii2源码分析之执行基本流程

用yii2框架用了将近2年,一直都没有去看过它底层源码,  马上快不用了,最近对其源码研究一番,哈哈

公司主营业务:成都做网站、成都网站设计、移动网站开发等业务。帮助企业客户真正实现互联网宣传,提高企业的竞争能力。创新互联是一支青春激扬、勤奋敬业、活力青春激扬、勤奋敬业、活力澎湃、和谐高效的团队。公司秉承以“开放、自由、严谨、自律”为核心的企业文化,感谢他们对我们的高要求,感谢他们从不同领域给我们带来的挑战,让我们激情的团队有机会用头脑与智慧不断的给客户带来惊喜。创新互联推出鼓楼免费做网站回馈大家。

废话少说,上代码,

入口文件是web/index.php

run();

解释1-1

直接上Yii.php文件源码


解释1-2

我们最关键的点来了分析application启动流程

首先看看Application构造函数

首先进入yii\web\Application类,发现没有构造方法,于是跟踪它的层级关系,列出来:

yii\web\Application -> \yii\base\Application -> \yii\base\Module -> \yii\di\ServiceLocator -> \yii\base\Component 

-> \yii\base\BaseObject -> \yii\base\Configurable(接口interface)

首先进入yii\base\Application找到__construct方法:

public function __construct($config = [])
{
    //保存当前启动的application实例
    Yii::$app = $this;    
    //将Yii::$app->loadedModules[实例类名] = 当前实例;
    $this->setInstance($this);

    $this->state = self::STATE_BEGIN;

    //见解释1-2-1
    $this->preInit($config);    

    //见解释1-2-2
    $this->registerErrorHandler($config);

    //见解释1-2-3
    Component::__construct($config);
}

解释1-2-1:

/*
该函数作用是将配置数组进一步合并完善数组中的key

$config即为入口文件包含到的config/web.php返回的数组,举例如下:
$config = [
    'id' => 'basic',
    'basePath' => dirname(__DIR__),
    'bootstrap' => ['log'],
    'aliases' => [
        '@bower' => '@vendor/bower-asset',
        '@npm'   => '@vendor/npm-asset',
    ],
    'components' => [
        'request' => [
            // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
            'cookieValidationKey' => '',
        ],
        'cache' => [
            'class' => 'yii\caching\FileCache',
        ],
        'user' => [
            'identityClass' => 'app\models\User',
            'enableAutoLogin' => true,
        ],
        'errorHandler' => [
            'errorAction' => 'site/error',
        ],
        'mailer' => [
            'class' => 'yii\swiftmailer\Mailer',
            'useFileTransport' => true,
        ],
        'log' => [
            'traceLevel' => YII_DEBUG ? 3 : 0,
            'targets' => [
                [
                    'class' => 'yii\log\FileTarget',
                    'levels' => ['error', 'warning'],
                ],
            ],
        ],
        'db' => $db,
    ],
    'params' => $params,
];
*/
public function preInit(&$config)
{
    if (!isset($config['id'])) {
        throw new InvalidConfigException('The "id" configuration for the Application is required.');
    }
    if (isset($config['basePath'])) {
        $this->setBasePath($config['basePath']);
        unset($config['basePath']);
    } else {
        throw new InvalidConfigException('The "basePath" configuration for the Application is required.');
    }

    if (isset($config['vendorPath'])) {
        $this->setVendorPath($config['vendorPath']);
        unset($config['vendorPath']);
    } else {
        $this->getVendorPath();
    }
    if (isset($config['runtimePath'])) {
        $this->setRuntimePath($config['runtimePath']);
        unset($config['runtimePath']);
    } else {
        // set "@runtime"
        $this->getRuntimePath();
    }

    //设置时区
    if (isset($config['timeZone'])) {
        $this->setTimeZone($config['timeZone']);
        unset($config['timeZone']);
    } elseif (!ini_get('date.timezone')) {
        $this->setTimeZone('UTC');
    }

    if (isset($config['container'])) {
        $this->setContainer($config['container']);

        unset($config['container']);
    }

    /*
        coreComponents返回核心组件
        return [
            'log' => ['class' => 'yii\log\Dispatcher'],
            'view' => ['class' => 'yii\web\View'],
            'formatter' => ['class' => 'yii\i18n\Formatter'],
            'i18n' => ['class' => 'yii\i18n\I18N'],
            'mailer' => ['class' => 'yii\swiftmailer\Mailer'],
            'urlManager' => ['class' => 'yii\web\UrlManager'],
            'assetManager' => ['class' => 'yii\web\AssetManager'],
            'security' => ['class' => 'yii\base\Security'],
        ];
        
        合并配置文件数组的components key内容
    */
    foreach ($this->coreComponents() as $id => $component) {
        if (!isset($config['components'][$id])) {
            $config['components'][$id] = $component;
        } elseif (is_array($config['components'][$id]) && !isset($config['components'][$id]['class'])) {
            $config['components'][$id]['class'] = $component['class'];
        }
    }
}

解释1-2-2:

protected function registerErrorHandler(&$config)
{
    //YII_ENABLE_ERROR_HANDLER可以在文件中配,默认为true
    if (YII_ENABLE_ERROR_HANDLER) {
        if (!isset($config['components']['errorHandler']['class'])) {
            echo "Error: no errorHandler component is configured.\n";
            exit(1);
        }
        /*
            晒个默认配置
            'errorHandler' => [
                'errorAction' => 'site/error',
            ],
            
            $this->set方法是引自\yii\di\ServiceLocator的set方法,
            注册组件$this->_definitions['erroHandler'] = ['errorAction' => 'site/error','class'=>'yii\web\ErrorHandler'];
        */
        $this->set('errorHandler', $config['components']['errorHandler']);
        unset($config['components']['errorHandler']);
        //这个方法会实例化errorHandler的class,实例化这步实际上用到依赖注入,之前我已经讲过一点,以后写个yii2创建对象流程
        //并将实例化的对象保存到$this->__components['errorHandler']
        $this->getErrorHandler()->register();
    }
}

解释1-2-3:

//实际调用的是yii\base\BaseObject类的构造方法
public function __construct($config = [])
{
    if (!empty($config)) {
        //将$config数组中的每个key都赋值$this->本地化变量
        Yii::configure($this, $config);
    }
    $this->init();
}

很明显追踪$this->init()方法,后面追踪到yii\base\Application的init方法。

public function init()
{
    $this->state = self::STATE_INIT;
    $this->bootstrap();
}

再看看bootstrap方法

先看看yii\web\Application的bootstrap方法

protected function bootstrap()
{
    //获得request对象实例
    $request = $this->getRequest();
    Yii::setAlias('@webroot', dirname($request->getScriptFile()));
    Yii::setAlias('@web', $request->getBaseUrl());

    parent::bootstrap();
}

再看看yii\base\Application的bootstrap方法

protected function bootstrap()
{
    if ($this->extensions === null) {
        $file = Yii::getAlias('@vendor/yiisoft/extensions.php');
        $this->extensions = is_file($file) ? include $file : [];
    }
    foreach ($this->extensions as $extension) {
        if (!empty($extension['alias'])) {
            foreach ($extension['alias'] as $name => $path) {
                Yii::setAlias($name, $path);
            }
        }
        if (isset($extension['bootstrap'])) {
            $component = Yii::createObject($extension['bootstrap']);
            if ($component instanceof BootstrapInterface) {
                Yii::debug('Bootstrap with ' . get_class($component) . '::bootstrap()', __METHOD__);
                $component->bootstrap($this);
            } else {
                Yii::debug('Bootstrap with ' . get_class($component), __METHOD__);
            }
        }
    }
    //已配置需要初始化的组件初始化
    foreach ($this->bootstrap as $mixed) {
        $component = null;
        if ($mixed instanceof \Closure) {
            Yii::debug('Bootstrap with Closure', __METHOD__);
            if (!$component = call_user_func($mixed, $this)) {
                continue;
            }
        } elseif (is_string($mixed)) {
            if ($this->has($mixed)) {
                $component = $this->get($mixed);
            } elseif ($this->hasModule($mixed)) {
                $component = $this->getModule($mixed);
            } elseif (strpos($mixed, '\\') === false) {
                throw new InvalidConfigException("Unknown bootstrapping component ID: $mixed");
            }
        }

        if (!isset($component)) {
            $component = Yii::createObject($mixed);
        }

        if ($component instanceof BootstrapInterface) {
            Yii::debug('Bootstrap with ' . get_class($component) . '::bootstrap()', __METHOD__);
            $component->bootstrap($this);
        } else {
            Yii::debug('Bootstrap with ' . get_class($component), __METHOD__);
        }
    }
}

到此new Application($config)这一步分析完毕

再来看看$app->run()做了什么

先打开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;
        //最重要的一点 见解释2-1
        $response = $this->handleRequest($this->getRequest());

        $this->state = self::STATE_AFTER_REQUEST;
        $this->trigger(self::EVENT_AFTER_REQUEST);

        $this->state = self::STATE_SENDING_RESPONSE;
        //见解释2-2
        $response->send();

        $this->state = self::STATE_END;

        return $response->exitStatus;
    } catch (ExitException $e) {
        $this->end($e->statusCode, isset($response) ? $response : null);
        return $e->statusCode;
    }
}

解释2-1:

打开yii\web\Application的handleRequest

//$request为yii\web\Request类的实例
public function handleRequest($request)
{
    if (empty($this->catchAll)) {
        try {

            list($route, $params) = $request->resolve();
        } catch (UrlNormalizerRedirectException $e) {
            $url = $e->url;
            if (is_array($url)) {
                if (isset($url[0])) {
                    // ensure the route is absolute
                    $url[0] = '/' . ltrim($url[0], '/');
                }
                $url += $request->getQueryParams();
            }

            return $this->getResponse()->redirect(Url::to($url, $e->scheme), $e->statusCode);
        }
    } else {
        $route = $this->catchAll[0];
        $params = $this->catchAll;
        unset($params[0]);
    }
    try {
        Yii::debug("Route requested: '$route'", __METHOD__);
        $this->requestedRoute = $route;
        /*
        例如访问url为http://domain/web/index.php?r=post/index&id=3
        $route为路由url字符串,得到post/index
        $params为Query String数组,得到['id'=>3, 'r'=> 'post/index']
        
        $result的值为对应conroller执行对应action返回的值或者对象
        */
        $result = $this->runAction($route, $params);
        if ($result instanceof Response) {
            return $result;
        }
        //构造一个Response对象
        $response = $this->getResponse();
        if ($result !== null) {
            $response->data = $result;
        }

        return $response;
    } catch (InvalidRouteException $e) {
        throw new NotFoundHttpException(Yii::t('yii', 'Page not found.'), $e->getCode(), $e);
    }
}

我们进入$this->runAction看看

public function runAction($route, $params = [])
{
    //得到($controller实例对象和action名称的字符串)
    $parts = $this->createController($route);
    if (is_array($parts)) {
        /* @var $controller Controller */
        list($controller, $actionID) = $parts;
        $oldController = Yii::$app->controller;
        Yii::$app->controller = $controller;
        //执行controller的对应的actionID方法,该方法返回的内容赋值给$result
        $result = $controller->runAction($actionID, $params);
        if ($oldController !== null) {
            Yii::$app->controller = $oldController;
        }

        return $result;
    }

    $id = $this->getUniqueId();
    throw new InvalidRouteException('Unable to resolve the request "' . ($id === '' ? $route : $id . '/' . $route) . '".');
}

解释2-2:

打开yii\web\Response的send方法

public function send()
{
    if ($this->isSent) {
        return;
    }
    $this->trigger(self::EVENT_BEFORE_SEND);
    //取得$response对象的format再获得该format对象的实例执行format方法(就是header设置Content-Type)
    //见2-2-1
    $this->prepare();
    $this->trigger(self::EVENT_AFTER_PREPARE);
    //见2-2-2
    $this->sendHeaders();
    //见2-2-3
    $this->sendContent();
    $this->trigger(self::EVENT_AFTER_SEND);
    $this->isSent = true;
}

解释2-2-1:

protected function prepare()
{
    if ($this->stream !== null) {
        return;
    }

    if (isset($this->formatters[$this->format])) {
        $formatter = $this->formatters[$this->format];
        if (!is_object($formatter)) {
            $this->formatters[$this->format] = $formatter = Yii::createObject($formatter);
        }
        if ($formatter instanceof ResponseFormatterInterface) {
            $formatter->format($this);
        } else {
            throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface.");
        }
    } elseif ($this->format === self::FORMAT_RAW) {
        if ($this->data !== null) {
            $this->content = $this->data;
        }
    } else {
        throw new InvalidConfigException("Unsupported response format: {$this->format}");
    }

    if (is_array($this->content)) {
        throw new InvalidArgumentException('Response content must not be an array.');
    } elseif (is_object($this->content)) {
        if (method_exists($this->content, '__toString')) {
            $this->content = $this->content->__toString();
        } else {
            throw new InvalidArgumentException('Response content must be a string or an object implementing __toString().');
        }
    }
}

解释2-2-2:

protected function sendHeaders()
{
    if (headers_sent($file, $line)) {
        throw new HeadersAlreadySentException($file, $line);
    }
    if ($this->_headers) {
        $headers = $this->getHeaders();
        foreach ($headers as $name => $values) {
            $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name)));
            // set replace for first occurrence of header but false afterwards to allow multiple
            $replace = true;
            foreach ($values as $value) {
                header("$name: $value", $replace);
                $replace = false;
            }
        }
    }
    $statusCode = $this->getStatusCode();
    header("HTTP/{$this->version} {$statusCode} {$this->statusText}");
    $this->sendCookies();
}

这里补充下sendCookies方法:

protected function sendCookies()
{
    if ($this->_cookies === null) {
        return;
    }
    $request = Yii::$app->getRequest();
    if ($request->enableCookieValidation) {
        if ($request->cookieValidationKey == '') {
            throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.');
        }
        $validationKey = $request->cookieValidationKey;
    }
    foreach ($this->getCookies() as $cookie) {
        $value = $cookie->value;
        if ($cookie->expire != 1 && isset($validationKey)) {
            $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey);
        }
        setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly);
    }
}

解释2-2-3:

protected function sendContent()
{
    if ($this->stream === null) {
        echo $this->content;

        return;
    }

    set_time_limit(0); // Reset time limit for big files
    $chunkSize = 8 * 1024 * 1024; // 8MB per chunk

    if (is_array($this->stream)) {
        list($handle, $begin, $end) = $this->stream;
        fseek($handle, $begin);
        while (!feof($handle) && ($pos = ftell($handle)) <= $end) {
            if ($pos + $chunkSize > $end) {
                $chunkSize = $end - $pos + 1;
            }
            echo fread($handle, $chunkSize);
            flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
        }
        fclose($handle);
    } else {
        while (!feof($this->stream)) {
            echo fread($this->stream, $chunkSize);
            flush();
        }
        fclose($this->stream);
    }
}

至此源码整个流程分析基本完毕,有些地方可能分析不够详细,后续再详细补充。

最后附加下官网文档的部分内容帮助大家理解

请求生命周期

以下图表展示了一个应用如何处理请求:

yii2源码分析之执行基本流程

  1. 用户向入口脚本 web/index.php 发起请求。

  2. 入口脚本加载应用配置并创建一个应用 实例去处理请求。

  3. 应用通过请求组件解析请求的 路由。

  4. 应用创建一个控制器实例去处理请求。

  5. 控制器创建一个动作实例并针对操作执行过滤器。

  6. 如果任何一个过滤器返回失败,则动作取消。

  7. 如果所有过滤器都通过,动作将被执行。

  8. 动作会加载一个数据模型,或许是来自数据库。

  9. 动作会渲染一个视图,把数据模型提供给它。

  10. 渲染结果返回给响应组件。

  11. 响应组件发送渲染结果给用户浏览器。


当前文章:yii2源码分析之执行基本流程
分享路径:http://scyanting.com/article/piigcs.html