2008年10月16日
[PHP] Smarty 樣版小技巧整理
在 Smarty 中,我們可以用以下的語法來完成類似 for 迴圈的效果:
{section name="forLoop" start=1 loop=10}
{/section}
其中 start 即為起始值, loop 為小於而不等於的終值。
如果以 PHP 語法來表示,則如下所示:
for ($i = 1; $i < 10; $i ++) {
// ...
}
使用無參數原生 PHP 函式
我們可以在樣版中使用許多原生的 PHP 函式 (但還是有例外) 來當做變數修飾函式,不過如果有些函式不需要參數時,我們可以用以下語法來呼叫:
{''|@time}
當然這裡的空字串對 time 函式來說是沒有意義的,而在函式前加上 @ 號,表示要直接呼叫原生 PHP 函式。
在 assign 設定 default 值
假設當樣版變數不存在或可轉換為 false 值時,我們會希望顯示一個預設值;而 Smarty 剛好就提供了一個 default 變數修飾函式讓我們使用,語法如下:
{$test|default:'NULL'}
而 default 變數修飾函式不僅只有在呈現變數能用,也能在樣版 assign 變數時使用,如下:
{assign var=value2 value=$value1|default:0}
註:當然不只有 default ,幾乎所有變數修飾函式都能在 assign 或其他可接受變數的樣版函式裡使用。
物件導向語法
Smarty 也支援 PHP 的物件導向語法,例如呼叫某物件函式:
{$test->getFakeValue()}
我們也能在樣版函式中,使用物件導向語法,而且後面也能串接變數修飾函式:
{assign var=nullValue value=$test->getNullValue()|default:'NULL'}
利用 config 變數或常數來當做變數修飾函式的參數
通常我們會用到設定值來避免重複設定一些常數值,例如用 PHP 陣列的方式指定給 Smarty :
$config = array(
'date' => '%Y/%m/%d',
'time' => '%H:%M',
);
$smarty->assign($config);
在樣版裡就可以這樣用:
{$date|date_format:$config.date}
我們也可以利用 PHP 的常數來當做設定,在樣版可以用「 $smarty.const.常數名稱」來取得常數內容:
{$date|date_format:$smarty.const.DATE_FORMAT}
當然,我們也可以透過 Smarty 內建的 config 機制;例如我們有個 config.conf ,內容如下:
date = "%Y/%m/%d"
time = "%H:%M"
然後在樣版內改用:
{config_load file="config.conf"}
{$date|date_format:#date#} {$time|date_format:#time#}
其中 #date# 與 #time# 就是從 config.conf 中讀取的。
註:感謝 Smarty 頭號粉絲 - 小魚提供以上技巧。
用 {cycle} 配合 CSS 來做表格列交替變色
我們可以用 cycle 配合 sction 或 foreach ,來做出交替變色的表格,語法如下:
{secion ....}
.....
<tr class="{cycle values="normal,alt"}">
.....
{/secion}
可以看出,它比使用 {if} 來得簡潔許多:
<tr class="{if $smarty.section.forLoop.iteration is odd}normal{else}alt{/if}">
註:感謝 Smarty 頭號粉絲 - 小魚提供以上技巧。
如果還有相關技巧的話,將會陸續再更新。
[PHP] Session 相關文章
身為 PHP 開發者,我其實對 PHP Session 的處理機制還是一知半解。
這次因為有個大案子上線的關係,讓我真的遇到了 Session 過多的問題。
這裡強力推薦宗董寫的一篇好文: PHP session 暫存檔過多的注意事項。
不過基本上在連線數會很高的網站,還是建議使用 MySQL 或 memcache 來當做 Session 的儲存媒體。
如果要使用 MySQL 的話,我們可以利用 session_set_save_handler 來建立資料庫連線並存取 Session 資料。
而使用 memcache 的話,可以參考 PHP 官方提供的方法,程式如下:
<?php
$host = 'localhost';
$port = '11211';
$session_save_path = "tcp://$host:$port?persistent=1&weight=2&timeout=2&retry_interval=10, ,tcp://$host:$port ";
ini_set('session.save_handler', 'memcache');
ini_set('session.save_path', $session_save_path);
希望以上的資訊能提供大家對 PHP 處理 Session 有更進一步的瞭解。
2008年08月31日
[PHP] 觀念教室 - 取值函式
通常在物件導向的開發過程裡,我們常會將類別的屬性隱藏起來,然後透過設值及取值函式來存取它們,也就是我們常說的 setter/getter 。
例如:
<?php
class SampleClass
{
private $_attr = 0;
public function setAttr($value)
{
$this->_attr = (int) $value;
}
public function getAttr()
{
return $this->_attr;
}
}
不過設值函式並不是本文的重點,這裡我先略過不談。
從上面的程式碼裡可以看到,取值函式似乎是一段再簡單不過的程式碼,它只是很單純的把物件內部的屬性值回傳而已。
但如果今天我們的屬性值是需要經過計算才能夠回傳的話,怎麼辦呢?我們應該把計算的邏輯寫在取值函式裡嗎?
在討論之前,我們先來假設一個簡單的狀況:
「現在有一個購物車程式,在使用者購買金額未超過 500 元時,我們要在總金額上加上運費 100 元;然而當總金額滿 500 元時,就不需要加入運費了。」
以下就是這個簡化過的購物車程式碼:
<?php
/**
* 購物車類別
*
*/
class Cart
{
/**
* 購物車項目列表
*
* @var array
*/
private $_itemList = array();
/**
* 總金額
*
* @var int
*/
private $_total = 0;
/**
* 加入購物車項目
*
* @param array $item
* @return Cart
*/
public function addItem($item)
{
$this->_itemList[] = $item;
return $this;
}
/**
* 更新購物車
*
* @return Cart
*/
public function refresh()
{
$this->_total = 0;
foreach ($this->_itemList as $item) {
$this->_total += $item['subTotal'];
}
return $this;
}
/**
* 清空購物車
*
* @return void
*/
public function clearAll()
{
$this->_itemList = array();
$this->_total = 0;
}
/**
* 取得總金額
*
* @return int
*/
public function getTotal()
{
if ($this->_total < 500) {
$this->_total += 100;
}
return $this->_total;
}
}
註:我故意省略掉數量、單價等資訊。
在邏輯上,這段程式碼似乎沒有什麼問題,在取得總金額時判斷是否應該加上運費看起來是很合理的。
是嗎?
相信有經驗的朋友,思考一下就能看出這個程式的邏輯錯誤。
看不出來的朋友也沒關係,我簡單寫個測試程式來說明:
<?php
require 'Cart.php';
// 建立購物車物件
$cart = new Cart();
// 顯示總金額 (金額為 520 元,大於 500 元,故不加上運費)
echo $cart
->addItem(array('name' => '商品1', 'subTotal' => 120))
->addItem(array('name' => '商品2', 'subTotal' => 100))
->addItem(array('name' => '商品3', 'subTotal' => 50))
->addItem(array('name' => '商品4', 'subTotal' => 70))
->addItem(array('name' => '商品5', 'subTotal' => 80))
->refresh()
->getTotal(), "\n";
// 再顯示一次總金額 (正確顯示 520 元)
echo $cart->getTotal(), "\n";
// 先清空購物車
$cart->clearAll();
// 顯示總金額 (金額為 340 元,小於 500 元,故加上 100 元運費)
echo $cart
->addItem(array('name' => '商品1', 'subTotal' => 120))
->addItem(array('name' => '商品2', 'subTotal' => 100))
->addItem(array('name' => '商品3', 'subTotal' => 50))
->addItem(array('name' => '商品4', 'subTotal' => 70))
->refresh()
->getTotal(), "\n";
// 再顯示一次總金額 (錯了!竟然變成 540 元!)
echo $cart->getTotal(), "\n";
從上面的程式可以發現,當我們購買金額小於 500 元時, Cart::getTotal 這個取值函式會幫我們自動加上運費。但是我們第二次呼叫 Cart::getTotal 時,程式竟然又幫我們加上了一次運費!
原因我想大家都看出來了,也就是說我們根本不應該在 Cart::getTotal 中加入運費判斷這件事情。這裡正確的做法是應該把運費判斷的條件寫在 Cart::refresh 這個方法裡,而 Cart::getTotal 應該要很單純地只是回傳 $_total 這個屬性值,也就是:
/**
* 更新購物車
*
* @return Cart
*/
public function refresh()
{
$this->_total = 0;
foreach ($this->_itemList as $item) {
$this->_total += $item['subTotal'];
}
// 將運費判斷改到這裡
if ($this->_total < 500) {
$this->_total += 100;
}
return $this;
}
或許有人會問,什麼狀況下你會去呼叫兩次 Cart::getTotal ?其實答案很多,例如有時候在 MVC 模式下,我們常會用 Model 的取值函式來呈現其對應的值,這時我們就可能會多次去呼叫 Cart::getTotal ;也有可能其他團隊成員並不知道 Cart::getTotal 的運作方式,他們一直很快樂地使用著 Cart::getTotal 來取得總金額,直到慘劇的發生...
所以這裡我們暫時得到一個結論:取值函式不應該參與運算邏輯。
是嗎?
其實這樣的結論還是有問題的,接下來我們來看看以下這個計時器類別:
<?php
class Timer
{
public function getTime()
{
return time();
}
}
$timer = new Timer();
echo $timer->getTime(), "\n";
sleep(2);
echo $timer->getTime(), "\n";
很明顯的,對計時器這種物件來說,即時性是很重要的,也就是說每一次取得的時間是會變動的。在這裡,取值函式就參與了時間的計算。
因此,我們要修正剛剛的結論,為它加入一個但書:除非有即時性的需求,否則取值函式不應該參與運算邏輯。
有其他想法嗎?歡迎大家一起討論。
2008年07月22日
[PHP] PHP 5.3 的 Lambda 和 closure ?
這篇也 Lag....這是前陣子的消息。
PHP 5.3 將有可能會有 Lambda (黏巴達?) 和 closure ,來源為 Request for Comments: Lambda functions and closures 這篇文章,所以以後 call_user_func 就不必再用蹩腳的字串來做為函式內容了。而且這是不是也表示 PHP 不但向 Java 靠攏,也開始向 JavaScript 靠攏了?
註:所以 Mark 剛剛跟我說 PHP 快變四不像了。
Lambda 語法如下:
function & (parameters) use (lexical vars) { body }
範例:
$lambda = function () { echo "Hello World!\n"; };
可以這樣用:
$lambda ();
call_user_func($lambda);
call_user_func_array($lambda, array());
所以 call_user_func 系列函式就可以用 Lambda 了。
進階的 Lambda 範例:
function replace_spaces ($text) {
$replacement = function ($matches) {
return str_replace ($matches[1], ' ', ' ').' ';
};
return preg_replace_callback ('/( +) /', $replacement, $text);
}
Closure 語法:
function (normal parameters) use ($var1, $var2, &$refvar) {}
範例:
function replace_in_array ($search, $replacement, $array) {
$map = function ($text) use ($search, $replacement) {
if (strpos ($text, $search) > 50) {
return str_replace($search, $replacement, $text);
} else {
return $text;
}
};
return array_map($map, $array);
}
可以看到原來可以用 callback 的函式,應該都能接受 closure 。
其他就請參考來源文章囉。
相關文章
2008年06月30日
[PHP] PHP 5.3 的新特色: Phar
註:以下程式我並沒有實作過,只是先分享一下給大家。
PHP 5.3 帶來很多有趣的特色,其中一個就是可以把你的 Library 打包成 phar 格式,並透過 require 敘述來引用裡面的程式碼。
首先你可以用 PHP 5.3 的新類別 Phar 來打包你想要的程式碼 (通常是類別檔案) :
<?php
$phar = new Phar('My.phar', 0, 'My.phar'); // 建立 My.phar 檔案
$phar->buildFromDirectory(
dirname(__FILE__) . '/My', '/\.php$/'
); // 打包所有 My 目錄下的 php 檔案
$phar->compressFiles( Phar::GZ ); // 以 GZ 格式壓縮
$phar->stopBuffering(); // 壓縮完成,寫入 phar 檔
然後你可以整包引入:
require 'My.phar';
也可以單獨引入其中的某支檔案:
require 'phar://My.phar/src/Test.php';
就連 PHPUnit 3.3 也試用 Phar 來打包囉了!
那麼效能呢?據 Jan Schneider 的消息指出, PHP 的開發團隊已經把這個功能的效能調整到接近 native PHP 的執行進度了!
很酷吧?期待 PHP 5.3 的來臨吧!
參考
2008年04月14日
[PHP] 在 PHP5 中實作 AOP 的概念
這篇積在我電腦裡很久了,一直沒公開...這次趁著要幫我的 Library 加料,順便拿出來分享一下心得。
什麼是 AOP
AOP 全名為 Aspect-Oriented Programming ,基本的觀念可以參考良葛格的 AOP 入門:
這裡我簡單提一下 AOP 的基本想法:
假設當我們呼叫物件的某些方法 (或是業務流程) 之後,會想要把相關的資訊記錄到 log 檔裡,我們也許會這樣寫:
<?php
/**
* Test
*/
class Test
{
/**
* 某個方法
*/
public function doSomething()
{
// 建立 Log 物件
$logger = new Log();
// 寫入前置 Log
$logger->save('before do something.');
// 主要的動作
// ...
// 寫入 Log
$logger->save('before do something.');
}
}
可是如果今天這個記錄 log 的這個動作只是臨時的,或是在未來可能會需要再加入不同的動作時 (例如寄信) ,難道我們還要在原有方法的程式碼裡修修改改嗎?有沒有什麼方式能協助我們動態地把記錄的動作插在原有動作之後呢?
AOP 就是從這個角度所延伸出來的一種觀念,它能協助我們在不侵入原有類別程式碼的狀況下,動態地為類別方法新增額外的權責;簡單來說, AOP 主要的目的就是切入類別原有方法執行之前或之後,並安插我們想要執行的動作。
註: IT 界似乎很喜歡發明深奧的名詞來詮釋一個簡單的概念,然後像我這樣不學無術的開發者就常被唬得一楞一楞的。
AOP 和 Decorator
先介紹幾篇實作 AOP 的文章:
- AOP for jQuery
- Aspect Oriented Programming in PHP as a contrast to other languages.
- Bunny Aspects
- More on Aspect Oriented PHP
- 在PHP里利用魔术方法实现准AOP
- AOP在PHP中的实现方式
- Class: AOP Library for PHP
其實一開始我以為 AOP 和 Decorator 模式在 PHP 上的實作方式是差不多的,不過實際上還有是些許的差別。
一般在 Decorator 模式中,具體類別和 Wrapper 類別都會有個共同的祖先,亦即一個抽象類別或介面,因此所產生出來的物件對 Client 程式來說,其抽象型態可以說是一樣的。
但是在 AOP in PHP 中,我們必須透過一個代理類別來切入原有的類別方法裡,雖然這個代理類別也能夠提供原有類別中的所有方法,但是實際上它卻已經失去了與原有類別所擁有的抽象型態了。
用 PHP 實作 AOP
首先我們來看看還沒有切入任何事件的目標類別:
<?php
/**
* Test class
*
*/
class TestClass
{
/**
* Method 1
*
* @param string $message
*/
public function method1($message)
{
echo "\n", __METHOD__, ":\n", $message, "\n";
}
/**
* Method 2
*
* @return int
*/
public function method2()
{
echo "\n", __METHOD__, ":\n";
return rand(1, 10);
}
/**
* Method 3
*
* @throws Exception
*/
public function method3()
{
echo "\n", __METHOD__, ":\n";
throw new Exception('Test Exception.');
}
}
這個類別提供了三個方法,其中 method1 和 method2 只是簡單的顯示資料而已,而 method3 則會丟出一個異常。
另外我們需要一個 Log 類別:
<?php
/**
* Log
*
*/
class Log
{
/**
* log message
*
* @param string $message
*/
public function save($message)
{
echo $message, "\n";
}
}
這個 Log 類別只提供一個 save() 方法,以顯示 log 訊息。
現在我們要完成的目標如下:
-
在 method1 執行前呼叫 Log::save() 。
-
在 method2 執行後呼叫 Log::save() 。
-
在 method3 發生異常時呼叫 Log::save() 。
這裡我用很簡單的方式來做,那就是直接使用一個 Aspect 類別:
<?php
/**
* Aspect
*
*/
class Aspect
{
/**
* Name of target class
*
* @var string
*/
private $_className = null;
/**
* Target object
*
* @var object
*/
private $_target = null;
/**
* Event callback
*
* @var array
*/
private $_eventCallbacks = array();
/**
* Add object
*
* @param object $target
* @return Aspect
*/
public static function addObject($target)
{
return new Aspect($target);
}
/**
* Contructor
*
* @param object $target
*/
public function __construct($target)
{
if (is_object($target)) {
$this->_target = $target;
$this->_className = get_class($this->_target);
}
}
/**
* Register event
*
* @param string $eventName
* @param string $methodName
* @param callback $callback
*/
private function _registerEvent($eventName, $methodName, $callback, $args)
{
if (!isset($this->_eventCallbacks[$methodName])) {
$this->_eventCallbacks[$methodName] = array();
}
if (!is_callable(array($this->_target, $methodName))) {
throw new Exception(get_class($this->_target) . '::' . $methodName . ' is not exists.');
}
if (is_callable($callback)) {
$this->_eventCallbacks[$methodName][$eventName] = array($callback, $args);
} else {
$callbackName = Aspect::getCallbackName($callback);
throw new Exception($callbackName . ' is not callable.');
}
}
/**
* Register 'before' handler
*
* @param string $methodName
* @param callback $callback
*/
public function before($methodName, $callback, $args = array())
{
$this->_registerEvent('before', $methodName, $callback, (array) $args);
}
/**
* Register 'after' handler
*
* @param string $methodName
* @param callback $callback
*/
public function after($methodName, $callback, $args = array())
{
$this->_registerEvent('after', $methodName, $callback, (array) $args);
}
/**
* Register 'on catch exception' handler
*
* @param string $methodName
* @param callback $callback
*/
public function onCatchException($methodName, $callback, $args = array())
{
$this->_registerEvent('onCatchException', $methodName, $callback, (array) $args);
}
/**
* Trigger event
*
* @param string $eventName
*/
private function _trigger($eventName, $methodName, $target)
{
if (isset($this->_eventCallbacks[$methodName][$eventName])) {
list($callback, $args) = $this->_eventCallbacks[$methodName][$eventName];
$args[] = $target;
call_user_func_array($callback, $args);
}
}
/**
* Execute method
*
* @param string $methodName
* @param array $args
* @return mixed
*/
public function __call($methodName, $args)
{
if (is_callable(array($this->_target, $methodName))) {
try {
$this->_trigger('before', $methodName, $this->_target);
$result = call_user_func_array(array($this->_target, $methodName), $args);
$this->_trigger('after', $methodName, $this->_target);
return $result ? $result : null;
} catch (Exception $e) {
$this->_trigger('onCatchException', $methodName, $e);
throw $e;
}
} else {
throw new Exception("Call to undefined method {$this->_className}::$methodName.");
}
}
/**
* Get name of callback
*
* @param callback $callback
* @return string
*/
public static function getCallbackName($callback)
{
$className = '';
$methodName = '';
if (is_array($callback) && 2 == count($callback)) {
if (is_object($callback[0])) {
$className = get_class($callback[0]);
} else {
$className = (string) $callback[0];
}
$methodName = (string) $callback[1];
} elseif (is_string($callback)) {
$methodName = $callback;
}
return $className . (($className) ? '::' : '') . $methodName;
}
}
這個類別有點小長,簡單說明如下:
-
我們利用 Aspect::addObject() 方法來指定要被切入的物件; addObject() 方法會回傳一個透明的 Aspect 物件。
-
利用 before 、 after 和 onCatchException 三個方法來指定切入的時機,它們會呼叫 _registerEvent() 方法來註冊要執行的回呼函式 (callback) 。
-
執行原來被切入物件的方法,這時會觸動 Aspect 的 __call() 方法,並在指定的切入時機呼叫 _trigger() 方法來執行我們所切入的回呼函式。
先來看看還沒有使用 AOP 前,我們對 TestClass 類別的測試:
<?php
require_once 'TestClass.php';
$test = new TestClass();
/* @var $test TestClass */
echo "=======\n";
$test->method1('abc');
echo "=======\n";
echo $test->method2(), "\n";
echo "=======\n";
$test->method3();
echo "=======\n";
/* 執行結果:
=======
TestClass::method1:
abc
=======
TestClass::method2:
2
=======
TestClass::method3:
Exception: Test Exception. in TestClass.php on line 38
*/
接下來我們利用 Aspect 類別來對 TestClass 物件的三個方法切入 Log::save() :
<?php
require_once 'Aspect.php';
require_once 'TestClass.php';
require_once 'Log.php';
$test = Aspect::addObject(new TestClass());
$logger = new Log();
$test->before('method1', array($logger, 'save'), 'Log saved (method1).');
$test->after('method2', array($logger, 'save'), 'Log saved (method2).');
$test->onCatchException('method3', array($logger, 'save'), 'Log saved (method3).');
/* @var $test TestClass */
echo "=======\n";
$test->method1('abc');
echo "=======\n";
echo $test->method2(), "\n";
echo "=======\n";
$test->method3();
echo "=======\n";
/* 執行結果:
=======
Log saved (method1).
TestClass::method1:
abc
=======
TestClass::method2:
Log saved (method2).
8
=======
TestClass::method3:
Log saved (method3).
Exception: Test Exception. in TestClass.php on line 38
*/
結論
我們可以從範例看到, AOP 能幫我們在某類別的方法中插入一些額外的動作,同時又能不破壞原有類別的程式碼。而它與 Decorator 最大的不同是, Decorator 必須用很多小類別來完成相同的動作,但是 AOP 則透過 PHP 的動態特性解決了這個問題。
當然 AOP 也不是萬靈丹,像在本文的實作裡它就不能接觸目標類別的非公開屬性。而之前也跟 Mark 聊了一下,其實 AOP 偏向於程式的整體設計,所以這裡的範例尚不能用於實戰之中,僅僅只是我個人一個概念的實作而已。
供大家參考看看吧。也歡迎一起討論~
2008年02月22日
[PHP] PHP 密技: include 與 require
可以接受回傳資料?
先調查一下,知道 include 或 require 可以取得回傳資料的請舉手... (眺望)
呃...不知道的朋友也不用煩惱,我來解釋一下。
如何回傳資料呢?假設現在有個 php 檔叫做 config.php ,內容如下:
<?php
return array('123', '456');
咦?那邊有人說 return 放錯地方了?不不不, PHP 能接受這樣的寫法。
好,現在我們來證明 include 或 require 能取得 config.php 所 return 回來的資料。請建立一支 test.php ,其內容是:
$config = require 'config.php';
var_dump($config);
執行看看,是不是可以跑呀?
所以我們可以在某支 PHP 程式中 return 一個資料 (任何型態) ,然後在另一支 PHP 程式中用 include 或 require 來取得這個資料。
把 require 放在參數裡
什麼?這不是密技?不不不,密技在底下:
function test($config) {
var_dump($config);
}
test(require 'config.php');
對!你沒看錯!直接把 require 放在函式的參數裡!
還沒完呢,再看:
class Test
{
public function __construct($config)
{
var_dump($config);
}
}
$a = new Test(require 'config.php');
連 new 建構子的參數都可以接受 require !
所以只要能放變數的地方,都可以放 include 或 require ,例如:
if (require 'config.php') {
var_dump(require 'config.php');
}
if ($config = require 'config.php') {
var_dump($config);
}
而且不僅是 include 及 require ,連 include_once 和 require_once 都可以這麼做。
我在某篇文章發現這個密技以後,分享給辦公室裡的同事們;沒想到玩了 PHP 這麼多年的他們也沒看過這個方法,看來大家對 PHP 的瞭解需要更深入一點囉!
Scope 的問題
接著我同事問了我一個問題:如果在參數使用 require 敘述,而且被 require 的 PHP 程式裡如果有定義全域變數的話,那麼這個變數在執行的 PHP 程式裡,它的 scope 在哪裡呢?
答案是:它還是全域。
怎麼說呢?現在我們在剛剛的 config.php 的 return 敘述前加上一行程式,如下:
<?php
$data = '789'; // 加上這行
return array('123', '456');
然後在 test.php 裡的 Global 部份 (也就是不在函式或類別定義裡) 的任意處加入:
var_dump($data);
是不是也可以正確顯示 config.php 中 $data 變數所指定的內容呢?這就表示在參數中使用 require 不會影響全域變數的 scope 。
還有其他 include 或 require 的密技嗎?歡迎大家一起討論囉~
什麼!你早就會了?太好了!我們非常非常需要你!
2008年01月16日
[PHP] mysql_query 的記憶體使用與分頁方式
說明
這個實驗主要是探討在 TWPUG 上的這篇 FIEND 寫的: [原創] [分享] 小弟寫的 cakephp 換頁 排序 功能 (第一版) 。
幾個實驗重點如下:
-
FIEND 提到兩次 Query 不是一個好方式,他的做法是用一次 Query 配合 while + mysql_result 就能做到分頁效果。
-
另外 shirock 從 PHP 原始碼的部份解釋 PHP 和 MySQL 抓資料後的處理方式,但 FIEND 卻說「用屁股想都知道 PHP 不可能把 QUERY 結果 全部拉回 PHP 端記憶體」。
- 還有 shirock 提到:「參考文章中已經很明白指出 mysql_query 跳過 PHP 內建記憶體配置機制,而直接使用 mysql C library 的函數儲存資料在 PHP 程序這端。而 memory_limit 只會管制到 PHP 內建記憶體配置機制的使用上限。所以 mysql_query 查詢大量資料時,不會受到 memory_limit 的限制。」
基本上我從來不知道屁股可以用來思考,所以我還是要實事求是,用 FIEND 的方法實驗一次。
註:不過我老是在上廁所時想到一些靈感...Orz
環境
- Windows XP
- PHP 5.2.5
- MySQL 5.0.45
- memory_limit = 16M (in php.ini)
另外我準備了一個資料庫,裡面包含了四個資料表:
| 資料表 | 筆數 | 硬碟空間 |
|---|---|---|
| r1000 | 一千筆 | 75KB |
| r10000 | 一萬筆 | 743KB |
| r100000 | 十萬筆 | 7,422KB |
| r1000000 | 一百萬筆 | 74,219KB |
資料表欄位為一個 id 欄位和一個 value 欄位;而 value 欄位為 varchar(64) ,其內容存的是兩個隨機的 md5 函式結果所組合的字串。可以用以下程式產生:
<?php
echo "CREATE DATABASE `page_test`;\n";
echo "USE `page_test`;\n";
foreach (array(1000, 10000, 100000, 1000000) as $r) {
$sql = <<<SQL
CREATE TABLE IF NOT EXISTS `r$r` (
`id` int(10) unsigned NOT NULL auto_increment,
`value` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM;
SQL;
echo $sql, "\n";
for ($i = 1; $i <= $r; $i ++) {
$value = md5(rand(0, 9999)) . md5(rand(0, 9999));
echo "INSERT INTO `r$r` (`value`) VALUES ('$value');\n";
}
}
程式
程式部份很簡單,就是按照 FIEND 說的步驟來寫的。只是我這裡改用 CLI 模式執行,以避掉 Apache 的影響。
<?php
echo ini_get('memory_limit'), "\n"; // 16M
echo "\n";
echo "========================\n";
echo "Start.\n";
echo "========================\n";
sleep(10);
$link = mysql_connect('localhost', 'username', 'password');
mysql_select_db('page_test', $link);
$result = mysql_query('SELECT * FROM r1000000', $link);
echo "\n";
echo "========================\n";
echo "mysql_query\n";
echo "========================\n";
sleep(10);
$count = mysql_num_rows($result);
echo 'count: ', $count, "\n";
echo "\n";
echo "========================\n";
echo "mysql_num_rows\n";
echo "========================\n";
sleep(10);
$start = rand(0, $count - 1);
$end = $start + 10;
$i = $start;
echo 'start: ' . $start, "\n";
echo 'end: ' . $end, "\n";
echo "\n";
while ($id = mysql_result($result, $i, 'id')) {
echo $i, ': ', $id, "\n";
$i ++;
if ($i >= $end || $i >= $count) break;
}
echo "\n";
echo "========================\n";
echo "while & mysql_result\n";
echo "========================\n";
sleep(10);
mysql_free_result($result);
echo "\n";
echo "========================\n";
echo "mysql_free_result\n";
echo "========================\n";
sleep(10);
mysql_close($link);
echo "\n";
echo "========================\n";
echo "mysql_close\n";
echo "========================\n";
sleep(10);
而記憶體的觀察我是用 Windows 的工作管理員,然後查看 php.exe 所使用的記憶體大小。在什麼都沒有執行的狀況下, php.exe 會佔用掉約 8MB 的記憶體 (在我的環境下) 。
結果如下 (每項執行 5 次後再取平均值) :
| 資料表 | 程式啟動 | mysql_query | mysql_num_rows | while & mysql_result | mysql_free_result | mysql_close |
|---|---|---|---|---|---|---|
| r1000 | 8,196KB | 8,592KB | 8,596KB | 8,656KB | 8,568KB | 8,556KB |
| r10000 | 8,196KB | 9,496KB | 9,500KB | 9,560KB | 8,592KB | 8,580KB |
| r100000 | 8,192KB | 18,840KB | 18,844KB | 18,904KB | 8,584KB | 8,596KB |
| r1000000 | 8,192KB | 110,684KB | 110,688KB | 110,748KB | 8,796KB | 8,784KB |
然後我比較 mysql_query 用掉的記憶體和原來資料表使用的硬碟空間:
| 資料表 | mysql_query | 硬碟空間 |
|---|---|---|
| r1000 | 396KB | 75KB |
| r10000 | 1,300KB | 743KB |
| r100000 | 10,648KB | 7,422KB |
| r1000000 | 102,492KB | 74,219KB |
很明顯地 mysql_query 所得到的 resource 佔的記憶體空間比 MySQL 使用的硬碟大小還多,這證明了 mysql_query 是有把資料內容抓到 PHP 這邊來;至於為什麼 MySQL 的反而比較小,我想這應該是 MySQL 將資料做壓縮的關係。
補充:我上面在 MySQL 壓縮的部份沒有任何根據,應該是錯誤的推論;因此我想就以 normansu 給我的說明為準:
應該不是.
執行 mysql_store_result 的時候,
在 result 和每一筆 record 都會多一個 Header 的空間,
大小不一定(看 field count).
所以使用的記憶體會比實際 mysql table 大.
在 source 中沒有看到任何 compress 的動作.
mysql 的這個流程讓我嚇一跳,
以往大部份用的是 mssql 和 oracle,
在 client 和 server 間的 data cache 機制都做得比較好,
看起來 mysql 像是把結果算出來後就直接全部丟出來.
所以使用 mysql 要比用 mssql 或oracle 要來得更小心一點.
然後由於在上面的實驗裡我已經將 php.ini 的 memory_limit 設為 16M ,也用過 php -i 檢查過了。但在執行一百萬筆測試時,卻沒有受到任何影響,因此也證明了 shirock 說的「所以 mysql_query 查詢大量資料時,不會受到 memory_limit 的限制。」
結論
從上面的實驗可以看到一次 Query 並取得資料總筆數雖是可行的,但這個前提是建立在 mysql_query 已經把資料內容全部放到 php.exe 的記憶體中。除非我誤解了 FIEND 的意思,不然他的作法看起來實在不適用於他所說的「存取大量資料的環境」。
所以一般常見的做法是採用就是 tokimeki 提到的兩次 Query 的方式,第一次先利用 SQL 的 COUNT() 指令取得我們所需要的總筆數,第二次再配合 LIMIT 去取得我們所要的資料。不用 LIMIT 的後果就是每當執行一次 script ,我們就要冒著記憶體使用量爆增的後果。
註:雖然 MySQL 有 Query Cache ,但對 php 端已經爆增的記憶體也於事無補了。
而我也贊成 tokimeki 說的:「另外之二,假設內容資料是非常龐大的,且必須利用查詢內容作某些運算(例如:矩陣運算之類的),那麼這樣的應用不該由PHP程式來完成,應由其他的方式來作計算(例如:資料庫作OLAP或是Server上某個用C/C++寫的程式定期跑),Web這邊只做顯示以及計算排程即可。」
其他不想多說了,被某人看不起也不是一天兩天的事了。我自知自己還有很多東西要學,而這些還有望其他高手前輩們給予我指教。
不過最後這個實驗也證明一件事:屁股不是用來想事情的。
2007年11月23日
[PHP] 交換兩個變數 (不使用 tmp 變數) 程式寫法
在宗董的 Blog 看到這篇:交換兩個變數 (不使用 tmp 變數) 程式寫法,本來想留言,不過宗董的 Blog 系統似乎有問題。
宗董的方法是這樣的:
$a ^= $b;
$b ^= $a;
$a ^= $b;
我是想說既然是用 PHP 了,就應該好好善用一下 PHP 的原生語法:
list($a, $b) = array($b, $a);
搞定~~
這個是從 PHP 程式設計專家必備手冊一書看來的。
2007年10月26日
[PHP] 神奇的 $this
今天發現了一個 PHP 5.2.4 的奇怪現象,查了官方手冊也沒發現有人特別提起 (也可能是我沒找到) 。
學過 PHP 物件導向的人都知道, $this 這個關鍵字是在生成一個物件後才能使用的。例如:
class Foo
{
private $_foo = '_foo in class Foo.';
public function test()
{
echo $this->_foo;
}
}
$foo = new Foo();
$foo->test(); // _foo in class Foo.
而且 $this 在 Class 的程式碼裡代表的也是這個物件本身,在上例中即為 $foo 。
不過在 method 裡使用 $this 有個限制,那就是該 method 不能以 static 的方式來呼叫;也就是說,以下的執行方式是錯的:
Foo::test(); // Fatal error: Using $this when not in object context in xxx.php
可是請看以下的程式碼:
<?php
class Foo
{
private $_foo = '_foo in class Foo.';
public function test()
{
echo $this->_foo;
}
}
class Bar
{
public function test()
{
Foo::test();
}
}
$b = new Bar();
$b->test(); // Notice: Undefined property: Bar::$_foo in xxx.php
發現什麼問題了嗎?在 Bar::test() 裡我們竟然可以用 static 的方式呼叫 Foo::test() ! 而且在 Foo::test() 裡的 $this->_foo 竟然變成了 Bar 類別的 $_foo 屬性!
至於這倒底是 PHP 的特色還是 Bug ?我也不知道,還望高手賜教。
