คราวก่อนเขียนเรื่อง Page Caching with CodeIgniter and Cache Clearing code เป็นการเกริ่นถึงวิธีใช้งาน Page Caching ของ Codeigniter รวมไปถึงผมได้แนบคลาสสำหรับการ ลบแคช (Cache Clearing) เข้าไปด้วย เพื่อความสะดวกในการใช้งาน
บทความนี้เลยคล้ายๆ กับบทต่อจากเรื่องข้างต้นนะครับ เป็นกรณีศึกษาที่ผมเจอและจำเป็นต้องใช้ เลยมาขอแชร์ความรู้นิดหนึ่ง เผื่อมีใครมีแนวทางที่ดีกว่าของผมนะครับ
Mini Introduction Page Caching with CodeIgniter
เกริ่นพื้นฐานเล็กน้อยครับ ปกติการทำ Page Caching ใน Codeigniter เราจะเก็บลงโฟลเดอร์หนึ่ง สมมุติว่าชื่อ "system/cache/" นะครับ ส่วนชื่อไฟล์ที่ทำการ Cache ก็จะนำเอา URI(หรือ URL) มาเข้ารหัสด้วย md5 เช่น "http://lab.tosdn.com" จะถูกเข้ารหัสและนำไปตั้งชื่อไฟล์ คือ "53db6ab6193a34a4ebac45ebd7592da6"
ถ้าไม่เข้าใจ ไปอ่านเต็มๆ ที่บทความเก่าครับ - Page Caching with CodeIgniter and Cache Clearing code
Problem of Page Caching with CodeIgniter
คราวนี้ ปัญหาที่ผมเจอมี 2 ข้อครับ
1. หากการทำ Caching มีไฟล์มากมายมหาศาลจะทำอย่างไร? ซึ่งปํญหานี้อาจจะไม่ค่อยสำคัญ หากเว็บเราไม่ได้คิดการใหญ่ และหน้าเพจไม่มากนัก
2. ถ้าเราเปิดให้สมาชิกมี Sub-Domain ได้เอง แปลว่า ในเว็บเราจะเสมือนมีเว็บสมาชิกย่อยลงไปอีก คราวนี้ ปัญหาที่หนีไม่พ้นก็คือ ปัญหาในข้อ 1 และ ปัญหาที่สำคัญตามมาคือ การบริหารจัดการไฟล์ Caching ที่ยากขึ้น เช่น สมาชิกคนนี้เปลี่ยนแปลงชื่อใน profile ตัวเอง ดังนั้น หน้าใน Sub-Domin ต้องมีการเปลี่ยนแปลงด้วย เช่นนั้นแล้วเราต้องมาทำการค้นหาว่า Cacgin Page ของสมาชิกคนนี้ มีไฟล์ไหนบ้าง แค่คิดก็สนุกแล้วครับ T-T
หรือบางคนไม่ได้ใช้เป็น Sub-Domain แต่ใช้เก็บค่าสมาชิกใน Session หรือ Cookie หรือ อื่นๆ แต่ URI ยังใช้คงเดิม เช่น สมาชิก A,B,C ก็เข้าแก้ไขชื่อตนเองใน http://www.web.com/profile เหมือนกัน ดังนั้น Caching ที่เก็บ มันก็จะมีชื่อเหมือนกัน อยู่ที่ว่า Codeigniter ทำการ Generate Page Caching ให้คนไหนก่อน พอคนต่อมาเข้ามาใช้ มันก็จะเจอข้อมูลของคนแรกที่ถู Generate ไว้ มันช่างวุ่นวายได้อีก T-T
Solution of Page Caching with CodeIgniter
เมื่อเจอปัญหาแบบนั้น ผมเลยลองแก้ไขด้วยการจัดระเบียบโฟลเดอร์ของ Page Caching ให้ลึกลงไปอีก 2 ชั้นโดยชั้นแรกเก็บเป็นอักษรหน้า 3 ตัว และชั้นสองเก็บ อักษรเต็มของชื่อ Domain หรือ Sub-Domain ไว้ และไฟล์ข้างในโฟลเดอร์นี้ ก็จะเก็บเฉพาะ หน้านั้นๆของ Domain หรือ Sub-Domain ที่ระบุไว้เท่านั้น
เอาหละครับ เมื่อทราบที่มา ปัญหา และวิธีแก้ไข ก็ลงมือกันเลย :D
How to use Sub-Domain or Many File in Page Caching with CodeIgniter
1. สร้างไฟล์ Library ใน Application มาหนึ่งตัวครับ เช่น ปล. หากในบทความก่อน ใครสร้างไว้แล้วก็ข้ามไปครับ
/system/application/libraries/My_Output.php (ใครตั้ง Suffix ว่าอะไรก็ใส่แทน My ไปเลยครับ)
2. ทำการ Extend Output ของ CodeIgniter และใส่โค้ด ดังนี้ครับ ปล. หากในบทความก่อน ใครสร้างไว้แล้วก็ทับได้เลยครับ เพราะผมพ่วง clear_cache() มาในโค้ดนี้แล้ว
if ( ! defined('BASEPATH')) exit('No direct script access allowed');
<br> /********<br> / TOSDN - URI from CI URI Library<br> / Update 12/12/2551 2:26<br> ********/</p> <p>/**<br> * Page Caching management class (extends from CI_Output)<br> *<br> *<br> * @package CodeIgniter<br> * @subpackage Libraries<br> * @hacked-by Chitpong Wuttanan<br> */<br> class My_Output extends CI_Output {
var $cache_name;
<br> var $cache_expiration;
/**<br> * Set Cache<br> *<br> * @access public<br> * @param integer<br> * @return void<br> *//**<br> * Update/serve a cached file<br> *<br> * @access public<br> * @return void<br> */<br> function _display_cache(&$CFG, &$URI)<br> {
<br> $cache_path = ($CFG->
item('cache_path') == '') ? BASEPATH.'cache/' : $CFG->
item('cache_path');
if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path))<br> {
<br> return FALSE;
<br>
}
$cache_topdomain_uri = md5($CFG->
item('base_url'));
<br> $cache_path_folder_uri = $cache_path."/".substr($cache_topdomain_uri, 0, 3)."/".$cache_topdomain_uri."/";
// Build the file path. The file name is an MD5 hash of the full URI<br> $uri = $CFG->
item('base_url').<br> $CFG->
item('index_page').<br> $URI->
uri_string;
$filepath = $cache_path_folder_uri.md5($uri);
if ( ! @file_exists($filepath))<br> {
<br> return FALSE;
<br>
}
if ( ! $fp = @fopen($filepath, FOPEN_READ))<br> {
<br> return FALSE;
<br>
}
flock($fp, LOCK_SH);
$cache = '';
<br> if (filesize($filepath) >
0)<br> {
<br> $cache = fread($fp, filesize($filepath));
<br>
}
flock($fp, LOCK_UN);
<br> fclose($fp);
// Strip out the embedded timestamp<br> if ( ! preg_match("/(\d+TS--->
)/", $cache, $match))<br> {
<br> return FALSE;
<br>
}
// Has the file expired? If so we'll delete it.<br> if (time() >
= trim(str_replace('TS--->
', '', $match['1'])))<br> {
<br> @unlink($filepath);
<br> log_message('debug', "Cache file has expired. File deleted");
<br> return FALSE;
<br>
}
// Display the cache<br> $this->
_display(str_replace($match['0'], '', $cache));
<br> log_message('debug', "Cache file is current. Sending it to browser.");
<br> return TRUE;
<br>
}
/**<br> * Write a Cache File<br> *<br> * @access public<br> * @return void<br> */<br> function _write_cache($output)<br> {
<br> $CI =& get_instance();
<br> $path = $CI->
config->
item('cache_path');
$cache_path = ($path == '') ? BASEPATH.'cache/' : $path;
if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path))<br> {
<br> return;
<br>
}
$cache_topdomain_uri = md5($CI->
config->
item('base_url'));
<br> $cache_path_folder = $cache_path."/".substr($cache_topdomain_uri, 0, 3);
<br> $cache_path_folder_uri = $cache_path_folder."/".$cache_topdomain_uri."/";
if ( ! is_dir($cache_path_folder) OR ! is_really_writable($cache_path_folder))<br> {
<br> @mkdir($cache_path_folder, 0755);
<br> @mkdir($cache_path_folder_uri, 0755);
<br>
}
if(empty($this->
cache_name)) {
<br> $uri = $CI->
config->
item('base_url').<br> $CI->
config->
item('index_page').<br> $CI->
uri->
uri_string();
<br> $cache_path_folder_uri .= md5($uri);
<br>
}
else {
<br> $cache_path_folder_uri .= md5($this->
cache_name);
<br>
}
if ( ! $fp = @fopen($cache_path_folder_uri, FOPEN_WRITE_CREATE_DESTRUCTIVE))<br> {
<br> log_message('error', "Unable to write cache file: ".$cache_path_folder_uri);
<br> return;
<br>
}
$expire = time() + ($this->
cache_expiration * 60);
if (flock($fp, LOCK_EX))<br> {
<br> fwrite($fp, $expire.'TS--->
'.$output);
<br> flock($fp, LOCK_UN);
<br>
}
<br> else<br> {
<br> log_message('error', "Unable to secure a file lock for file at: ".$cache_path_folder_uri);
<br> return;
<br>
}
<br> fclose($fp);
<br> @chmod($cache_path_folder_uri, DIR_WRITE_MODE);
log_message('debug', "Cache file written: ".$cache_path_folder_uri);
<br>
}
function clear_cache($set_uri = NULL, $all=FALSE){
<br> $CFG =& load_class('Config');
<br> $cache_topdomain_uri = md5($CFG->
item('base_url'));
<br> $cache_path_folder_uri = $CFG->
item('cache_path')."/".substr($cache_topdomain_uri, 0, 3)."/".$cache_topdomain_uri;
if($all == FALSE) {
<br> $filepath = ($CFG->
item('cache_path') == '') ? BASEPATH.'cache/' : $cache_path_folder_uri."/".md5($set_uri);
<br> if(file_exists($filepath))<br> {
<br> @unlink($filepath);
<br> log_message('debug', "Cache deleted for: ".md5($set_uri)." (".$set_uri.")");
<br>
}
else {
<br> return FALSE;
<br>
}
<br>
}
else {
<br> $filepath = ($CFG->
item('cache_path') == '') ? BASEPATH.'cache/' : $cache_path_folder_uri;
<br> if ($handle = opendir($filepath)) {
<br> while (($file = readdir($handle)) !== false) {
<br> if ($file != "." && $file != "..") {
<br> @unlink($filepath."/".$file);
<br>
}
<br>
}
<br> closedir($handle);
<br> log_message('debug', "Cache deleted for: ".md5($set_uri)." (".$set_uri.")");
<br>
}
else {
<br> return FALSE;
<br>
}
<br>
}
<br>
}
<br>
}
3. วิธีการนำไปใช้ (Usage) ใช้เหมือนเดิมทุกประการครับ :D
$this->output->cache(60); //1 Hour $this->load->view(’profile.php’);
ปิดท้ายด้วยการอธิบายโค้ดนิดหนึ่งนะครับ
โค้ดตัวนี้ผมได้ทำการ extend ไว้กับคลาส Output เดิม และทำการสร้าง Function ในชื่อเดิม เพื่อเรียกใช้แทน Function เดิม โดย
_display_cache() - จะทำหน้าที่เรียกการแสดงผล โดยตรวจสอบว่ามีการทำ Caching ไว้ไหม ถ้ามี และยังไม่หมดอายุ ตามเวลาที่กำหนด ก็จะเรียก Cache โดยตรวจสอบก่อนว่ามาจาก URI ไหน ให้เข้ารหัสด้วย md5และตัด 3 ตัวแรกออกมา เพื่อเข้าโฟลเดอร์ชั้นแรก จากนั้นเข้าโฟลเดอร์ชั้นสอง ด้วยรหัส md5 ตัวเต็ม จากนั้นจึงจะเจอไฟล์ที่มันจะตรวจสอบ แต่ถ้าไม่เจอ มันจะไปเรียก _write_cache() เพื่อสร้างไฟล์ใหม่ทันที รวมไปถึงสร้างโฟลเดอร์ให้อัตโนมัติด้วยนะครับ
_write_cache() - อันนี้ทำหน้าที่สร้างไฟล์ครับ โดยมีขั้นตอนเหมือน _display_cache() ครับ แต่ทำหน้าที่สร้างไฟล์ในโฟลเดอร์ที่เรากำหนดไว้
clear_cache() - เอาไว้ Clear Cache ครับ ตามที่เคยเขียนไว้ใน Page Caching with CodeIgniter and Cache Clearing code
ยังไงก็ลองเล่นดูนะครับ หากมีวิธีที่ดีกว่านี้ ก็เสนอได้ครับ :D
อ้างอิง : Sub-Domain or Many File in Page Caching with CodeIgniter
Introduction Page Caching with CodeIgniter
ใน CodeIgniter จะมีคำสั่งทำ Page Caching ไว้ให้อยู่แล้ว ซึ่งมันเอาไว้ทำการ Caching หน้าเว็บไซต์ที่ได้รับการประมวลผลแล้ว มห้กลายเป็น หน้า Statics ตามระยะเวลาที่เรากำหนดไว้ ดังนั้น เมื่อโหลดหน้านั้นขึ้นมาใหม่ภายในระยะเวลานั้น จะไม่ทำการประมวลผลอีก ซึ่งจะส่งผลให้เว็บโหลดเร็วขึ้นมากๆ (เช่น โหลดจาก 0.2 วินาที เหลือเพียง 0.0020 วินาที) รวมไปถึงประหยัดทรัพยากรณ์เซอเวอร์ได้อีกมากโข
ซึ่งวิธีการใช้ ก็ง่ายมากครับ แค่ใส่คำสั่ง $this->output->cache(5); ไว้ก่อนคำสั่งเรียก View จะหมายถึงการทำ Caching สำหรับหน้า (View) นั้นๆ เป็นเวลา 5 นาที เช่น
$this->output->cache(60); //1 Hour $this->load->view('profile.php');
ดังนั้น คำสั่งทั้งหมดในหน้า profile.php จะไม่ถูก ประมวลผลซ้ำภายใน 5 นาที หลังจากมีคนเข้าครั้งแรก เพราะระบบจะไปเรียกหน้า Cache มาแสดงแทน ซึ่ง เป็นหน้านิ่งๆ (Static page) เหมือน html ธรรมดาๆ
พอเข้าใจหลักการ Page Caching ของ CodeIgniter แล้วนะครับ คราวนี้ มาพูดถึงข้อจำกัดของมันบ้าง
Restriction of Page Caching with CodeIgniter
ข้อจำกัดของ Page Caching ด้วย CodeIgniter เท่าที่คนส่วนใหญ่ รวมถึงผมพบก็คือ มันง่ายเกินไป! เพราะมันทำงานอัตโนมัตินั่นเอง :D
เพราะ วิธีการทำงานของ Page Caching ใน CodeIgniter มันจะทำการ เก็บ output ของคำสั่งประมวลผลทั้งหมด ไปเก็บไว้ในชื่อไฟล์ ที่เข้ารหัสด้วย MD5 และชื่อไฟล์นั้น มันเอามาจาก URL หรือ URI ที่เรียกมันครับ เช่น
จะถูกเข้ารหัสและนำไปตั้งชื่อไฟล์ คือ
53db6ab6193a34a4ebac45ebd7592da6
ดังนั้น เมื่อมีการเข้าหน้านี้ซ้ำอีกครั้ง มันจะดึงค่า URL หรือ URI ที่ได้ มาเข้ารหัส MD5 และตรวจสอบว่า Cache หมดอายุ (Cache Expire) หรือยัง ถ้าหมดแล้ว จะทำการลบไฟล์เดิม > ประมวลผลข้อมูลใหม่ > แล้วก็เก็บค่าข้อมูลใหม่ ในชื่อไฟล์เดิม อีกครั้ง วนอย่างนี้ไปเรื่อยๆ
Problem of Page Caching with CodeIgniter
อันนี้เป็นปัญหาที่ผมเจอ รวมถึงใครหลายๆ คนที่ใช้ CodeIgniter นั่นก็คือ ต้องการลบหน้าที่ Cache ออก โดยไม่ต้องรอให้หมดเวลา แต่เราสามารถสั่งการได้เอง
การทำ Page Caching หรือ Database Caching ของผม ปกติจะทำการเก็บใหม่ ทุกครั้งที่มีการเปลี่ยนแปลงข้อมูล แต่ถ้าไม่มีการเปลี่ยนแปลงข้อมูล ก็ให้อยู่นิ่งๆ แบบนั้นไปเรื่อยๆ หรือจนกว่าจะหมดเวลาของมันไปเอง
ดังนั้น ในระบบจัดการข้อมูลของผม จำต้องมีการลบ Cache (Clear Cache) ทุกครั้งที่มีการแก้ไข
แต่ CodeIgniter ทำไม่ได้ครับ :D
Solution of Page Caching with CodeIgniter
วิธีแก้ไขปัญหาข้างต้น หลายคนไปใช้ Smarty ซึ่งเป็นระบบการจัดการ Template (Template Engine) ที่ดีมากตัวหนึ่ง หรือไปใช้ตัวอื่นๆ นำมา Integrate รวมกับ Codeigniter แทนการใช้งานระบบ Template เดิมของมัน (ที่ผมเขียนมาทั้งหมดคือระบบ Template เดิมที่มันมีมาให้อยู่แล้วนะครับ อย่าเพิ่งสับสนๆ)
แต่ที่ผมลองใช้งาน มันจัดการ Template ได้ดีสมน้ำสมเนื้อมันเอง แต่ผมรู้สึกว่ามันใหญ่เทอะทะ เพราะระบบมันฉลาดเกินไป! กล่าวคือ ผมต้องการ Page Caching ทั้งหน้าแค่นั้น ไม่ได้ต้องการแบ่งส่วนการ Caching เช่น ส่วน Header ของเว็บทำการ Cache ไว้ แต่ Body ไม่ต้องทำ อะไรทำนองนั้น
เขียนมาถึงตรงนี้ ใครเคลิ้มตาม เจอปัญหาแบบผม และต้องการใช้งานแบบผม มาลุยแก้ปัญหากันดีกว่าครับ :D
How to Cache Clearing with CodeIgniter?
1. สร้างไฟล์ Library ใน Application มาหนึ่งตัวครับ เช่น
/system/application/libraries/My_Output.php (ใครตั้ง Suffix ว่าอะไรก็ใส่แทน My ไปเลยครับ)
2. ทำการ Extend Output ของ CodeIgniter และใส่โค้ด ดังนี้ครับ
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/** * Clear Cache * * Responsible for sending final output to browser * * @package CodeIgniter * @subpackage Libraries * @added-by Chitpong Wuttanan */ class My_Output extends CI_Output {
function clear_cache($set_uri = NULL){ $CFG =& load_class('Config'); $filepath = ($CFG->item('cache_path') == '') ? BASEPATH.'cache/' : $CFG->item('cache_path').md5($set_uri);
if(file_exists($filepath)) { @unlink($filepath); log_message('debug', "Cache deleted for: ".$set_uri); } else { return FALSE; } } }
?>
3. วิธีการนำไปใช้ (Usage)
3.1 ใช้แบบเดิม โดยไม่ระบุ URI ซึ่งการทำงานจะเหมือนเดิมทุกอย่าง
$this->output->cache(60); //1 Hour $this->load->view('profile.php');
4. วิธีการลบ Cache (Clear Cache)
$this->output->clear_cache('http://www.web.com/profile');
มา อธิบายโค้ดกันนิดหนึ่งครับ
โค้ดตัวนี้ผมได้ทำการ extend ไว้กับคลาส Output เดิม เพื่อสะดวกต้อการเรียกใช้ครับ โดยทำการสร้าง Function ชื่อ clear_cache() เข้าไป
ในฟังก์ชั่นนี้ จะ เรียก URI ที่เราระบุ ทำการเข้ารหัสด้วย md5 และ ลบออก ง่ายไหมครับ ไม่ต้องไป Integrate ระบบ Template Engine ให้ยุ่งยากเลย
Reference Page Caching with CodeIgniter and Cache Clearing code
คนส่วนใหญ่เวลาเริ่มต้นทำ CodeIgniter สิ่งหนึ่งที่ภายใน "คู่มือการใช้งาน" ได้บอกไว้อาจจะยาวและต้องใช้ประสบการณ์ในการทำความเข้าใจซักเล็กน้อยซึ่งผมจะอธิบายเป็นอันๆไปเลยนะครับ โดยปกติแล้ว CI จะมี


โดย CodeIgniter นั้นสามารถใช้ได้หลาย Application ในอันเดียว (ซึ่งผมไม่ค่อยนิยมทำแบบนั้น) ดังนั้นภายในโฟลเดอร์ system ซึ่งสามารถเปลี่ยนชื่อได้ นั้นจะประกอบด้วยพวก libraries , helpers , language , plugins ซึ่งเหมือนกับตัวที่อยู่ในโฟลเดอร์ application มีความแตกต่างเพียงเรื่องเดียวก็คือ ถ้าอยู่ในโฟลเดอร์ system จะสามารถใช้ได้มากกว่า 1 applications แต่ถ้าอยู่ใน application นั้นๆก็จะใช้ได้เฉพาะ application แล้วความหมายของแต่ละอันละคืออะไร
โฟลเดอร์ถัดมาที่ผมจะอธิบายก็คือ cache , fonts , logs ซึ่งรายละเอียดคือ
ส่วน codeigniter เป็นโฟลเดอร์ในส่วนของ core ของ codeigniter ซึ่งไม่ควรไปแตะ ถ้าไม่ได้อยากรู้วิธีเขียนกลไก แล้วต่อมาคือ scaffolding ซึ่งในส่วนนี้เป็น theme และสิ่งที่เกี่ยวข้องกับ function scaffold ซึ่งใน CodeIgniter ตรงส่วนนี้ ไม่ดีพอที่จะใช้ หมดแล้วในส่วนของ system คราวนี้เหลือในส่วน application ซึ่งผมจะเริ่มจากพื้นฐานก่อนเลยคือ
ซึ่งหลักการ Model-View-Controller (MVC) นั้นมีคำอธิบายอยู่ลองศึกษาเพิ่มเติมจาก บทความนี้ครับ เสร็จแล้วเราจะยังเหลือโฟลเดอร์เหลือดังนี้ errors , configs , hooks ซึ่งผมจะอธิบายทีละอันดังนี้ครับ
หมดแล้วครับทุกโฟลเดอร์ วันหลังจะมาเขียนเรื่อง CodeIgniter เพิ่มขึ้นเรื่อยๆนะครับ
ส่วนตัวผมเพิ่งใช้งาน Framework เป็นครั้งแรก ที่ผ่านมาก็จะทำการเขียนโค้ดแบบเพียวๆ หรือที่เรียกกันว่า Hard Code นั่นเอง
ดังนั้นเมื่อผมเริ่มรู้จัก PHP Framework ทำให้ผมเลือกที่จะเล่น CI ด้วยเหตุผล