yii2源碼分析之執行基本流程

用yii2框架用了將近2年,一直都沒有去看過它底層源碼,  馬上快不用了,最近對其源碼研究一番,哈哈

廢話少說,上代碼,

入口文件是web/index.php

<?php

defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');

//這行我在composer autoload流程已經分析過
require __DIR__ . '/../vendor/autoload.php';
//見解釋1-1
require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
//配置文件
$config = require __DIR__ . '/../config/web.php';
//最關鍵的一點,見解釋1-2
(new yii\web\Application($config))->run();


解釋1-1

直接上Yii.php文件源碼

<?php

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

class Yii extends \yii\BaseYii
{
}
//實際上調用的是BaseYii的autoload方法,自動加載yii的類
spl_autoload_register(['Yii', 'autoload'], true, true);
//yii類名和yii類名所在文件的映射數組    
Yii::$classMap = require(__DIR__ . '/classes.php');    
//依賴注入容器,這個後續文章再分析,先知道有這麼一個東東
Yii::$container = new yii\di\Container();


解釋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);
    }
}


至此源碼整個流程分析基本完畢,有些地方可能分析不夠詳細,後續再詳細補充。


最後附加下官網文檔的部分內容幫助大家理解


請求生命週期

以下圖表展示了一個應用如何處理請求:

請求生命週期

  1. 用戶向入口腳本 web/index.php 發起請求。

  2. 入口腳本加載應用配置並創建一個應用 實例去處理請求。

  3. 應用通過請求組件解析請求的 路由

  4. 應用創建一個控制器實例去處理請求。

  5. 控制器創建一個動作實例並針對操作執行過濾器。

  6. 如果任何一個過濾器返回失敗,則動作取消。

  7. 如果所有過濾器都通過,動作將被執行。

  8. 動作會加載一個數據模型,或許是來自數據庫。

  9. 動作會渲染一個視圖,把數據模型提供給它。

  10. 渲染結果返回給響應組件。

  11. 響應組件發送渲染結果給用戶瀏覽器。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章