PHP自動加載功能原理解析

前言


在開始以前,歡迎關注我本身的博客:www.leoyang90.cnphp

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

1、PHP自動加載功能


PHP自動加載功能的由來

在PHP開發過程當中,若是但願從外部引入一個 class,一般會使用 include 和 require 方法,去把定義這個 class 的文件包含進來。這個在小規模開發的時候,沒什麼大問題。但在大型的開發項目中,使用這種方式會帶來一些隱含的問題:若是一個 PHP 文件須要使用不少其它類,那麼就須要不少的 require/include 語句,這樣有可能會形成遺漏或者包含進沒必要要的類文件。若是大量的文件都須要使用其它的類,那麼要保證每一個文件都包含正確的類文件確定是一個噩夢, 何況 require_once 的代價很大。laravel

PHP5 爲這個問題提供了一個解決方案,這就是類的自動裝載 (autoload) 機制。autoload 機制可使得 PHP 程序有可能在使用類時才自動包含類文件,而不是一開始就將全部的類文件 include 進來,這種機制也稱爲 lazy loading。 程序員

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

  1. 使用類以前無需 include 或者 requirecomposer

  2. 使用類的時候纔會 require/include 文件,實現了 lazy loading,避免了 require/include 多餘文件。框架

  3. 無需考慮引入類的實際磁盤地址,實現了邏輯和實體文件的分離。函數

  
若是想具體詳細的瞭解關於自動加載的功能,能夠查看資料:
PHP的類自動加載機制
PHP的autoload機制的實現解析post

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 思想,命名空間在必定程度上算是類名的別名,那麼爲何要推出命名空間,命名空間的優勢是什麼呢

2、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 新特性系列(一) —— 命名空間

3、PSR標準


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

具體詳細的規範標準能夠查看
PHP中PSR規範

PSR0 標準

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

一、 一個徹底合格的 namespace 和 class 必須符合這樣的結構:「< Vendor Name>(< Namespace>)*< Class Name>」
二、每一個 namespace 必須有一個頂層的 namespace("Vendor Name" 提供者名字)
三、每一個 namespace 能夠有多個子 namespace
四、當從文件系統中加載時,每一個 namespace 的分隔符(/)要轉換成 DIRECTORY_SEPARATOR (操做系統路徑分隔符)
五、在類名中,每一個下劃線(_)符號要轉換成 DIRECTORY_SEPARATOR (操做系統路徑分隔符)。在 namespace 中,下劃線_符號是沒有(特殊)意義的。
六、當從文件系統中載入時,合格的 namespace 和 class 必定是以 .php 結尾的
七、verdor name,namespaces,class 名能夠由大小寫字母組合而成(大小寫敏感的)

  
具體規則可能有些讓人暈,咱們從頭講一下。
  
咱們先來看PSR0標準大體內容,第一、二、三、7條對命名空間的名字作出了限制,第四、5條對命名空間和文件目錄的映射關係作出了限制,第6條是文件後綴名。
  
前面咱們說過,PSR標準是如何規範命名空間和所在文件目錄之間的映射關係?是經過限制命名空間的名字、所在文件目錄的位置和二者映射關係。
  
那麼咱們可能就要問了,哪裏限制了文件所在目錄的位置了呢?其實答案就是:
  

限制命名空間名字 + 限制命名空間名字與文件目錄映射 = 限制文件目錄

  
好了,咱們先想想,對於一個具體程序來講,若是它想要支持PSR0標準,它須要作什麼調整呢?

  1. 首先,程序必須定義一個符合PSR0標準第四、5條的映射函數,而後把這個函數註冊到 spl_register() 中;

  2. 其次,定義一個新的命名空間時,命名空間的名字和所在文件的目錄位置必須符合第一、二、三、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,若是沒有頂級命名空間,咱們就得寫兩個路徑和這兩個命名空間相對應,若是再有 Request二、Request3 呢。有了頂層命名空間 /Symfony,那咱們就僅僅須要一個目錄對應便可,剩下的就利用 PSR 標準去解析就好了。
  

3.每一個namespace能夠有多個子namespace

這個很簡單,Request 能夠定義成 /Symfony/Core/Request,也能夠定義成 /Symfony/Core/Transport/Request,/Core 這個命名空間下面能夠有不少子命名空間,放多少層命名空間都是本身定義。

  

4.當從文件系統中加載時,每一個 namespace 的分隔符(/)要轉換成 DIRECTORY_SEPARATOR (操做系統路徑分隔符)

如今咱們終於來到了映射規範了。命名空間的/符號要轉爲路徑分隔符,也就是說要把 /Symfony/Core/Request 這個命名空間轉爲 SymfonyCoreRequest 這樣的目錄結構。
  

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

這句話的意思就是說,若是咱們的命名空間是 /Symfony/Core/Request_a,那麼咱們就應該把它映射到 SymfonyCoreRequesta 這樣的目錄。爲何會有這種規定呢?這是由於 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

  PSR-4風格

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

對比以上兩種結構,明顯能夠看出PSR-4帶來更簡潔的文件結構。

Written with StackEdit.

相關文章
相關標籤/搜索