วันนี้ มาออกแบบ Web Service ด้วย PHP กันเถอะ (CodeIgniter Version)

โอ้โห ไม่ได้อัพเดทบล็อกนี้ (มีบล็อกเดียว) มานานมากๆ เผลอแป๊บเดียว 4 เดือนเข้าไปแล้ว ก็มันขี้เกียจนี่หว่า ไว้จะทำ Screen Cast แทนไม่รู้จะลำบากขึ้นมั้ย 555
เอาล่ะ ไหนๆ ก็อัพ แล้ว เอาเรื่องที่หาอ่าน บทความภาษาไทย ไม่ค่อยได้ดีกว่า เรื่องของ "Web Service" ครับ ปกติส่วนมากจะมีแต่บทความวิธีใช้ แต่ว่า วันนี้ผมจะมาเขียน บทความ "วิธีทำ" กันดีกว่า
สมัยนี้ หนียังไงก็หนีไม่พ้น กับการต้องเข้าไปยุ่งกับ API ของเว็บบิ๊กๆ ทั้งหลาย ซึ่งแต่ละเว็บก็มีการใช้ Protocol ต่างกันไป ถ้ายิ่ง บิ๊กมาก ก็จะมี Protocol ให้ใช้แทบจะครบ
มีทั้ง RESTful, XML-RPC, JSON-RPC, SOAP, etc. ซึ่งลงท้ายทุกตัวทำงานเหมือนกันหมด คือใช้ติดต่อ ระหว่าง โปรแกรม ซึ่งตอนนี้ API ของเว็บทุกเว็บ อย่างน้อยจะต้องมี 1 protocol คือ REST เพราะว่ามันง่ายกับ Developer ในการติดต่อ แล้วก็เขียนเชื่อมไป จะว่า XML-RPC, JSON-RPC ก็ต้องถือว่า เป็น REST ด้วยเหมือนกันแหละ เพียงแต่ Response Format ต่างออกไป
เอาล่ะ ทฤษฎีอาจจะไม่ค่อยแน่น แต่ปฎิบัติ ผมพอไหว 555
วันนี้เราจะมาออกแบบ ชุด API ของ REST กัน โดยที่โจทย์ของเรามีดังนี้
1. API จะต้อง Authen จากโปรแกรมเชื่อมใช้งานได้
2. Structure ต้องเป็นระเบียบสามารถเพิ่ม ชุด function ได้ง่าย
3. Response ต้องออกได้ทั้ง XML และ JSON
4. ต้องมี Failed Message ในกรณีที่ API ไม่สามารถทำงานได้สมบูรณ์
อุปกรณ์เครื่องครัวที่ต้องเตรียมมา
1. CodeIgniter ตัวนี้ใช้เป็น Base Render MVC เฉยๆ จะมีไม่มีก็ไม่ว่ากัน เอา Structure ตามถนัด
2. Zend_Rest_Server ตัวนี้ก็เหมือน เนื้อสัน ไม่มีก็ทำ เสต๊ก ไม่ได้
3. Zend_Json ตัวนี้เอาไปแปลง Response Format กลับไป กลับมาระหว่า XML -> JSON
เอาล่ะ มาเริ่มกันเลย....
ก่อนอื่นเราต้องมี File Bootstrap ของ API คือตัวกลางนั่นแหละ เวลาที่ ใครเรียกจะต้องมาที่ไฟล์นี้ก่อนซึ่งตัวนี้ผมใช้ Controller นั้น CI มาทำ
โดยในไฟล์นี้ผมตั้งชื่อ Controller ว่า Rest.php
<?php
class Rest extends CI_Controller {
public static $server;
private $_methods = array('blogs');
public function __construct()
{
parent::__construct();
// load zend reset server class
require_once('Zend/Rest/Server.php');
self::$server = new Zend_Rest_Server();
}
public function _remap($method, $attrs)
{
// check existing api methods
if (!in_array($method, $this->_methods)) {
die('API is not exists');
}
// if having specific method
if (method_exists($this, $method)) {
call_user_func_array(array($this, $method), $attrs);
}
else {
$call = "service_{$method}";
$this->load->model('services/'.$call);
self::$server->setClass($call);
}
// keep response
self::$server->returnResponse(true);
$xmlstr = self::$server->handle();
// response format
$format = $this->input->get_post('format');
if (strcasecmp($format, 'json') == 0)
{
// format output to valid structure
$dom = new DOMDocument('1.0');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXML($xmlstr);
// now we have valid format
$xmlstr = $dom->saveXML();
// zend not provide attribute, so we change to the code below
$this->load->loadClass('Zend_Json');
$response = Zend_Json::fromXml($xmlstr, true);
header('Content-Type:application/javascript;charset=' . config_item('charset'));
echo Zend_Json::prettyPrint($response); exit(0);
}
// return XML format
header('Content-Type:text/xml; charset='.config_item('charset'));
echo $xmlstr;
}
}
?>
ไฟล์นี้ก็มีประมาณนี้ครับ โดยแต่ละ method มีการทำงาน คร่าวๆ ดังนี้
method __construct
ส่วนนี้จะเป็นส่วนของการ include Zend_Rest_Server เข้ามาใช้งาน กับโหลด helper บางตัวเข้ามา (ผมจะขยายงาน helper ตัวนี้ทีหลัง)
method _remap
ส่วนนี้เป็น คล้ายๆ magic method ใน CI คือ เอาง่ายๆ มันเหมือน __call ใน class ปกติ ที่เวลาเราเรียก method อะไรมันจะเข้ามาที่นี่ก่อน นั่นแหละ
ส่วนการทำงานของมันก็คือ เป็นตัวรับ method ที่ user เข้ามาใช้งาน จุดสำคัญช่วงแรกอยู่ที่โคดตรงช่วงนี้
$call = "service_{$method}";
$this->load->model('services/'.$call);
self::$server->setClass($call);
จะเป็นตัวรับ method ว่า user call อะไรมาเช่น blogs, albums, friends
ก็จะไปเรียก model ที่อยู่ตาม path แบบนี้
/application/models/services/service_blogs.php
จากนั้นก็เอา methods ใน service_blogs ว่ามองเป็น method ของชุด API blogs
ส่วนท้ายๆ มันคือกันทำ format ระหว่าง xml, json ค่อยๆ ไล่โคดเอาแล้วกันนะครับ
ทีนี้มาดูในส่วนของ service_blogs.php กันว่า เรามีอะไรบ้าง
<?php
class service_blogs extends CI_Model {
public function __construct()
{
parent::__construct();
}
public function GetItems($page=1, $limit=20, $sort="created_at-desc", $criteria=array())
{
// implement
return array();
}
public function GetItem($id)
{
// implement
return array();
}
}
?>
เราจะมี methods ที่รอการ implement เรียงอยู่ครับ ซึ่งถ้าเราทำ สองไฟล์นี้จบแล้ว เราจะสามารถเรียกแบบนี้ได้
http://xxx.com/api/rest/blogs?method=GetItem&page=1&limit=20
โดยถ้าเราแยกชิ้นของ URL ออกมาจะมองเป็น โปรแกรมได้ดังนี้
1. rest = controller (CI)
2. blogs = model (models/services/service_blogs.php)
3. method:GetItems = method นึง ใน class ของ service_blogs
4. &page=1&limit=20 = arguments ของ method นั้นๆ
ที่เหลือ เราก็แค่ implement ลงไป Zend_Rest_Server จะจัดการทำ ให้ออกมาเป็น XML เอง
ทีนี้เราลองมา Implement ดูสักนิด ที่ method GetItem แล้วกัน สมมุตผมโคดลงไปแบบนี้
public function GetItem($id)
{
return array(
'response' => array(
'id' => $id,
'message' => "You are calling blogs:GetItem with parameter id = ".$id
)
);
}
แล้วผมมาลองเรียกแบบนี้
http://xxx.com/api/rest/blogs?method=GetItem
สิ่งที่ผมจะได้กลับมาก็คือ

เพราะว่าอะไรครับ ?
เพราะว่า id ผมเป็น field บังคับ Zend_Rest_Server จะมาเป็นตัวจัดการตรงนี้เอง แต่ถ้าผมเขียนโคดใหม่เป็น
public function GetItem($id=123)
{
return array(
'response' => array(
'id' => $id,
'message' => "You are calling blogs:GetItem with parameter id = ".$id
)
);
}
สังเกตุตรง id=123 นะครับ
ผลลัพธ์ที่ได้ ก็จะผิดกัน จะเหมือนกับผมเรียกไปด้วย URL
http://xxx.com/api/rest/blogs?method=GetItem&id=123 นั่นแหละครับ ตามภาพด้านล่าง

เอาล่ะ ทีนี้เราลองมาทำการ validate ชุดของ api แบบง่ายๆกันดูนะครับ ผมจะเพิ่มโคดอีกหน่อย ดังนี้
public function GetItem($id=123)
{
$username = $this->input->get_post('username');
$password = $this->input->get_post('password');
if (strcmp($username, 'Teepluss') != 0 || strcmp($password, '123') != 0) {
throw new Exception('Sorry, User is exists our database.');
}
return array(
'response' => array(
'id' => $id,
'message' => "You are calling blogs:GetItem with parameter id = ".$id
)
);
}
จะเห็นได้ว่า ผมมีการร้องขอ ให้กรอก username + password เข้ามาแล้ว ที่ผมไม่เอาใส่เป็น parameters ของ method เพราะจะทำให้ดูว่ามันจะเรียกแบบนี้เลยก็ได้ ส่วนค่าตรง parameters ของ method จะแทนได้ทั้ง get และ post เลยเช่นเดียวกัน แตกต่างกันตรงที่ parameter Zend Rest มีการจัดการ validate เรื่อง missing parameter ให้ด้วย เท่านั้นเอง
ทีนี้ผมจะลองเรียกไปใหม่ แบบนี้
http://xxx.com/api/rest/blogs?method=GetItem&id=123
ข้อความตอบกลับมาของผมก็คือ

นั่นก็เพราะผมยังไม่ได้ใส่ username + password ไปนั่นเอง เอาใหม่ทีนี้ใส่ให้ถูกๆ เป็น
http://xxx.com/api/rest/blogs?method=GetItem&id=123&username=Teepluss&password=123
ข้อความตอบกลับของผมก็จะเป็น

ถูกต้องตามปกติ และนอกจากนั้นแล้ว จากโคด ที่เราทำการเขียนไว้ต้องแต่ controller rest.php ยังสามารถทำให้เราจัดการกับ format ได้อีกด้วย เช่นผมพิมพ์ไปว่า
http://xxx.com/api/rest/blogs?method=GetItem&id=123&username=Teepluss&password=123&format=json
ค่าที่ตอบกลับมาผมจะได้มาใน format ของ json ดังภาพด้านล่าง

ซึ่ง Format JSON นี่ล่ะ เหมาะนักในการไปใช้ใน โปรแกรมจาก อุปกรณ์ต่างๆ เช่น iPhone, Android เป็นต้น
ไม่ยากเกินไปใช่มั้ยครับ สำหรับ 4 เดือนที่หายไป วันนี้มีของมาฝากเท่านี้แหละ เห็นมั้ยล่ะ มันต้องพิมพ์เยอะแค่ไหน ในการอัพ บล็อกแต่ละที นี่ล่ะที่ขี้เกียจ T___T
เจอกันโอกาสหน้านะครับ
Tee++;

4 comments