Okay,我們可以透過 include, include_once, require 或是 require_once 來將檔案引入到我們目前正在編寫的這個檔案,我們也知道習慣上我們會將一個 Class 存放在單一的 PHP 檔案中,例如 Member.php 相對於 Member 這個 Class,當我們的程式需要用到這個 Class 的時候就可以用上述的方法來引用這個 Class 以供後續操作。

//index.php
include_once "Member.php";

$member = new Member();
$member->getMemberList();

完美!!! 但是如果今天我們有一卡車的 Classes 要引入呢?

//index.php
include_once "Member.php";
include_once "Mailsender.php";
include_once "Validate.php";
inlcude_once "Other.php";

What the #$%^# ... Okay! 夠了! 這看上去實在是不怎麼舒服,光這些引入就佔了一堆行數,我都還沒開始寫呢! 那有沒有辦法可以解決這個問題呢?有的 - Autoload !!!

PHP Autoload

PHP Autoload 機制可以讓我們能夠在需要這個物件的時候才去真正的引入這個 Class,這個動作就是常聽到的 Lazyload 延遲載入,在這裡我們不會談論實際上 PHP 怎麼去實現這個機制,只階段性的介紹 PHP 內幾種 Autoload 的做法。

  • __autoload
  • spl_autoload
  • autoload 與 namespace

__autoload

PHP5 提供了 __autoload() 這個魔術方法實現上述 Autoload 機制,雖然這個方法效能上及方便性並不是非常理想(後續會提到其他做法),讓我們來用這個方法實際寫點東西吧!

為了讓例子看起來比較簡單,我們將不會把資料夾分的太細,以下為我們的根目錄資料夾。

+ classes
+-- Member.php
- autoload.php
- index.php

首先我們看到 classes 資料夾底下的 Member.php。

<?php
//classes/Member.php
class Member
{
    public function getMemberList()
    {
        echo "print member list...<br>";
    }
}

是的! 沒什麼特別的,我們在 classes 資料夾底下建立了一個 Member Class 包含了一個 public method getMemberList() 簡單的輸出一串字串 print member list...

接著來到我們的重頭戲 autoload.php 這個檔案

<?php
//autoload.php
function __autoload($className) {    
    $filename = __DIR__ . "/classes/" . $className . ".php";
    if (is_readable($filename)) {
        require $filename;
    }
}

在這個 function 內,我們首先取得存放 classes 路徑中的檔案名稱 $filename 接著我們用到了 is_readable() 這個 PHP 內建 function 判斷是檔案是否存在及可讀取,然後 require 引入這個檔案。聽起來棒極了,但是你沒有告訴我怎麼使用這個 __autoload() 這個 function 啊!!! 別急,究竟要怎麼讓這個東西動起來,讓我們看下去...

<?php
//index.php
include_once __DIR__ . "/autoload.php";

$member = new Member();
$member->getMemberList();

我們將 autoload.php 引入到我們程式的最一開始,現在當需要使用在 classes 目錄底下(不包含子目錄)的所有 class 都將會自動被載入,你不需要在手動的引入你所需要的 class。

執行 index.php 會輸出一段文字 print member list... 在你的 browser 上,太神奇了!!!

什麼!!! 有件事我忘了跟你說嗎? 呃... __autoload() 不能重覆的被定義,也就是說,當你嘗試引入兩支不一樣的 autoload.php (或許你有其他目錄底下的 class 也想被自動載入...) 的時候,PHP 將會報出錯誤訊息 Fatal error: Cannot redeclare __autoload()...

這樣子很不方便耶! 別急,PHP 當然知道要幫你解決這個問題,下一章節將說明。
(你當然可以通過 php.ini 設定 include path 來解決這樣子的問題,不過這有點不明智就是了...)

spl_autoload 與 spl_autoload_register

PHP 在 5.1.2 以後提供了 SPL 這個方便的大玩意,SPL 全名為 Standard PHP Library,顧名思義,提供你 許多 Interface 及 class,目的為解決常見的問題。

  • spl_autoload
    預設實作 __autoload() 魔術方法,也就是說,當 spl_autoload_register() 沒有定義或是沒有帶任何參數的時候,預設會採用 __autoload() 這個 function

  • spl_autoload_register
    spl_autoload_register() 讓你能夠註冊你自定義的 autoload function,當 spl_autoload_register() 有註冊之後,__autoload() 將會失效,你必需自己手動將 __autoload() 註冊,才能使用。

廢話不多說,我們來寫點程式。

首先我們新增兩個目錄 first, second 在我們剛剛建立的目錄,同時也個新增兩個 class 在 first 和 second 底下。

+ classes
+-- Member.php
+ first
+-- First.php
+ second
+-- Second.php
- autoload.php
- index.php

讓我們來看一下 First.php 和 Second.php 的內容。

<?php
//first/First.php
class First
{
    public function doSomething()
    {
        echo "I am first class...<br>";
    }
}
<?php
//second/Second.php
class Second
{
    public function doSomething()
    {
        echo "I am second class...<br>";
    }
}

新增的這兩個 class 內容沒什麼特別的,各有一個 doSomething() 的 method 分別輸出 I am first class...I am second class..., 接著我們來修改一下我們 index.php。

<?php
//index.php
$first = new First();
$first->doSomething();

$second = new Second();
$second->doSomething();

聰明的你一定會說,你把 autoload.php 的引入拿掉,這一定出錯的啊! 沒錯,所以我們現在要來註冊我們自己定義的 autoload,我們在資料夾底下再新增兩個檔案 firstAutoload.php 和 secondAutoload.php。

+ classes
+-- Member.php
+ first
+-- First.php
+ second
+-- Second.php
- firstAutoload.php
- secondAutoload.php
- autoload.php
- index.php

接著我們來看看這兩個 autoload 裡寫了些什麼。

<?php
//firstAutoload.php
function firstAutoload($className) {    
    $filename = __DIR__ . "/first/" . $className . ".php";
    if (is_readable($filename)) {
        require $filename;
    }
}

spl_autoload_register('firstAutoload');
<?php
//secondAutoload.php
function secondAutoload($className) {    
    $filename = __DIR__ . "/second/" . $className . ".php";
    if (is_readable($filename)) {
        require $filename;
    }
}

spl_autoload_register('secondAutoload');

我們定義了自己的 autoload function 之後就使用 spl_autoload_register() 註冊這個 function 的名稱,接著我們在 index.php 裡把這兩個自定義的 autoload 檔案引入。

<?php
//index.php
include_once __DIR__ . "/firstAutoload.php";
include_once __DIR__ . "/secondAutoload.php";

$first = new First();
$first->doSomething();

$second = new Second();
$second->doSomething();

完美的輸出像這樣子的畫面...

I am first class...
I am second class...

眼尖的你一定發現了,我們這樣子每要加入一個目錄放 class 不就要多寫一個 autoload,而且還要引入這些檔案,那不就跟原來一樣,喔不對,還比原來麻煩。
嗯,你說的沒錯,那我們用 foreach 來處理這樣的問題呢?

我們再新增一個檔案 allAutoload.php 在目錄底下,並看看內容寫了些什麼。

<?php
//allAutoload.php

function allAutoload($className)
{
    $folders = array(
        __DIR__ . "/first/",
        __DIR__ ."/second/",
    );
    
    foreach ($folders as $folder) {
        $fileName = $folder . $className . ".php";
        if (is_readable($fileName)) {
            require $fileName;
        }
    }
}

spl_autoload_register('allAutoload');

Okay,我們把我們想要自動載入的目錄都設定在一個 array 裡,接著我們 foreach 這個 array,裡面做的事其實就跟先前的一樣,接著我們用 spl_autoload_register() 註冊這個 function 的名稱,我們來看一下 index.php 做了哪些改變。

<?php
//index.php
include_once __DIR__ . "/allAutoload.php";

$first = new First();
$first->doSomething();

$second = new Second();
$second->doSomething();

這次我們只需要引入一個 allAutoload.php 檔案了,我們又再一次完全的輸出了...

I am first class...
I am second class...

看起來是不錯,不過聰明的你一定又發現了,這樣子我們每次要新增一個 class 目錄或是子目錄的時候,都要去更動我們的 autoload 檔案耶,這樣還是很麻煩呀!

沒關係,是 namespace 該出場的時候了。

autoload 與 namespace

PHP 5.3.0 以後提供了namespace (命名空間) 的功能解決了 class 名稱重覆定義造成衝突的問題,這問題很容易發生在你的 project 內有使用到其他第三方的程式庫的時候。而 PHP FIG 也在它所制定的 PSR-0 定義了依照 namespace 使用 autoload 的方法。

我們先來看一下 簡單的 namespace 使用方法。

<?php 
//MyClass.php

namespace MyNamespace;

class MyClass
{
    public function doSomething()
    {
        echo "This is my class...<br>";
    }
}
<?php
//index.php
include_once __DIR__ . "/MyClass.php";

$myClass = new \MyNamespace\MyClass();
$myClass->doSomething();

在 MyClass.php 內,我們看到 namespace MyNamespace; 定義了這個 class 的命名空間,也就是說當我們之後需要實例化這個 class 的時候,我們不能像這樣子實例化這個 class new MyClass();,我們需要在 class 名稱前面加上命名空間,命名空間以 \ 做分隔,new \MyNamespace\MyClass();,如同上面的 index.php 一樣,執行 index.php 完美的輸出...

This is my class...

讓我們來依照 PSR-0 的 autoload 範例(出處: http://www.php-fig.org/psr/psr-0/)來寫我們的 autoload 吧!
首先我們想要將我們的 class 全部整理到根目錄下的 classes 目錄的 MyNamespace 目錄底下,我們先來看一下我們的目錄結構。

+ classes
+-+ MyNamespace
- autoload.php
- index.php

當然,我們還沒有建立任何的 class 我們的 MyNmaespace 目錄底下,我們先來看一下 autoload.php 寫了些什麼。

<?php
//autoload.php

function autoload($className)
{
    $className = ltrim($className, '\\');
    $fileName  = '';
    $namespace = '';
    if ($lastNsPos = strrpos($className, '\\')) {
        $namespace = substr($className, 0, $lastNsPos);
        $className = substr($className, $lastNsPos + 1);
        $fileName  = 'classes/' . str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
    }
    $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
    
    require $fileName;
}

spl_autoload_register('autoload');

autoload.php 會依照定義的 namespace 把 \ 替換成系統的目錄分隔符號(最後一個 \ 之後的 _ 也會被替換成系統目錄分隔符號),找到相對應的 class 檔案並引入。我們只要在程式的一開始引入這支 autoload.php 檔案,就可以在不需要手動引入 class 的情況下任意的實例化 MyNamespace 目錄底下的 class 檔案並進行操作,讓我們實際在 MyNamespace 目錄底下建立一個 class 及 兩個子目錄各包含一個 class 檔案。

+ classes
+-+ MyNamespace
+-+-- MyClass.php
+-+-+ Member
+-+-+-- Member.php
+-+-+ Email
+-+-+-- Mailler.php
- autoload.php
- index.php
<?php
//classes/MyNamespace/MyClass.php

namespace MyNamespace;

class MyClass
{
    public function doSomething()
    {
        echo "This is my class...<br>";
    }
}
<?php
//classes/MyNamespace/Member/Member.php

namespace MyNamespace\Member;

class Member
{
    public function getMemberList()
    {
        echo "This is member list...<br>";
    }
}
<?php
//classes/MyNamespace/Email/Mailler.php

namespace MyNamespace\Email;

class Mailler
{
    public function sendMail()
    {
        echo "send email...<br>";
    }
}

值得注意的是當我們在建立一個新的 class 的時候,千萬記得將 class 的 namespace 定義到正確的目錄結構上,這樣子我們的 autoload 才能夠正確的辨認相對應的 class,我們來看看我們的 index.php 內容與執行的結果吧!

<?php
//index.php
include_once __DIR__ . "/autoload.php";

$myClass = new \MyNamespace\MyClass();
$myClass->doSomething();

$member = new \MyNamespace\Member\Member();
$member->getMemberList();

$mailler = new \MyNamespace\Email\Mailler();
$mailler->sendMail();

執行結果如下。

This is my class...
This is member list...
send email...

我們只引入了 autoload.php,之後只要是要實例化在 MyNamespace 目錄底下的 class 就都不用再手動引入了,未來想要增加新的 class 也不需要再去定義 array 或是 設定 php include path,如果覺得 new \MyNamespace\Member\Member();這樣的宣告方式太冗長了,可以考慮使用 use 這個 keyword,在 Using namespaces: Aliasing/Importing 有詳細的用法。
Okay,我想我們今天該告一段落了,如果覺得不想自己寫這些 autoload 的 function,有機會我會在寫一篇如何使用 composer 套件管理中的 autoload 來更方便的管理你的 autoload,我們下次見^^