天天看點

PHP自動加載下——PSR4

1.先來介紹一下PSR規範

PHP-FIG,它的網站是:www.php-fig.org。就是這個聯盟組織發明和創造了PSR規範,其中自動加載涉及其中兩個規範,一個是PSR0,一個是PSR4, PSR0規範已經過時了,官方有提示,現在主要是用PSR4規範定義自動加載标準。

2.PRS4簡介

這個 PSR 描述的是通過檔案路徑自動載入類的指南;它作為對 PSR-0 的補充;根據這個 指導如何規範存放檔案來自動載入;

術語「類」是一個泛稱;它包含類,接口,traits 以及其他類似的結構;

完全限定類名應該類似如下範例:

()*

完全限定類名必須有一個頂級命名空間(Vendor Name);

完全限定類名可以有多個子命名空間;

完全限定類名應該有一個終止類名;

下劃線在完全限定類名中是沒有特殊含義的;

字母在完全限定類名中可以是任何大小寫的組合;

所有類名必須以大小寫敏感的方式引用;

當從完全限定類名載入檔案時:

在完全限定類名中,連續的一個或幾個子命名空間構成的命名空間字首(不包括頂級命名空間的分隔符),至少對應着至少一個基礎目錄。

在「命名空間字首」後的連續子命名空間名稱對應一個「基礎目錄」下的子目錄,其中的命名 空間分隔符表示目錄分隔符。子目錄名稱必須和子命名空間名大小寫比對;

終止類名對應一個以 .php 結尾的檔案。檔案名必須和終止類名大小寫比對;

自動載入器的實作不可抛出任何異常,不可引發任何等級的錯誤;也不應傳回值;

完全限定類名 命名空間字首 基礎路徑 完全路徑
\Acme\Log\Writer\File_Writer Acme\Log\Write ./acme-log-writer/lib/ ./acme-log-writer/lib/File_Writer.php
\Aura\Web\Response\Status Aura\Web /path/to/aura-web/src/ /path/to/aura-web/src/Response/Status.php
\Symfony\Core\Request Symfony\Core ./vendor/Symfony/Core/ ./vendor/Symfony/Core/Request.php
\Zend\Acl Zend /usr/includes/Zend/ /usr/includes/Zend/Acl.php

大家注意看第二列和第四列,命名空間字首對應基礎路徑,命名空間字首之後的子命名空間必須對應代碼目錄(類名必須是PHP檔案)

3.優化自動加載方法

上一節中封裝自動加載的方法比較簡單,無法自動加載帶命名空間的類

spl_autoload_register(function ($class) {

    // 命名空間字首
    $prefix = 'Foo\\Bar\\';

    // 命名空間字首對應的基礎目錄
    $base_dir = __DIR__ . '/src/';

    // 檢查new的類是否有命名空間字首
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        return;
    }

    // 擷取去掉命名空間字首後的類名
    $relative_class = substr($class, $len);

    // 将命名空間的中的分隔符替換為目錄分隔符,再加上基礎目錄和.php字尾,最終拼接成
    // 檔案路徑
    $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';

    // 如果檔案存在則require
    if (file_exists($file)) {
        require $file;
    }
});
           

但是上面的方法隻能适用固定的命名空間字首,不能通用。

4、再次優化通用自動加載方法

<?php
namespace Example;

/**
 * 下面這個例子實作了一個命名空間字首對應多個基礎目錄
 *
 * 現在我們的目錄結構是下面這樣:
 *
 *     /demo/autoload/
 *          controller/
 *             DemoController.php              # Foo\Bar\DemoController
 *             Admin/
 *                 AdminController.php         # Foo\Bar\Admin\AdminController
 *          model/
 *             DemoModel.php                   # Foo\Bar\DemoModel
 *             Admin/
 *                 AdminModel.php              # Foo\Bar\Admin\AdminModel
 *
 * Foo\Bar分别對應基礎路徑 /demo/autoload/controller 和 /demo/autoload/model
 */
class Psr4AutoloaderClass
{
    /**
     * 一個數組,key為命名空間字首,值為基礎路徑
     *
     * @var array
     */
    protected $prefixes = array();

    /**
     * 封裝自動加載函數
     *
     * @return void
     */
    public function register()
    {
        spl_autoload_register(array($this, 'loadClass'));
    }

    /**
     *
     * 添加一個基礎路徑對應一個命名空間字首
     *
     * @param string $prefix 命名空間字首.
     * @param string $base_dir 命名空間類檔案的基礎路徑
     * @param bool true為往數組頭部添加元素,false為往數組尾部添加元素
     * @return void
     */
    public function addNamespace($prefix, $base_dir, $prepend = false)
    {
        // 去掉左邊的\
        $prefix = trim($prefix, '\\') . '\\';

        // 規範基礎路徑
        $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';

        // 初始化數組
        if (isset($this->prefixes[$prefix]) === false) {
            $this->prefixes[$prefix] = array();
        }

        // 将命名空間字首和基礎路徑存入數組
        if ($prepend) {
            array_unshift($this->prefixes[$prefix], $base_dir);
        } else {
            array_push($this->prefixes[$prefix], $base_dir);
        }
    }

    /**
     * 真正包含檔案方法,将給到類名檔案包含進來
     *
     * @param string $class 全限定類名(包含命名空間).
     * @return 成功将傳回檔案路徑,失敗則傳回false
     */
    public function loadClass($class)
    {
        $prefix = $class;
        //查找$prefix最後一個\的位置,看看最後一個\之前的字元串是否在$this->prefixes中
        //如果不存在則繼續查詢上一個\的位置,擷取上一個\之前的字元串是否在$this->prefixes中
        //如果循環結束還是沒有找到則傳回false
        while (false !== $pos = strrpos($prefix, '\\')) {
            $prefix = substr($class, 0, $pos + 1);

            $relative_class = substr($class, $pos + 1);

            $mapped_file = $this->loadMappedFile($prefix, $relative_class);
            if ($mapped_file) {
                return $mapped_file;
            }

            //去掉右邊的\
            $prefix = rtrim($prefix, '\\');
        }

        return false;
    }

    /**
     * 如果參數中的$prefix在$this->prefixes中存在,那麼将循環$this->prefixes[$prefix]裡的value(基礎路徑)
     * 之後拼接檔案路徑,如果檔案存在将檔案包含進來
     *
     * @param string $prefix 命名空間字首.
     * @param string $relative_class 真正的類名(不包含命名空間路徑的類名).
     * @return mixed 包含成功傳回檔案路徑,否則傳回false
     */
    protected function loadMappedFile($prefix, $relative_class)
    {
        // 檢查數組中是否有$prefix這個key
        if (isset($this->prefixes[$prefix]) === false) {
            return false;
        }

        // 将數組中所有的基礎路徑中的檔案包含進來
        foreach ($this->prefixes[$prefix] as $base_dir) {

            // 拼接檔案絕對路徑
            $file = $base_dir
                . str_replace('\\', '/', $relative_class)
                . '.php';

            // 如果檔案存在則包含進來
            if ($this->requireFile($file)) {
                // 傳回檔案路徑
                return $file;
            }
        }

        // 沒有找到檔案
        return false;
    }

    /**
     *如果檔案存在則包含進來.
     *
     * @param string $file 檔案路徑.
     * @return bool
     */
    protected function requireFile($file)
    {
        if (file_exists($file)) {
            require $file;
            return true;
        }
        return false;
    }
}