溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

yii2源碼分析之執(zhí)行基本流程

發(fā)布時(shí)間:2020-06-12 03:27:59 來源:網(wǎng)絡(luò) 閱讀:7514 作者:china_lx1 欄目:web開發(fā)

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

廢話少說,上代碼,

入口文件是web/index.php

<?php

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

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


解釋1-1

直接上Yii.php文件源碼

<?php

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

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


解釋1-2

我們最關(guān)鍵的點(diǎn)來了分析application啟動(dòng)流程

首先看看Application構(gòu)造函數(shù)

首先進(jìn)入yii\web\Application類,發(fā)現(xiàn)沒有構(gòu)造方法,于是跟蹤它的層級(jí)關(guān)系,列出來:

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

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


首先進(jìn)入yii\base\Application找到__construct方法:

public function __construct($config = [])
{
    //保存當(dāng)前啟動(dòng)的application實(shí)例
    Yii::$app = $this;    
    //將Yii::$app->loadedModules[實(shí)例類名] = 當(dāng)前實(shí)例;
    $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:

/*
該函數(shù)作用是將配置數(shù)組進(jìn)一步合并完善數(shù)組中的key

$config即為入口文件包含到的config/web.php返回的數(shù)組,舉例如下:
$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();
    }

    //設(shè)置時(shí)區(qū)
    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'],
        ];
        
        合并配置文件數(shù)組的components key內(nèi)容
    */
    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可以在文件中配,默認(rèn)為true
    if (YII_ENABLE_ERROR_HANDLER) {
        if (!isset($config['components']['errorHandler']['class'])) {
            echo "Error: no errorHandler component is configured.\n";
            exit(1);
        }
        /*
            曬個(gè)默認(rèn)配置
            'errorHandler' => [
                'errorAction' => 'site/error',
            ],
            
            $this->set方法是引自\yii\di\ServiceLocator的set方法,
            注冊(cè)組件$this->_definitions['erroHandler'] = ['errorAction' => 'site/error','class'=>'yii\web\ErrorHandler'];
        */
        $this->set('errorHandler', $config['components']['errorHandler']);
        unset($config['components']['errorHandler']);
        //這個(gè)方法會(huì)實(shí)例化errorHandler的class,實(shí)例化這步實(shí)際上用到依賴注入,之前我已經(jīng)講過一點(diǎn),以后寫個(gè)yii2創(chuàng)建對(duì)象流程
        //并將實(shí)例化的對(duì)象保存到$this->__components['errorHandler']
        $this->getErrorHandler()->register();
    }
}


解釋1-2-3:

//實(shí)際調(diào)用的是yii\base\BaseObject類的構(gòu)造方法
public function __construct($config = [])
{
    if (!empty($config)) {
        //將$config數(shù)組中的每個(gè)key都賦值$this->本地化變量
        Yii::configure($this, $config);
    }
    $this->init();
}

很明顯追蹤$this->init()方法,后面追蹤到y(tǒng)ii\base\Application的init方法。

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

再看看bootstrap方法

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

protected function bootstrap()
{
    //獲得request對(duì)象實(shí)例
    $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;
        //最重要的一點(diǎn) 見解釋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類的實(shí)例
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數(shù)組,得到['id'=>3, 'r'=> 'post/index']
        
        $result的值為對(duì)應(yīng)conroller執(zhí)行對(duì)應(yīng)action返回的值或者對(duì)象
        */
        $result = $this->runAction($route, $params);
        if ($result instanceof Response) {
            return $result;
        }
        //構(gòu)造一個(gè)Response對(duì)象
        $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);
    }
}

我們進(jìn)入$this->runAction看看

public function runAction($route, $params = [])
{
    //得到($controller實(shí)例對(duì)象和action名稱的字符串)
    $parts = $this->createController($route);
    if (is_array($parts)) {
        /* @var $controller Controller */
        list($controller, $actionID) = $parts;
        $oldController = Yii::$app->controller;
        Yii::$app->controller = $controller;
        //執(zhí)行controller的對(duì)應(yīng)的actionID方法,該方法返回的內(nèi)容賦值給$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對(duì)象的format再獲得該format對(duì)象的實(shí)例執(zhí)行format方法(就是header設(shè)置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();
}


這里補(bǔ)充下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);
    }
}


至此源碼整個(gè)流程分析基本完畢,有些地方可能分析不夠詳細(xì),后續(xù)再詳細(xì)補(bǔ)充。


最后附加下官網(wǎng)文檔的部分內(nèi)容幫助大家理解


請(qǐng)求生命周期

以下圖表展示了一個(gè)應(yīng)用如何處理請(qǐng)求:

yii2源碼分析之執(zhí)行基本流程

  1. 用戶向入口腳本 web/index.php 發(fā)起請(qǐng)求。

  2. 入口腳本加載應(yīng)用配置并創(chuàng)建一個(gè)應(yīng)用 實(shí)例去處理請(qǐng)求。

  3. 應(yīng)用通過請(qǐng)求組件解析請(qǐng)求的 路由。

  4. 應(yīng)用創(chuàng)建一個(gè)控制器實(shí)例去處理請(qǐng)求。

  5. 控制器創(chuàng)建一個(gè)動(dòng)作實(shí)例并針對(duì)操作執(zhí)行過濾器。

  6. 如果任何一個(gè)過濾器返回失敗,則動(dòng)作取消。

  7. 如果所有過濾器都通過,動(dòng)作將被執(zhí)行。

  8. 動(dòng)作會(huì)加載一個(gè)數(shù)據(jù)模型,或許是來自數(shù)據(jù)庫(kù)。

  9. 動(dòng)作會(huì)渲染一個(gè)視圖,把數(shù)據(jù)模型提供給它。

  10. 渲染結(jié)果返回給響應(yīng)組件。

  11. 響應(yīng)組件發(fā)送渲染結(jié)果給用戶瀏覽器。


向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI