天天看點

使用 CodeIgniter 架構快速開發 PHP 應用(七)

CodeIgniter 和對象

這是玩家章節。它講述的是 CodeIgniter 的工作原理,也就是揭開CI頭上'神秘的面紗'。如果你是 CI 的新手,你可能想要跳過它。不過, 遲早, 你可能想要了解CI的幕後在發生什麼 ,為什麼不真正的玩轉它呢?

當我剛開始使用 CodeIgniter 的時候,對象使我迷惑。 我是在使用 PHP 4的時候接觸CI的, PHP4并不是真正的面向對象的語言。我在一大堆對象和方法、屬性和繼承,還有封裝等資料結構中轉悠,總是被類似的出錯資訊包圍 " 調用非對象的成員函數". 我如此頻繁地看到它們,是以我想到要印一件T恤衫,上寫: 神秘,無規律可循, 而我仿佛正穿着它站在一個現代藝術展會的會場上。

這一章的内容包含CI使用對象的方法, 和你OO程式設計的方法。 順便說一下,術語 '變量/屬性', '方法/函數'是等義的,當 CI 和 PHP 經常會混着使用它們。比如,你在你的控制器中寫一個 '函數', 純 OO 程式員會稱他們是'方法'。你稱之為類的變量而純OO程式員會叫它們‘屬性’。

面向對象程式設計

我正在假定你和我一樣有 OOP 的基本知識, 但如果隻是在PHP4中嘗試過可能還不太夠。 PHP 4 不是一種 OO 語言, 雖然具備了一些 OO 的特征。 PHP 5 會更好一些, 它的引摯已經徹底改寫成面向對象的了。

不過基本的OO特征PHP4也能實作,而且 CI 設法讓你無論是使用PHP4 還是PHP5,都有一樣的行為特征。

重要的是你要記住,當 OO 程式運作時,總會有一個或數個真實的對象存在。對象可能彼此調用,隻有當對象處于運作狀态的那一刻你才可以讀取變量(屬性) 和方法 (函數)。 是以知道和控制哪個對象目前在運作很重要。當一個類沒有執行個體化時,你不能對它内部的屬性和方法操作,靜态方法和屬性除外。

PHP,作為一個過程式程式設計和OO程式設計的混合體, 可以讓你混合編寫又是過程式又是OO的程式,你可以在一過程式代碼中執行個體化一個類,然後使用它的屬性和方法,用完後把它從記憶體中釋放掉。這一些工作,CI都可以為你代勞。

CI '超級-對象'的工作原理

CI 生成一個超級大對象: 它把你的整個項目當作一個大的對象。

當你啟用 CI 的時候,一連串複雜的事件開始發生。 如果你設定你的 CI 産生日志,你将會見到類似下列的記錄:

1 DEBUG - 2006-10-03 08:56:39 --> Config Class Initialized

2 DEBUG - 2006-10-03 08:56:39 --> No URI present. Default controller 

                                  set.

3 DEBUG - 2006-10-03 08:56:39 --> Router Class Initialized

4 DEBUG - 2006-10-03 08:56:39 --> Output Class Initialized

5 DEBUG - 2006-10-03 08:56:39 --> Input Class Initialized

6 DEBUG - 2006-10-03 08:56:39 --> Global POST and COOKIE data 

                                  sanitized

7 DEBUG - 2006-10-03 08:56:39 --> URI Class Initialized

8 DEBUG - 2006-10-03 08:56:39 --> Language Class Initialized

9 DEBUG - 2006-10-03 08:56:39 --> Loader Class Initialized

10 DEBUG - 2006-10-03 08:56:39 --> Controller Class Initialized

11 DEBUG - 2006-10-03 08:56:39 --> Helpers loaded: security

12 DEBUG - 2006-10-03 08:56:40 --> Scripts loaded: errors

13 DEBUG - 2006-10-03 08:56:40 --> Scripts loaded: boilerplate

14 DEBUG - 2006-10-03 08:56:40 --> Helpers loaded: url

15 DEBUG - 2006-10-03 08:56:40 --> Database Driver Class Initialized

16 DEBUG - 2006-10-03 08:56:40 --> Model Class Initialized

在啟動時-每次一頁通過英特網送出請求-CI 每次啟動都執行相同的程式。你能通過 CI 檔案追蹤記錄:

1. index.php 檔案收到一個頁請求。 URL可能指出哪一個控制器被調用, 如果不, CI 有一個預設值控制器 (第 2 行).Index.php 開始一些基本檢查然後調用 codeigniter.php 檔案(\codeigniter\ codeigniter.php).

2. codeigniter.php 檔案執行個體化 Config 、Router、Input,URL(等等)類.(第 1 行, 和 3-9行) 這些被調用的叫做'基礎'類: 你很少直接與它們互動,但是CI做的每件事都與它們有關。 

3. codeigniter.php 測試了解它正在使用哪一個PHP版本,根據版本決定調用base4 還是base5(/codeigniter/base4.(或base5)php). 這些建立一個 '單一' 執行個體: 即一個類隻能有一個執行個體。 不管哪個都有一個&get_instance() 方法。 注意符号 &:, 這是引用執行個體的符号。 是以如果你調用 &get_instance() 方法, 它産生類的單一執行個體。換句話說,整個應用中這個執行個體是唯一的,其中包含許許多多架構中其它類的執行個體。

4. 在安全檢查之後,codeigniter.php 執行個體化被請求的控制器、或一個預設控制器 (第 10 行) 。 新的類叫做 $CI 。在URL中(或預設值)中被指定的函數被調用,類被執行個體化之後,相當于活了,實實在在存在于記憶體中。 CI 然後将會執行個體化你需要的任何其他的類, 并包含函數庫腳本檔案。 是以在日志中,model類被執行個體化。(第16 行)'模闆檔案' 腳本, 也被裝載(第 13 行), 這是我編寫的包含标準代碼的一個檔案。 它是一個.php 檔案,儲存在scripts目錄中,但是它不是一個類: 僅僅是一組函數。 如果你正在寫 '純粹的' PHP代碼,你可能會使用 'include ' 或者 'require'把這個檔案放進命名空間,CI會使用它自己的 '裝載' 函數把它放入“超級對象“中。

'namespace' 的概念或範圍在這裡是決定性的。 當你聲明一個變量、數組、對象等等的時候,PHP把變量名稱儲存在記憶體中并為它們的内容配置設定一個記憶體塊。如果你用相同的名字定義二個變量就會出現問題。 (在一個複雜的網站中,容易犯這樣的錯誤。) 因為這個原因,PHP 有幾條規則。 舉例來說:

。 每個函數有它自己的namespace 或者範圍, 而且定義在一個函數中的變量一般是一個局部變量。 在函數外面, 它們是看不到的。

。 你能聲明 '全局' 變量, 放在特别的全局 namespace,在整個程式中都可以調用。

。 對象有他們自己的 namespaces:對象内的變量(屬性)是與對象同時存在的,可以通過對象來引用。

是以 $variable, global variable, 和 $this->variable是三件不同的事情。

特别地,在 OO 之前,這可能導緻各種混亂: 你可能有太多的變量在同一namespace中(以緻于許多沖突的變量名互相覆寫),也可能發現有些變量在某個位置無法存取。CI 為此提供了一個解決辦法。

如果你想要了解,哪個類和方法在目前的namespace 中可用, 試着在welcom控制器中插入下列 '檢測' 代碼:

$fred= get_declared_classes();

foreach ($fred as $value) {

    $extensions = get_class_methods($value);

    print "class is $value, methods are: ";

    print_r($extensions);

}

試着運作它,它列出了270個已明的類。 大部分是PHP的。 最後的 11 個來自 CI: 10個是 CI 基礎類 (config 、router等等。) 而且都是我的控制器調用的類。下面列出這11個類,清單隻保留了最後的兩個方法,其它的被省略了:

258: class is CI_Benchmark

259: class is CI_Hooks, 

260: class is CI_Config, 

261: class is CI_Router, 

262: class is CI_Output, 

263: class is CI_Input, 

264: class is CI_URI, 

265: class is CI_Language, 

266: class is CI_Loader, 

267: class is CI_Base, 

268: class is Instance, 

269: class is Controller, methods are: Array ( [0] => Controller [1] => _ci_initialize [2] => _ci_load_model [3] => _ci_assign_to_models [4] => _ci_autoload [5] => _ci_assign_core [6] => _ci_init_scaffolding [7] => _ci_init_database [8] => _ci_is_loaded [9] => _ci_scaffolding [10] => CI_Base ) 

270: class is Welcome, methods are: Array ( [0] => Welcome [1] => 

index [2] => Controller [3] => _ci_initialize [4] => _ci_load_model [5] => _ci_assign_to_models [6] => _ci_autoload [7] => _ci_assign_core [8] => _ci_init_scaffolding [9] => _ci_init_database [10] => _ci_is_loaded [11] => _ci_scaffolding [12] => CI_Base )

注意-看一下Welcome類括号中包含的内容 (270号: 即我正在使用的控制器) ,它列出了Controller類的所有方法 (269 号). 這就是為什麼你總是需要從一個控制器類派生子類的原因-因為你需要你的新控制器保留這些函數。 (而且同樣地,你的models應該總是從model類繼承.) Welcome類有兩個額外的方法: welcome()和index()。 到現在為止,在 270個類中,我寫的隻有這二個函數!

你可能還注意到類的執行個體-即object。 有一個指向它的變量,注意到那個引用符号了嗎?表明在整個系統中,CI_Input類隻有一個執行個體,可以用類變量input調用它:

["input"]=>&object(CI_Input)#6(4){[" use_xss_clean"]=> bool(false)[" ip_address"]=> bool(false)[" user_agent"]=> bool(false)[" allow_get_array"]=> bool}(false)

記得我們何時裝載了input檔案而且建立了最初的輸入類? 它包含的屬性是:

use_xss_clean is bool(false)

ip_address is bool(false)

user_agent is bool(false)

allow_get_array is bool(false)

你可以看到, 他們現在已經全部被包括在執行個體中,“設計圖紙”變成了房子,不是嗎?

所有其它的 CI 的基礎類(routers, output等等。) 同樣地被包含了。 你不需要調用這些基礎類,但是 CI 本身需要他們使你的代碼工作。

引用複制

剛才提到,類變量input引用了CI_Input類:(["input"]=>&object(CI_Input)), 加不加引用符号差別在于:加上引用符号,一變俱變,不加引用符号,原始對象的内容不會改變。你可能會對此感到困惑,用一個簡單的例子來說明:

$one    =    1;

$two    =    $one;

echo $two;

顯示 1, 因為 $two是$one的拷貝。 然而,如果你再重新$one指派:

$one    =    5;

仍然顯示 1, 因為在對 $one 重新指派前 $two已經賦為1了,而$one和$two是兩個不同的變量,各自配置設定有一小塊記憶體,分别存放它們的值。

如果在$one改變的時候,$two也要相應地改變,我們就要使用引用了,這個時候,$one和$two實際上是指向了同個記憶體塊,一變俱變:

代碼: 

$two    =&    $one;

現在顯示5: 我們改變變量$one,實際上也同時改變了$two。

把符号“=” 改成 “=&” 意味着 '引用'. 針對對象來說,如果你要複制一個對象,與原來的對象沒有關聯,用“=”,如果要使用兩個變量指向同一個對象,就使用“&=", 這時候,一個變量作出的任何改變都會影響到别個變量。

在CI'超級對象'中加入你自己的代碼

你可以為CI'超級對象'加上你自己的代碼。假定你已經寫了一個名為Status的model, 它有兩個屬性:$one和 $two, 構造函數配置設定兩個值給他們:$one = 1 和 $two = 2。 當你裝載這model時,讓我們來看看會發生什麼。

instance類有一個變量叫做load, 用來引用對象CI_Loader。 是以你在你的控制器中寫的代碼是:

$this->load->model($status);

換句話說,調用目前CI“超級對象”的類變量load的model方法,裝載一個model, 這個model的名稱存放在變量$status中. 讓我們看一下儲存在/system/libraries/loader.php)中的model方法:

   function model($model, $name ='') {      

      if ($model == '') {

          return;

      }

      $obj=& get_instance();

      $obj->_ci_load_model($model, $name);

   }

(這個函數裡的變量$name是你要裝載的model的一個别名。 我不知道為什麼要使用一個别名,也許它會用在其他的 namespaces 中。)

就象你看到的,model執行個體是被類變量引用的。 因為 get_instance()是一個單一執行個體的方法,你總是針對同一個執行個體進行操作。

如果你再運作控制器, 把我們的 '檢查' 代碼來顯示類變量, 你現在将會見到這個類執行個體包含兩個新的屬性,$one的$two:

["status"]=> object(Status)#12(14){["one"]=> int(1)["two"]=> int(2)... (等等)

換句話說, CI'超級對象' 現在包括一個對象叫做$status, 它包含了我們剛定義的兩個變量,并被賦以我們給定的值1和2。

是以我們正在逐漸地建立一個大的 CI'超級對象', 允許你使用它的某些方法和屬性,而不擔心它們來自哪裡,或處于什麼 namespace 中。

這是需要 CI 箭符号的理由。 為了要使用一個model中的方法, 你一定先裝載model到你的控制器中:

$this->load->model('Model_name');

這使model被裝載入目前控制器類的執行個體中,也就是$this->中。你随後可以調用控制器中的model對象中的方法, 像這樣:

$this->Model_name->function();

就行了。

CI'超級對象'的問題

當Rick剛開始開發CI時,為了讓CI在PHP4和PHP5下行為一緻,他必須在Base4檔案中使用比較醜陋'的代碼,不管醜不醜,我們不用關心,隻要CI能夠在PHP4環境下工作得和PHP5一樣好就行了。

有其他二個話題值得在這裡提一下:

。 你可以嘗試開發一個不是現成的對象并讓它參與工作

。 你必須小心地架構成你的網站, 因為你不能從别一個控制器裡調用某個控制器裡的方法

讓我們一個一個地來分析這二個問題。 你記得我提到的T恤衫那件事嗎?在調用一個成員函數時我一直收到“企圖調用一個非對象的成員函數”的出錯資訊,這個出錯資訊産生的原因一般是因為你調用了一個類方法,但是忘了裝載這個類。換句話說,你寫了下列語句:

但是忘記在它之前調用:

還有一些其它的情形,比如,你在類的一個方法中裝載了model, 然後你企圖在另一個方法裡調用model的成員方法,雖然在同一個對象中,這樣做也不行。是以最好的方法是在類的構造函數中裝載model, 然後可以在這個類的所有方法中使用。

問題也可能更嚴重。 如果你寫你自己的類, 舉例來說,你可能想要使用這個類存取資料庫, 或在你的 config 檔案中讀取資訊, 換句話說,讓這個類存取CI超級對象的某些部分。(如何裝入你自己的類和libraries會在第 13 章讨論。) 概括起來,除非你的新類是一個控制器,一個模型或視圖,它不能在CI 超級對象中被構造。 是以你不能在你的新類中寫這樣的代碼:

$this->config->item(base_url);

這不會工作, 因為對你的新類來說, $this-> 意味着它本身, 而不是 CI 超級對象。取而代之地,你必須通過調用Instance類用另一個變量名(通常是 $obj)把你的新類加入CI超級對象:

$obj =& get_instance();

現在你能象調用CI超級對象一樣地調用它:

$obj->config->item('base_url);

而且這次它能夠工作。

是以,當你編寫你的新類時,記得它有它自己的辨別符。 讓我們使用一個較簡短的例子來把這個問題講得更清楚一點。

你想要寫一個library 類, 用向你的伺服器發出頁面請求的URL查找它的地理位置。 這個library類有點象netGeo類,你可以在下列網址找到它:

<a href="http://www.phpclasses.org/browse/package/514.html" target="_blank">http://www.phpclasses.org/browse/package/514.html</a>

你需要從 CI 的 config 檔案取得基本URL部分。 後半段網址通過你的新類的構造函數中的swith語句生成,如果這個客戶在德國,調用德國頁函數,依次類推。當這個工作在構造函數中做完以後,你需要把結果放到一個類變量中,是以可以在同一個類的其它函數中使用,這意味着:

。 基本URL從CI config檔案中取得,這個隻能通過CI超級對象的引用獲得,換句話說,你可以用$obj-&gt;config-&gt;item('base_url');獲得

。 URL的後半部分由你的新類的構造函數生成,并寫到一個類變量中:$base. 這個變量與CI超級對象無關,它屬于你的新類, 被引用為$this-&gt;base

裝載時會用到兩個關鍵詞:$this-&gt; 和 $obj-&gt;, 在同一段代碼中被引用,舉例來說:

class my_new_class {

  var $base;

  My_new_class() {

      $obj= &amp; get_instance();

      // geolocation code here, returning a value through a 

      // switch statement

      // this value is assigned to $local_url

      $this-&gt;base =$obj-&gt;config-&gt;item('base_url');

      $this-&gt;base .= $local_url;

  }

如果你不清楚這些概念,就會成為頻繁出現“調用非對象的成員函數"的原因. 舉例說,如果你試着調用$obj-&gt;base或 $this-&gt;config-&gt;item()時,這個出錯資訊就出現了。

轉向剩餘的問題, 你不能從一個控制器内部調用别一個控制器的成員方法。 你為什麼會想要這樣做? 這視情況而定。 在一個應用中,我在每個控制器内部寫了一系列自我測試函數, 如果我調用 $this-&gt;selftest(), 它完成了各種不同的有用測試。 但是在每個函數中重複代碼似乎與OO程式設計的設計原則不符,是以我想在其中一個控制器中寫一個函數,可以進入到其它的控制器中執行自我測試代碼。當我這樣做了,期望得到想要的結果。結果,當然不能如我所願,因為在一個控制器内不能調用另一個控制器的成員方法。

作為一個準則,如果你有代碼被超過一個控制器調用的話,把它放入一個model或者其它什麼分離的代碼檔案中,這樣就可以使用了。

這些都是小問題。 正如Rick告訴我的一樣:

"我想要簡化問題,是以我決定建立一個大的控制器對象包含需要的很多對象執行個體...當一個使用者建立他們自己的控制器時,他們能夠輕松地通路任何資源,不用擔心作用域的問題"。

這樣做相當不錯,絕大多數情況下,CI超級對象有效率地,完全處在幕後完成工作。是以我不必要去做那件T恤衫了。

摘要

我們已經看到CI建立的'超級對象' 確定所有的方法和變量可以自動地擷取而不用擔心如何管理以及為“作用域”操心。

CI 用引用執行個體的方法把一個一個類執行個體組合成一個超級對象。大多數情況下,你不需要知道CI超級對象是如何工作的,隻需要正确使用“-&gt;"符号就行了。

我們也學習了如何編寫自己的類,并使它與CI很好地協同工作。