【Yii】自動加載機制和別名

自動加載

什麼是類的自動加載,詳見PHP官方文檔 http://php.net/manual/zh/language.oop5.autoload.php

YII自動加載

Yii的類自動加載,依賴於PHP的 spl_autoload_register() , 註冊一個自己的自動加載函數(autoloader),類僅在調用時纔會被加載,依賴別名實現了快速定位,這也是YII高性能的一個重要體現。

首先,從入口文件開始分析。

<?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';//引入composer的自動加載
require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';//引入YII

$config = require __DIR__ . '/../config/web.php';//引入配置文件

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

我們直接來看Yii.php

<?php
require __DIR__ . '/BaseYii.php';

class Yii extends \yii\BaseYii
{
}

spl_autoload_register(['Yii', 'autoload'], true, true);//註冊YII的自動加載函數
Yii::$classMap = require __DIR__ . '/classes.php';//導入classMap(一個類的映射表)
Yii::$container = new yii\di\Container();

因爲用的是spl_autoload_register(['Yii', 'autoload'], true, true),儘管前面先引入的composer的自動加載,當第三個參數爲true時,會將該自動加載放到隊首,所以最先使用。

因爲Yii.php繼承了BaseYii,我們到BaseYii中看autoload方法

public static function autoload($className)
    {
        if (isset(static::$classMap[$className])) {//如果在上面代碼引入的classmap中,則直接讀取
            $classFile = static::$classMap[$className];
            if ($classFile[0] === '@') {
                $classFile = static::getAlias($classFile);//如果是個別名,就getAlias
            }
        } elseif (strpos($className, '\\') !== false) {//如果類名中含有\則認爲是一個正確的類名,將所有的\轉換成/並在前面加上@在後面加上.php,作爲一個路徑別名進行解析,然後執行getAlias方法,得到實際的路徑
            $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?");
        }
    }

看到這,就必須得看一下別名了

別名

別名用來表示文件路徑和 URL,這樣就避免了在代碼中硬編碼一些絕對路徑和 URL。 一個別名必須以 @ 字符開頭,以區別於傳統的文件路徑和 URL。 Yii 預定義了大量可用的別名。例如,別名 @yii 指的是 Yii 框架本身的安裝目錄,而 @web 表示的是當前運行應用的根 URL。
注意:別名所指向的文件路徑或 URL 不一定是真實存在的文件或資源。

\yii\base\Application

public function __construct($config = [])
    {
        Yii::$app = $this;//將application對象賦值給Yii::$app,可以直接用Yii::$app調用application的方法
        static::setInstance($this);

        $this->state = self::STATE_BEGIN;

        $this->preInit($config);//初始化一些配置參數和加載核心組件

        $this->registerErrorHandler($config);//註冊錯誤處理函數
        Component::__construct($config);//註冊一些其他的組件和配置
    }

    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']);//設置@app別名爲根目錄,這裏正是爲什麼命名空間都是app\開頭的原因
            unset($config['basePath']);
        } else {
            throw new InvalidConfigException('The "basePath" configuration for the Application is required.');
        }

        if (isset($config['vendorPath'])) {
            $this->setVendorPath($config['vendorPath']);//設置@vendor,@bower,@npm別名
            unset($config['vendorPath']);
        } else {
            // set "@vendor"
            $this->getVendorPath();
        }

        ...

        // merge core components with custom components
        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'];
            }
        }
    }
public function getVendorPath()
{
    // 在未設置vendorPath時,使用默認值
    if ($this->_vendorPath === null) {
        $this->setVendorPath($this->getBasePath() . DIRECTORY_SEPARATOR . 'vendor');
    }

    return $this->_vendorPath;
}

// 這裏定義了3個別名
public function setVendorPath($path)
{
    $this->_vendorPath = Yii::getAlias($path);
    Yii::setAlias('@vendor', $this->_vendorPath);
    Yii::setAlias('@bower', $this->_vendorPath . DIRECTORY_SEPARATOR . 'bower');
    Yii::setAlias('@npm', $this->_vendorPath . DIRECTORY_SEPARATOR . 'npm');
}

public function getRuntimePath()
{
    // 在未設置runtimePath時,使用默認值
    if ($this->_runtimePath === null) {
        $this->setRuntimePath($this->getBasePath() . DIRECTORY_SEPARATOR . 'runtime');
    }

    return $this->_runtimePath;
}

// 這裏定義了 @runtime 別名
public function setRuntimePath($path)
{
    $this->_runtimePath = Yii::getAlias($path);
    Yii::setAlias('@runtime', $this->_runtimePath);
}

接下來咱們看一下設置別名和獲取別名的方法

public static function setAlias($alias, $path)
{
    // 如果擬定義的別名並非以@打頭,則在前面加上@
    if (strncmp($alias, '@', 1)) {
        $alias = '@' . $alias;
    }

    // 找到別名的第一段,即@ 到第一個 / 之間的內容,如@foo/bar/qux的@foo
    $pos = strpos($alias, '/');
    $root = $pos === false ? $alias : substr($alias, 0, $pos);

    if ($path !== null) {
        // 去除路徑末尾的 \ / 。如果路徑本身就是一個別名,直接解析出來
        $path = strncmp($path, '@', 1) ? rtrim($path, '\\/')
            : static::getAlias($path);

        // 檢查是否有 $aliases[$root],
        // 看看是否已經定義好了根別名。如果沒有,則以$root爲鍵,保存這個別名
        if (!isset(static::$aliases[$root])) {
            if ($pos === false) {
                static::$aliases[$root] = $path;
            } else {
                static::$aliases[$root] = [$alias => $path];
            }
        // 如果 $aliases[$root] 已經存在,則替換成新的路徑,或增加新的路徑
        } elseif (is_string(static::$aliases[$root])) {//如果是個字符串
            if ($pos === false) {//不含有/,在是根別名
                static::$aliases[$root] = $path;//直接更新
            } else {//否則就加變成數組
                static::$aliases[$root] = [
                    $alias => $path,
                    $root => static::$aliases[$root],
                ];
            }
        } else {
            static::$aliases[$root][$alias] = $path;//添加
            krsort(static::$aliases[$root]);
        }

    // 當傳入的 $path 爲 null 時,表示要刪除這個別名。
    } elseif (isset(static::$aliases[$root])) {
        if (is_array(static::$aliases[$root])) {
            unset(static::$aliases[$root][$alias]);
        } elseif ($pos === false) {
            unset(static::$aliases[$root]);
        }
    }
}

根據上面的方法,可以看到,yii支持給別名再設置一個別名,比如

// 使用一個別名定義另一個別名
Yii::setAlias('@fooqux', '@foo/qux');
public static function getAlias($alias, $throwException = true)
{
    // 一切不以@打頭的別名都是無效的,直接返回
    if (strncmp($alias, '@', 1)) {
        return $alias;
    }

    // 先確定根別名 $root
    $pos = strpos($alias, '/');
    $root = $pos === false ? $alias : substr($alias, 0, $pos);

    // 從根別名開始找起,如果根別名沒找到,就不存在
    if (isset(static::$aliases[$root])) {
        if (is_string(static::$aliases[$root])) {
            return $pos === false ? static::$aliases[$root] :
                static::$aliases[$root] . substr($alias, $pos);
        } else {
            // 由於寫入前使用了 krsort() 所以,較長的別名會被先遍歷到。
            foreach (static::$aliases[$root] as $name => $path) {
                if (strpos($alias . '/', $name . '/') === 0) {
                    return $path . substr($alias, strlen($name));
                }
            }
        }
    }

    if ($throwException) {
        throw new InvalidParamException("Invalid path alias: $alias");
    } else {
        return false;
    }
}

別名的解析過程相對簡單:

  • 先按根別名找到可能保存別名的列表。
  • 遍歷這個列表下的所有別名。由於之前別名是按鍵值逆排序的,所以優先匹配長別名。
  • 將找到的最長匹配別名替換成其所對應的值。

YII框架的預定義別名如下

array(8) {
  ["@yii"]=>
  array(6) {
    ["@yii/swiftmailer"]=>
    string(63) "basic/vendor/yiisoft/yii2-swiftmailer"
    ["@yii/gii"]=>
    string(55) "basic/vendor/yiisoft/yii2-gii"
    ["@yii/faker"]=>
    string(57) "basic/vendor/yiisoft/yii2-faker"
    ["@yii/debug"]=>
    string(57) "basic/vendor/yiisoft/yii2-debug"
    ["@yii/bootstrap"]=>
    string(61) "basic/vendor/yiisoft/yii2-bootstrap"
    ["@yii"]=>
    string(51) "basic/vendor/yiisoft/yii2"
  }
  ["@app"]=>
  string(31) "basic"
  ["@vendor"]=>
  string(38) "basic/vendor"
  ["@bower"]=>
  string(50) "basic/vendor/bower-asset"
  ["@npm"]=>
  string(48) "basic/vendor/npm-asset"
  ["@runtime"]=>
  string(39) "basic/runtime"
  ["@webroot"]=>
  string(1) "."
  ["@web"]=>
  string(1) "."
}

結束語

Yii的自動加載,就是結合類的映射表+命名空間別名的形式進行快速定位和載入的,本文只是簡單的羅列了一下代碼,關於更深層次的東西,可以自己根據編輯器代碼跟蹤功能一步步查看。

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