Write the Code. Change the World.

4月 03

原文地址:http://justericgg.logdown.com/posts/196891-php-series-autoload

在不使用框架,手动写php时,使用命名空间和autoload很有用。

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...
";
}
}
是的! 沒什麼特別的,我們在 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...
";
}
}
<?php
//second/Second.php
class Second
{
public function doSomething()
{
echo "I am second class...
";
}
}
新增的這兩個 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...
";
}
}
<?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...
";
}
}
<?php
//classes/MyNamespace/Member/Member.php

namespace MyNamespace\Member;

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

namespace MyNamespace\Email;

class Mailler
{
public function sendMail()
{
echo "send email...
";
}
}
值得注意的是當我們在建立一個新的 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,我們下次見^^

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注