Routing URL for PHP

ตอนนี้ในส่วนของ PHM (PHPHoffman Framework) ที่ต้องปรับ Architecture ใหม่แล้ว สิ่งที่ต้องปรับก่อนและทำมาได้ 7-8 วันคือส่วนของ Routing URL ที่ปรับเปลี่ยนจากการโยนทุกอย่างไปที่ mod_rewrite ของ Apache ซึ่งผมมองว่ามันดูไม่ค่อยดีเท่าไหร่ ประกอบกับถ้าจะทำ น่าจะทำที่ php มากกว่า อย่างน้อย ๆ ทำ cache ก็ยังง่ายกว่าอยู่ จึงนั่งดูว่าใน CMS อย่าง Drupal และ Blogware อย่าง WordPress เค้าทำยังไง ซึ่งนั่งไล่ดู code เค้าแล้วตัว mod_rewrite ที่เค้าใช้ก็มีประมาณ code ด้านล่างเท่านั้นที่เป็นส่วนหลัก ๆ

ไฟล์ .htaccess

  1. <ifmodule mod_rewrite.c>
  2.     RewriteEngine On
  3.     RewriteCond %{REQUEST_FILENAME} !-d
  4.     RewriteCond %{REQUEST_FILENAME} !-f
  5.     RewriteRule ^(.*)$ index.php?qs=$1 [QSA,L]
  6. </ifmodule>

จะเห็นว่าโยน String หลัง directory ทั้งหมดใส่ลงไปใน QueryString ชื่อว่า "qs" ทั้งหมด แล้วเอาไอ้ String ทั้งหมดไปตัดแต่งใน PHP เอาทีหลัง ซึ่งอย่างที่บอกไปว่า PHP มี cache ที่ดีอยู่แล้ว ผมเลยคิดว่าเราจะใช้มันแทน

สิ่งต่อมาที่ต้องคิดคือการ config ไฟล์ routing ที่ควรจะเป็นแบบไหนดี นั่งไล่ดูทั้ง RoR (Ruby On Rails), CakePHP และบทความตามเว็บต่าง ๆ มักจะใช้ Style ของ RoR ทั้งนั้นเพียงซึ่งผมเอามาเพียงแต่ pattern ของการกำหนด connect ที่ map URL กับ action ของระบบเท่านั้น ส่วนการ config ผมกลับโยนลงไปใน XML Style แทน ดังต่ออย่างต่อไปนี้

ไฟล์ routing.xml

  1. <routing><!-- High Priority -->  
  2.     <map pattern="login" action="user/login" />
  3.     <map pattern="signup" action="user/signup" />
  4.     <map pattern="signout" action="user/logout" />
  5.     <map pattern="userdetail/:value" action="user/detail/:value" />
  6.     <!-- Begin Default Routing URL -->
  7.     <map pattern=":controller/:action/:value" />
  8.     <map pattern=":controller/:action" />  
  9.     <map pattern=":page" />
  10.     <!-- End Default Routing URL -->  
  11. </routing><!-- Low Priority -->

จากตัวโครงสร้างการ map ตัว url นั้นจะแบ่งเป็นสองส่วนคือ pattern กับ action โดยตัว pattern เป็นตัวที่บ่งบอกถึงส่วนที่เข้ามาในตัวแปร qs ที่รับเข้ามา ส่วน action คือตัวที่ระบบจะทำการ recursion กลับมาอีกครั้งเพื่อทำ default routing ด้านล่างอีกที โดยตัว keyword จะมีอยู่ 4 ตัวคือ :controller, :action, :value และ :page ซึ่งทั้งสามตัวเป็นส่วนสำคัญในการควบคุมการเกิด action ของระบบจาก pattern ที่เพิ่มเติมมานอกเนื่อจาก 3 ตัวที่เป็น default ของทั้งหมด จริง ๆ ตอนนี้อาจจะพัฒนาเพิ่มโดยที่ developer สามารถเพิ่ม keyword ลงไปได้อย่างง่าย ๆ จากไฟล์ XML Style ตัวนี้ ตอนนี้กำลังหาส่วนที่ทำให้ง่ายที่สุดก่อน

ต่อมาคือการ parse ตัว XML เราใช้ตัว HTML/XML Parser Class ของ Dennis Pallett  (หารายละเอียดได้จาก entry เก่า ๆ ครับ) ซึ่งเป็นตัว HTML/XML Parser Class ที่อยู่ใน Core ของ PHM อยู่แล้ว

โดย Pattern มาตรฐานคือ /page, /controller/action หรือ /controller/action/value ซึ่งตอนนี้ตัวที่ทำผมทำเป็น Class เพื่อง่ายต่อการนำไปใช้ โดยตัว code ก็คือ

ไฟล์ routing.php

  1. < ?php
  2. /**
  3.  * Routing URL Class for PHP
  4.  *
  5.  * This is a helper class that is used to Routing URL.
  6.  *
  7.  * @author   Ford AntiTrust, annop@thaicyberpoint.com, http://www.thaicyberpoint.com
  8.  * @copyright Ford AntiTrust, 2007
  9.  * @package Routing URL
  10.  * @version 0.1a.2
  11.  */
  12.  require_once("HTMLXML_Parser.php");
  13.  
  14. class RoutingURL extends HTMLXML_Parser{
  15.    
  16.     private $qs;
  17.     private $array_xml;
  18.     private $match_data;
  19.     private $path_routingfile = "routing.xml";
  20.     private $keyword = array(
  21.                                         ':controller'   => '[a-zA-Z0-9][a-zA-Z0-9_\-\.]*',
  22.                                         ':action'        => '[a-zA-Z0-9][a-zA-Z0-9_\-\.]*',
  23.                                         ':value'         => '[a-zA-Z0-9][a-zA-Z0-9_\-\;\.=]*',
  24.                                         ':page'          => '[a-zA-Z0-9][a-zA-Z0-9_\-\;\.=]*'
  25.                                         );
  26.  
  27.     function __construct($qs, $routingpathfile = NULL) {    
  28.         if(empty($qs)) return;
  29.         if(!empty($routingpathfile)) $this->set_routingpathfile($routingpathfile);
  30.  
  31.         $this->qs = $this->filter_slash_at_last_char($qs);
  32.  
  33.         $this->array_xml = parent::parse(file_get_contents($this->get_routingpathfile()));
  34.  
  35.         foreach($this->array_xml[0]['children'] as $key=>$val) {
  36.  
  37.             $pattern = $this->filter_slash_at_last_char($val['attr']['PATTERN']);
  38.             $action = $this->filter_slash_at_last_char($val['attr']['ACTION']);
  39.             $raw_pattern = explode('/', $pattern);
  40.  
  41.             foreach($this->keyword as $keywordkey=>$keywordval)          
  42.                 $pattern = eregi_replace($keywordkey, $keywordval, $pattern);        
  43.            
  44.             // Test Output
  45.             // echo $this->filter_slash_at_last_char($val['attr']['PATTERN']), ' < - ',$pattern, ' <- ',$this->qs, '<br />';
  46.  
  47.             if($pattern == $this->qs && !empty($action)) {
  48.                     $this->__construct($action);
  49.                 break;
  50.             }
  51.             elseif(eregi('^'.$pattern.'$', $this->qs)) {
  52.                 $filter_pattern = explode('/', $pattern);
  53.                 $array_qs = explode('/', $this->qs);
  54.                 $index = 0;
  55.                 foreach($filter_pattern as $patternkey=>$patternval){
  56.                     $this->match_data[$raw_pattern[$index]] = $array_qs[$index];
  57.                     if(isset($this->keyword[$raw_pattern[$index]]))
  58.                         $action = eregi_replace($raw_pattern[$index], $array_qs[$index],$action);                    
  59.                     $index++;
  60.                 }
  61.                 if(!empty($action))
  62.                     $this->__construct($action);
  63.                 break;
  64.             }
  65.         }
  66.     }
  67.     function get_match_data() {
  68.         return $this->match_data;
  69.     }
  70.     function filter_slash_at_last_char($pattern) {
  71.         return (substr($pattern, -1) == "/"  ? substr($pattern, 0, -1) : $pattern);
  72.     }
  73.     function set_routingpathfile($path) {
  74.         $this->path_routingfile = $path;
  75.     }
  76.     function get_routingpathfile() {
  77.         return $this->path_routingfile;
  78.     }
  79. }
  80. $obj = new RoutingURL($_GET['qs']);
  81. print_r($obj->get_match_data());
  82. ?>

เมื่อทดสอบ จาก URL : http://127.0.0.1/routing/login http://127.0.0.1/routing/login ตัว method "get_match_data" จะส่ง Array ออกมาเป็น

  1. Array ( [:controller] => user [:action] => login )

เราก็เอาค่าใน Array ไปใช้ประโยชน์ในการ map ตัว controller เพื่อเรียก Class มาสร้างเป็น Object, map ตัว action เพื่อไปเรียก method ใน object และ value สำหรับ map parameter ลงในตัวแปรของ method นั้น ๆ ต่อไป

โดยรวมตอนนี้ทั้งหมด OK แล้ว ทดสอบมาได้ 2 วันผลเป็นที่น่าพอใจ สิ่งที่ควรระวังที่สุดในการทำ Routing URL คือ Infinite loop ในการเรียก action ซึ่งก็เหมือนกับของ RoR นั้นแหละ อันนี้คงต้องระวังกันหน่อยครับ จริง ๆ มันก็ลักษณะการ recursion ไปเรื่อยจนเจอ Terminate case นั้นแหละ ตอนนี้ก็ต้องปรับกันต่อไป และทำ Performance tuning ด้วย รวมถึง Test เปรียบเทียบกับแบบใช้ mod_rewrite ว่าอันไหนเร็วกว่า, ใช้ง่ายและปรับแต่งได้ง่ายกว่ากันด้วย ;)

[Download]

[Update, 15:28, 24/08/2007]
มีคนถามว่าทำไมต้อง XML Style ด้วย ? คำตอบก็เพราะว่าต่อไปจะเขียน Java App หรือ .NET App ตัวเล็ก ๆ เพื่อเข้าไปเขียนหรือจัดการมันแทนที่จะมานั่งเขียนใน XML เช่นมี Textbox 2 ตัวไว้ใส่ข้อมูล โดยช่องนึงไว้ใส่ pattern อีกอันไว้ใส่ action แล้วก็กด update หรือจัดการอื่น ๆ เสีย แค่นั้นเอง หรือถ้ามีซับซ้อนมากขึ้น เราก็ปรับ Tools ให้มันเข้าไปใช้งานได้ง่ายขึ้น ประมาณนั้น

[Update, 15:40, 24/08/2007]
เดี่ยวเรื่อง Document จะตามมาอีกทีครับ ตอนนี้ขอปรับแต่งและทดสอบให้มากกว่านี้อีกหน่อย แต่แบบนี้น่าจะทำให้ได้ idea มากขึ้่น และสิ่งหนึ่งที่ผมคิดไว้คือถ้า server เป็น iis ตัว url มันก็จะถูกปรับเพียงเล็กน้อยเท่านั้น ซึ่งแค่เขียน funciton เพิ่มสำหรับ detect ค่า config สัก 1 ตัวเพื่อบอกว่าเราใช้ mod_rewrite อยู่หรือไม่ ถ้าไม่ได้ใช้ก็ก็แค่เพิ่ม QueryString ลงไปใน path URL เท่านั้นเอง ซึ่งลดการทำงานของ Developer ในการต้องมานั่งกังวลกับการเข้ากันได้ของตัว Web App ที่เขียนขึ้นมา ซึ่งได้นำ Concept แบบเดียวกับ Drupal นั้นเอง

[Update, 16:55, 24/08/2007]
มีการแจ้งข้อผิดพลาดเข้ามาแล้วครับ ตอนนี้แก้ไขแล้วเรียบร้อย ;)

[Update, 23:00, 25/08/2007]
นอนไม่ค่อยหลับตื่นมาเจอ bug นิดหน่อยแก้เสียเลย -_-‘

1 thought on “Routing URL for PHP

Leave a Reply