天天看點

PHP 自動加載功能原了解析

這篇文章是對 PHP 自動加載功能的一個總結,内容涉及 PHP 的自動加載功能、 PHP 的命名空間、 PHP 的 PSR0 與 PSR4 标準等内容。

一、PHP自動加載功能

PHP自動加載功能的由來

在 PHP 開發過程中,如果希望從外部引入一個 class,通常會使用 include 和 require 方法,去把定義這個 class 的檔案包含進來。這個在小規模開發的時候,沒什麼大問題。但在大型的開發項目中,使用這種方式會帶來一些隐含的問題:如果一個 PHP 檔案需要使用很多其它類,那麼就需要很多的 require/include 語句,這樣有可能會造成遺漏或者包含進不必要的類檔案。如果大量的檔案都需要使用其它的類,那麼要保證每個檔案都包含正确的類檔案肯定是一個噩夢, 況且 require_once 的代價很大。

PHP5 為這個問題提供了一個解決方案,這就是類的自動裝載(autoload)機制。 autoload 機制可以使得 PHP 程式有可能在使用類時才自動包含類檔案,而不是一開始就将所有的類檔案 include 進來,這種機制也稱為 lazy loading。

總結起來,自動加載功能帶來了幾處優點:

  1. 使用類之前無需 include 或者 require。
  2. 使用類的時候才會 require/include 檔案,實作了 lazy loading,避免了 require/include 多餘檔案。
  3. 無需考慮引入類的實際磁盤位址,實作了邏輯和實體檔案的分離。

如果想具體詳細的了解關于自動加載的功能,可以檢視資料:

PHP的類自動加載機制

​​PHP的autoload機制的實作解析​​

PHP 自動加載函數 __autoload()

通常 PHP5 在使用一個類時,如果發現這個類沒有加載,就會自動運作 _autoload() 函數,這個函數是我們在程式中自定義的,在這個函數中我們可以加載需要使用的類。下面是個簡單的示例:

function __autoload($classname) {
           require_once ($classname . "class.php"); 
        }      

在我們這個簡單的例子中,我們直接将類名加上擴充名 ”.class.php” 構成了類檔案名,然後使用 require_once 将其加載。從這個例子中,我們可以看出 autoload 至少要做三件事情:

  1. 根據類名确定類檔案名;
  2. 确定類檔案所在的磁盤路徑(在我們的例子是最簡單的情況,類與調用它們的PHP程式檔案在同一個檔案夾下);
  3. 将類從磁盤檔案中加載到系統中。

第三步最簡單,隻需要使用 include/require 即可。要實作第一步,第二步的功能,必須在開發時約定類名與磁盤檔案的映射方法,隻有這樣我們才能根據類名找到它對應的磁盤檔案。

當有大量的類檔案要包含的時候,我們隻要确定相應的規則,然後在 _autoload() 函數中,将類名與實際的磁盤檔案對應起來,就可以實作 lazy loading 的效果。從這裡我們也可以看出 _autoload() 函數的實作中最重要的是類名與實際的磁盤檔案映射規則的實作。

__autoload() 函數存在的問題

如果在一個系統的實作中,如果需要使用很多其它的類庫,這些類庫可能是由不同的開發人員編寫的, 其類名與實際的磁盤檔案的映射規則不盡相同。這時如果要實作類庫檔案的自動加載,就必須在 _autoload() 函數中将所有的映射規則全部實作,這樣的話 autoload() 函數有可能會非常複雜,甚至無法實作。最後可能會導緻 autoload() 函數十分臃腫,這時即便能夠實作,也會給将來的維護和系統效率帶來很大的負面影響。

那麼問題出現在哪裡呢?問題出現在autoload() 是全局函數隻能定義一次,不夠靈活,是以所有的類名與檔案名對應的邏輯規則都要在一個函數裡面實作,造成這個函數的臃腫。那麼如何來解決這個問題呢?答案就是使用一個 _autoload 調用堆棧,不同的映射關系寫到不同的 _autoload 函數中去,然後統一注冊統一管理,這個就是 PHP5 引入的 SPL Autoload。

SPL Autoload

SPL 是 Standard PHP Library (标準 PHP 庫)的縮寫。它是 PHP5 引入的一個擴充庫,其主要功能包括 autoload 機制的實作及包括各種 Iterator 接口或類。SPL Autoload 具體有幾個函數:

  1. spl_autoload_register:注冊 _autoload() 函數
  2. spl_autoload_unregister:登出已注冊的函數
  3. spl_autoload_functions:傳回所有已注冊的函數
  4. spl_autoload_call:嘗試所有已注冊的函數來加載類
  5. spl_autoload :_autoload() 的預設實作
  6. spl_autoload_extionsions: 注冊并傳回 spl_autoload 函數使用的預設檔案擴充名。

這幾個函數具體詳細用法可見 ​​php中spl_autoload詳解​​

簡單來說,spl_autoload 就是 SPL 自己的定義 _autoload() 函數,功能很簡單,就是去注冊的目錄(由 set_include_path 設定)找與 classname 同名的 .php/.inc 檔案。當然,你也可以指定特定類型的檔案,方法是注冊擴充名( spl_autoload_extionsions )。

而 spl_autoload_register() 就是我們上面所說的 autoload 調用堆棧,我們可以向這個函數注冊多個我們自己的 _autoload() 函數,當 PHP 找不到類名時, PHP 就會調用這個堆棧,一個一個去調用自定義的 _autoload() 函數,實作自動加載功能。如果我們不向這個函數輸入任何參數,那麼就會注冊 spl_autoload() 函數。

好啦,PHP 自動加載的底層就是這些,注冊機制已經非常靈活,但是還缺什麼呢?我們上面說過,自動加載關鍵就是類名和檔案的映射,這種映射關系不同架構有不同方法,非常靈活,但是過于靈活就會顯得雜亂,PHP 有專門對這種映射關系的規範,那就是 PSR 标準中 PSR0 與 PSR4。

不過在談 PSR0 與 PSR4 之前,我們還需要了解 PHP 的命名空間的問題,因為這兩個标準其實針對的都不是類名與目錄檔案的映射,而是命名空間與檔案的映射。為什麼會這樣呢?在我的了解中,規範的面向對象 PHP 思想,命名空間在一定程度上算是類名的别名,那麼為什麼要推出命名空間,命名空間的優點是什麼呢

二、Namespace命名空間

要了解命名空間,首先先看看 ​​官方文檔​​ 中對命名空間的介紹:

什麼是命名空間?從廣義上來說,命名空間是一種封裝事物的方法。在很多地方都可以見到這種抽象概念。例如,在作業系統中目錄用來将相關檔案分組,對于目錄中的檔案來說,它就扮演了命名空間的角色。具體舉個例子,檔案 foo.txt 可以同時在目錄 /home/greg 和 /home/other 中存在,但在同一個目錄中不能存在兩個 foo.txt 檔案。另外,在目錄 /home/greg 外通路 foo.txt 檔案時,我們必須将目錄名以及目錄分隔符放在檔案名之前得到 /home/greg/foo.txt。這個原理應用到程式設計領域就是命名空間的概念。

在 PHP 中,命名空間用來解決在編寫類庫或應用程式時建立可重用的代碼如類或函數時碰到的兩類問題:

1 使用者編寫的代碼與 PHP 内部的類/函數/常量或第三方類/函數/常量之間的名字沖突

2 為很長的辨別符名稱(通常是為了緩解第一類問題而定義的)建立一個或簡短)的名稱,提高源代碼的可讀性。

PHP 命名空間提供了一種将相關的類、函數和常量組合到一起的途徑。

簡單來說就是 PHP 是不允許程式中存在兩個名字一樣一樣的類或者函數或者變量名的,那麼有人就很疑惑了,那就不起一樣名字不就可以了?事實上很多大程式依賴很多第三方庫,名字沖突什麼的不要太常見,這個就是官網中的第一個問題。那麼如何解決這個問題呢?在沒有命名空間的時候,可憐的程式員隻能給類名起 a_b_c_d_e_f 這樣的,其中 a/b/c/d/e/f 一般有其特定意義,這樣一般就不會發生沖突了,但是這樣長的類名編寫起來累,讀起來更是難受。是以 PHP5 就推出了命名空間,類名是類名,命名空間是命名空間,程式寫/看的時候直接用類名,運作起來機器看的是命名空間,這樣就解決了問題。

另外,命名空間提供了一種将相關的類、函數和常量組合到一起的途徑。這也是面向對象語言命名空間的很大用途,把特定用途所需要的類、變量、函數寫到一個命名空間中,進行封裝。

解決了類名的問題,我們終于可以回到PSR标準來了,那麼PSR0與PSR4是怎麼規範檔案與命名空間的映射關系的呢?答案就是:對命名空間的命名(額,有點繞)、類檔案目錄的位置和兩者映射關系做出了限制,這個就是标準的核心了。更完整的描述可見 ​​現代 PHP 新特性系列(一) —— 命名空間​​

三、PSR标準

在說 PSR0 與 PSR4 之前先介紹一下 PSR 标準。 PSR 标準的發明者和規範者是:PHP-FIG,它的網站是:​​www.php-fig.org​​。就是這個聯盟組織發明和創造了PSR-[0-4]規範。FIG 是 Framework Interoperability Group(架構可互用性小組)的縮寫,由幾位開源架構的開發者成立于 2009 年,從那開始也選取了很多其他成員進來,雖然不是 “官方” 組織,但也代表了社群中不小的一塊。組織的目的在于:以最低程度的限制,來統一各個項目的編碼規範,避免各家自行發展的風格阻礙了程式設計師開發的困擾,于是大夥發明和總結了 PSR,PSR 是 Proposing a Standards Recommendation(提出标準建議)的縮寫,其中前 5 個标準分别是:

PSR-0 (Autoloading Standard) 自動加載标準

PSR-1 (Basic Coding Standard)基礎編碼标準

PSR-2 (Coding Style Guide) 編碼風格向導

PSR-3 (Logger Interface) 日志接口

PSR-4 (Improved Autoloading) 自動加載的增強版,可以替換掉PSR-0了。

具體詳細的規範标準可以檢視 ​​PHP中PSR-[0-4]規範​​

PSR0标準

PRS-0規範是他們出的第1套規範,主要是制定了一些自動加載标準(Autoloading Standard)PSR-0強制性要求幾點:

1、 一個完全合格的 namespace 和 class 必須符合這樣的結構:“\< Vendor Name>(< Namespace>)*< Class Name>”

2、每個 namespace 必須有一個頂層的namespace("Vendor Name"提供者名字)

3、每個 namespace 可以有多個子 namespace

4、當從檔案系統中加載時,每個 namespace 的分隔符(/)要轉換成 DIRECTORYSEPARATOR (作業系統路徑分隔符)

5、在類名中,每個下劃線()符号要轉換成 DIRECTORY_SEPARATOR (作業系統路徑分隔符)。在 namespace 中,下劃線 _符号是沒有(特殊)意義的。

6、當從檔案系統中載入時,合格的 namespace 和 class 一定是以 .php 結尾的

7、verdor name,namespaces,class 名可以由大小寫字母組合而成(大小寫敏感的)

具體規則可能有些讓人暈,我們從頭講一下。

我們先來看PSR0标準大緻内容,第 1、2、3、7 條對命名空間的名字做出了限制,第 4、5 條對命名空間和檔案目錄的映射關系做出了限制,第 6 條是檔案字尾名。

前面我們說過,PSR 标準是如何規範命名空間和所在檔案目錄之間的映射關系?是通過限制命名空間的名字、所在檔案目錄的位置和兩者映射關系。

那麼我們可能就要問了,哪裡限制了檔案所在目錄的位置了呢?其實答案就是:

限制命名空間名字 + 限制命名空間名字與檔案目錄映射 = 限制檔案目錄

好了,我們先想一想,對于一個具體程式來說,如果它想要支援PSR0标準,它需要做什麼調整呢?

  1. 首先,程式必須定義一個符合 PSR0 标準第 4、5 條的映射函數,然後把這個函數注冊到 spl_register() 中;
  2. 其次,定義一個新的命名空間時,命名空間的名字和所在檔案的目錄位置必須符合第 1、2、3、7 條。

一般為了代碼維護友善,我們會在一個檔案隻定義一個命名空間。

好了,我們有了符合 PSR0 的命名空間的名字,通過符合 PSR0 标準的映射關系就可以得到符合 PSR0 标準的檔案目錄位址,如果我們按照 PSR0 标準正确存放檔案,就可以順利 require 該檔案了,我們就可以使用該命名空間啦,是不是很神奇呢?

接下來,我們詳細地來看看 PSR0 标準到底規範了什麼呢?

我們以 laravel 中第三方庫 Symfony 其中一個命名空間 /Symfony/Core/Request 為例,講一講上面 PSR0 标準。

  1. 一個完全合格的 namespace 和 class 必須符合這樣的結構:“\< Vendor Name>(< Namespace>)*< Class Name>”

上面所展示的 /Symfony 就是 Vendor Name,也就是第三方庫的名字,/Core 是 Namespace 名字,一般是我們命名空間的一些屬性資訊(例如 request 是 Symfony 的核心功能);最後 Request 就是我們命名空間的名字,這個标準規範就是讓人看到命名空間的來源、功能非常明朗,有利于代碼的維護。

2 . 每個 namespace 必須有一個頂層的 namespace("Vendor Name" 提供者名字)

也就是說每個命名空間都要有一個類似于 /Symfony 的頂級命名空間,為什麼要有這種規則呢?因為 PSR0 标準隻負責頂級命名空間之後的映射關系,也就是 /Symfony/Core/Request 這一部分,關于 /Symfony 應該關聯到哪個目錄,那就是使用者或者架構自己定義的了。所謂的頂層的 namespace,就是自定義了映射關系的命名空間,一般就是提供者名字(第三方庫的名字)。換句話說頂級命名空間是自動加載的基礎。為什麼标準要這麼設定呢?原因很簡單,如果有個命名空間是 /Symfony/Core/Transport/Request,還有個命名空間是 /Symfony/Core/Transport/Request1,如果沒有頂級命名空間,我們就得寫兩個路徑和這兩個命名空間相對應,如果再有 Request2、Request3 呢。有了頂層命名空間 /Symfony,那我們就僅僅需要一個目錄對應即可,剩下的就利用 PSR 标準去解析就行了。

3.每個 namespace 可以有多個子 namespace

這個很簡單,Request 可以定義成 /Symfony/Core/Request,也可以定義成 /Symfony/Core/Transport/Request,/Core 這個命名空間下面可以有很多子命名空間,放多少層命名空間都是自己定義。

4.當從檔案系統中加載時,每個 namespace 的分隔符(/)要轉換成 DIRECTORY_SEPARATOR (作業系統路徑分隔符)

現在我們終于來到了映射規範了。命名空間的/符号要轉為路徑分隔符,也就是說要把 /Symfony/Core/Request 這個命名空間轉為 \Symfony\Core\Request 這樣的目錄結構。

5.在類名中,每個下劃線_符号要轉換成 DIRECTORYSEPARATOR (作業系統路徑分隔符)。在 namespace 中,下劃線\符号是沒有(特殊)意義的。

這句話的意思就是說,如果我們的命名空間是 /Symfony/Core/Request_a,那麼我們就應該把它映射到 \Symfony\Core\Request\a 這樣的目錄。為什麼會有這種規定呢?這是因為PHP5之前并沒有命名空間,程式員隻能把名字起成 Symfony_Core_Request_a 這樣,PSR0的這條規定就是為了相容這種情況。

剩下兩個很簡單就不說了。

有這樣的命名空間命名規則和映射标準,我們就可以推理出我們應該把命名空間所在的檔案該放在哪裡了。依舊以 Symfony/Core/Request 為例, 它的目錄是 /path/to/project/vendor/Symfony/Core/Request.php,其中 /path/to/project 是你項目在磁盤的位置,/path/to/project/vendor 是項目用的所有第三方庫所在目錄。/path/to/project/vendor/Symfony 就是與頂級命名空間 /Symfony 存在對應關系的目錄,再往下的檔案目錄就是按照 PSR0 标準建立的:

/Symfony/Core/Request => /Symfony/Core/Request.php

一切很完滿了是嗎?不,還有一些瑕疵:

  1. 我們是否應該還相容沒有命名空間的情況呢?
  2. 按照 PSR0 标準,命名空間 /A/B/C/D/E/F 必然對應一個目錄結構 /A/B/C/D/E/F,這種目錄結構層次是不是太深了?

PSR4 标準

2013 年底,新出了第 5 個規範 ——PSR-4。

PSR-4 規範了如何指定檔案路徑進而自動加載類定義,同時規範了自動加載檔案的位置。這個乍一看和 PSR-0 重複了,實際上,在功能上确實有所重複。差別在于 PSR-4 的規範比較幹淨,去除了相容 PHP 5.3 以前版本的内容,有一點 PSR-0 更新版的感覺。當然,PSR-4 也不是要完全替代 PSR-0,而是在必要的時候補充 PSR-0 ——當然,如果你願意,PSR-4 也可以替代 PSR-0。PSR-4 可以和包括 PSR-0 在内的其他自動加載機制共同使用。

PSR4 标準與 PSR0 标準的差別:

  1. 在類名中使用下劃線沒有任何特殊含義。
  2. 命名空間與檔案目錄的映射方法有所調整。

對第二項我們詳細解釋一下 (​​Composer自動加載的原理​​):

假如我們有一個命名空間: Foo/class,Foo 是頂級命名空間,其存在着使用者定義的與目錄的映射關系:

"Foo/" => "src/"      

按照 PSR0 标準,映射後的檔案目錄是: src/Foo/class.php,但是按照 PSR4 标準,映射後的檔案目錄就會是: src/class.php,為什麼要這麼更改呢?原因就是怕命名空間太長導緻目錄層次太深,使得命名空間和檔案目錄的映射關系更加靈活。

再舉一個例子,來源 PSR-4——新鮮出爐的PHP規範:

PSR-0 風格

vendor/
    vendor_name/
        package_name/
            src/
                Vendor_Name/
                    Package_Name/
                        ClassName.php       # Vendor_Name\Package_Name\ClassName
            tests/
                Vendor_Name/
                    Package_Name/
                        ClassNameTest.php   # Vendor_Name\Package_Name\ClassName      
vendor/
    vendor_name/
        package_name/
            src/
                ClassName.php       # Vendor_Name\Package_Name\ClassName
            tests/
                ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest