โดยปกติแล้ว PHP จะทำงานร่วมกับเว็บเซิร์ฟเวอร์อย่าง Apache หรือ Nginx ที่เราสามารถเรียกใช้ไฟล์ PHP ผ่าน URL โดยตรงได้เลย เช่นเราอยากเรียกไฟล์ post.php
ขึ้นมาทำงาน ก็สามารถเรียกผ่าน URL https://domain.com/post.php
ตรงๆ ได้เลย
แต่พอเมื่อเราโดดไปเขียนภาษาอื่น ไม่ว่าจะ ASP.NET MVC หรือของยอดนิยมในตอนนี้อย่าง Node.js เราจะเจอวิธีการกำหนด URL อีกแบบ ที่เราต้องไปกำหนดแพทเทิร์นของ URL ระบุว่า URL มาในลักษณะนี้จะเรียกหน้าไหนมาแสดง เราเรียกวิธีกำหนด URL แบบนี้ว่าการทำ routing
เรามักจะเห็นการทำ routing แบบนี้เมื่อเราใช้เฟรมเวิร์กสักตัวในการสร้างเว็บไซต์ แต่จริงๆ แล้วมันก็มีเป็นไลบรารี่สำเร็จรูปให้เราติดตั้งใช้งานได้เช่นกันในกรณีที่เราอยากทำโปรเจ็กท์เล็กๆ แบบไม่ใช้งานเฟรมเวิร์ก
Request Methods
ก่อนที่เราจะไปดูเรื่องการทำ routing นั้น เรามาดูเรื่อง HTTP Request Methods กันก่อน ซึ่งโดยปกติจะมีอยู่ 9 เมท็อด คือ GET
, HEAD
, POST
, PUT
, DELETE
, CONNECT
, OPTIONS
, TRACE
, และ PATCH
แต่ในการทำเว็บ หลักๆ เราจะได้ยุ่มย่ามอยู่แค่ 2 เมท็อดเท่านั้น คือ GET
และ POST
(ถ้าใครทำ API อาจจะได้เล่นกับเมท็อดอื่นด้วย) นิยามสั้นๆ ของสองเมท็อดนี้ในการทำเว็บไซต์ก็คือ
- GET ใช้เรียกดูข้อมูลจากเซิร์ฟเวอร์ เช่นเรียกดูหน้าเว็บปกติ
- POST ใช้ส่งข้อมูลกลับไปที่เซิร์ฟเวอร์ เช่นส่งข้อมูลไปกับฟอร์มเพื่อบันทึก
สำหรับเมท็อดอื่นๆ สามารถดูรายละเอียดได้ที่เว็บไซต์ MDN
ในการกำหนด Routing นั้น ถ้าเราสามารถระบุได้ว่า request ที่เข้ามายัง path นี้เป็นคนละประเภท ก็ให้ประมวลผลคนละอย่างกัน เช่นถ้าเป็น GET /notes
ก็ให้แสดงรายการโน้ตทั้งหมด แต่ถ้ามาเป็น POST /notes
ก็ให้รับข้อมูลมาสร้างเป็นโน้ตชิ้นใหม่
AltoRouter
หลายๆ ครั้งเรามักจะได้คำแนะนำให้เขียนไฟล์ .htaccess
เพื่อซ่อนนามสกุลไฟล์ .php
และทำเป็น path-based routing หลอกๆ (แต่จริงๆ ยังเป็นการเปิดไฟล์ตาม URL เหมือนเดิม) หรือบางคนก็เขียน RewriteRule
เพื่อแก้ request เอาดื้อๆ เลย
แม้วิธีนี้จะพอใช้งานได้ แต่ในความเป็นจริงมันมักจะสร้างปัญหาให้ในภายหลัง และมักจะผูกอยู่กับเซิร์ฟเวอร์แบบเดียว (ในที่นี้คือ Apache ถ้าย้ายไปใช้ Nginx หรือ IIS จะต้องเขียน rules ใหม่ทั้งหมด)
วิธีที่แนะนำจริงๆ นั่นคือหาไลบรารี่ router มาใช้สักตัวหนึ่ง โดยเรายังต้องเขียน .htaccess
เพิ่มเติมเล็กน้อย เพื่อส่ง request ทั้งหมดกลับมาที่ index.php
จากนั้นจึงค่อยใช้ router มาวิเคราะห์ request path และดึงหน้าที่ถูกต้องมาแสดงอีกทีหนึ่ง
ไลบรารี่ที่เราจะลองใช้กันในวันนี้คือ AltoRouter เราสามารถติดตั้งไลบรารี่นี้ได้ง่ายๆ ผ่าน Composer
composer require altorouter/altorouter
จากนั้นให้ประกาศ use AltoRouter as Router;
และเรียกไฟล์ autoload เข้ามาในหน้า index.php
<?php use AltoRouter as Router; require_once 'vendor/autoload.php';
เพียงเท่านี้ router ของเราก็พร้อมใช้งานแล้ว
การสั่ง
use ... as ...
จะเป็นการเปลี่ยนชื่อคลาสที่เราจะเรียก ในกรณีนี้คือเราเปลี่ยนชื่อคลาสAltoRouter
ให้เหลือแค่Router
รีไดเรค request ทั้งหมดให้มาที่ index.php
สิ่งสำคัญของการทำ routing นั้นคือการโยน request ทั้งหมดให้มาลงที่ index.php
เพื่อให้ router สามารถเอาพาทมาวิเคราะห์ได้ ซึ่งจริงๆ ในส่วนนี้ก็จะเหมือนกับการเปิด pretty url ของ WordPress นั่นแหละ โดยเราสามารถระบุคอนฟิกของเว็บเซิร์ฟเวอร์แต่ละตัวได้ดังนี้
Apache .htaccess
RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-f RewriteRule . index.php [L]
Nginx.conf
try_files $uri /index.php;
Microsoft IIS web.config
<!-- Rewrites requires Microsoft URL Rewrite Module for IIS Download: https://www.microsoft.com/en-us/download/details.aspx?id=47337 Debug Help: https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/using-failed-request-tracing-to-trace-rewrite-rules --> <configuration> <system.webServer> <rewrite> <rules> <rule name="Imported Rule 1" stopProcessing="true"> <match url="^(.*)/$" ignoreCase="false" /> <conditions> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" /> </conditions> <action type="Redirect" redirectType="Permanent" url="/{R:1}" /> </rule> <rule name="Imported Rule 2" stopProcessing="true"> <match url="^" ignoreCase="false" /> <conditions> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" /> </conditions> <action type="Rewrite" url="index.php" /> </rule> </rules> </rewrite> </system.webServer> </configuration>
PHP built-in server
ส่วนใครที่ทดสอบโปรเจ็กท์ด้วย built-in server ของ PHP ก็สั่งคำสั่งนี้ได้เลย
php -S localhost:8000 index.php
กำหนดพาทของ URL
สมมุติว่าเราจะทำเว็บสำหรับเอาไว้จดโน้ตแบบง่ายๆ เราอาจจะมีหน้าต่างๆ ดังนี้
index.php
หน้ารวมโน้ตทั้งหมดnew-note.php
หน้าเขียนโน้ตใหม่note.php
หน้าแสดงโน้ต (เวลาเรียกจะเป็นnote.php?id=24601
เพื่อดึงโน้ตไอดี 24601 ออกมาแสดง)
สำหรับคนที่เขียน PHP มาสักพัก น่าจะพอนึกภาพออกว่าในแต่ละหน้านั้นจะมีโค้ดลักษณะประมาณไหน และในบทความนี้เราจะมาทำ routing ให้มันใหม่เป็นแบบนี้
/
หน้ารวมโน้ตทั้งหมด/new-note
หน้าเขียนโน้ตใหม่ (ถ้ามาเป็นGET
จะเป็นฟอร์มเขียนโน้ต ถ้ามาเป็นPOST
จะเป็นการเซฟโน้ต)/note/<id>
หน้าแสดงโน้ต (เวลาเรียกจะเป็น/note/24601/
เพื่อดึงโน้ตไอดี 24601 ออกมาแสดง)
ในการกำหนดแพทเทิร์นจะมีขั้นตอนง่ายๆ คือประกาศสร้างออพเจ็กท์ Router
ขึ้นมาก่อน จากนั้นใช้เมท็อด map()
เพื่อแมพพาทต่างๆ เข้ากับ callback ที่ต้องการ โดยเมท็อด map()
จะรับพารามิเตอร์ 3 ค่าคือ:
$method
คือ Request method ที่ระบุด้านบน$path
คือ url path ที่ต้องการ$callback
เป็น callback function ที่เราจะเรียกใช้เมื่อมีการเรียกใช้$path
นี้
<?php use AltoRouter as Router; require __DIR__.'/vendor/autoload.php'; $router = new Router(); $router->map( "GET", "/", function() { echo "หน้าแรก"; } ); $router->map( "GET", "/new-note", function() { echo "หน้าเขียนโน้ต"; } ); $router->map( "POST", "/new-note", function() { echo "หน้าบันทึกโน้ต" } ); $router->map( "GET", "/note/[i:id]", function( $id ) { echo "หน้าดูโน้ต: " . $id; } );
ใน AltoRouter นั้นจะไม่มีการประมวลผล route ให้อัตโนมัติ เราจะต้องสั่ง match()
แล้วเรียกใช้ฟังก์ชัน callback เอาเอง (วิธีนี้ทำให้เราสามารถเอา AltoRouter ไปเสียบกับโค้ดที่มีอยู่แล้วได้ง่ายขึ้น รวมถึงสามารถเอาไปเสียบกับเว็บเวิร์ดเพรสเพื่อกำหนด route เองได้ด้วย)
$match = $router->match(); if( is_array($match) && is_callable( $match['target'] ) ) { call_user_func_array( $match['target'], $match['params'] ); } else { echo "ไม่พบหน้าที่ต้องการ"; }
จากโค้ดด้านบนจะเห็นว่าเราแมพพาท new-note
สองครั้ง ครั้งหนึ่งสำหรับ GET
และอีกครั้งสำหรับ POST
ซึ่งก็ถือถ้าใช้เบราเซอร์เปิดหน้าเว็บมาที่ /new-note
ก็จะเจอกับข้อความว่า “หน้าเขียนโน้ต” แต่ถ้าเป็นฟอร์มที่ใช้ method="post"
ส่งมาที่หน้านี้ ก็จะเจอกับข้อความว่า “หน้าบันทึกโน้ต”
การระบุพาทด้วยแพทเทิร์น
จากตัวอย่างด้านบน จะเห็นว่านอกจากเราจะระบุพาทตรงๆ ได้แล้ว เรายังสามารถระบุพาทเป็นแพทเทิร์นได้ด้วย เช่นการกำหนดพาทเป็น /note/[i:id]
การระบุแพทเทิร์นใน AltoRouter จะแบ่งออกเป็นแพทเทิร์นที่จะถูกแกะออกมาเป็นค่าที่ใช้ได้อีกทีหนึ่ง (แพทเทิร์นที่มีพารามิเตอร์) จะมีลักษณะคือระบุ :param_name
เอาไว้ภายใน []
ด้วย ซึ่งฟังก์ชัง callback ของแพทเทิร์นที่มีพารามิเตอร์นั้นจะต้องรับค่าพารามิเตอร์นั้นๆ เข้ามาด้วย กับอีกแบบคือแค่สำหรับตรวงว่าแพทเทิร์นตรงเท่านั้น (แพทเทิร์นที่ไม่มีพารามิเตอร์ หรือไม่มีระบุ :param_name
)
ในตัวอย่างจะเห็นว่าพาท /note
หรือหน้าเปิดอ่านโน้ตนั้น จะเห็นว่ามีการระบุแพทเทิร์นเอาไว้เป็น /note/[i:id]
และในฟังก์ชัน callback ก็มีรับค่า $id
เข้ามา
ประเภทของแพทเทิร์นใน AltoRouter หลักๆ จะมีดังนี้
*
ใช้ดัก request ทั้งหมด อะไรก็ได้[i]
ดักค่าที่เป็นตัวเลขเท่านั้น[a]
ดักค่าที่เป็นตัวหนังสือและตัวเลข (alphanumeric)[h]
ดักค่าที่เป็นเลขฐาน 16 (ตัวเลข 0-9 และหนังสือ A-F)[*]
ดักค่าอะไรก็ได้ จนกว่าจะถึง/
ตัวต่อไป[**]
ดักค่าทั้งหมดจนสุด URL
แพทเทิร์นที่อยู่ใน []
เราก็สามารถเติม :param_name
เข้าไปเพื่อดักมาใช้เป็นพารามิเตอร์ได้ทันที เช่น
[i:id]
ดักตัวเลขมาใช้เป็น$id
[a:category]
ดักตัวเลขและตัวหนังสือมาใช้เป็น$category
[h:key]
ดักเลขฐาน 16 มาใช้เป็น$key
[*:slug]
ดักค่าอะไรก็ได้มาใช้เป็น$slug
[**:trailing]
ดักค่าทั้งหมดจากจุดนี้ไปจนถึงสุด URL มาใช้เป็น$trailing
ดังนั้นถ้าสมมุติว่าเราต้องการกำหนดแพทเทิร์นให้มีลักษณะเป็น /note/<เลขอะไรก็ได้ ไม่ถูกเอามาใช้>/<ชื่อหมวดหมู่>/<ไอดี>
เราก็จะสามารถเขียนได้แบบนี้
$router->map( "GET", "/note/[i]/[:category]/[i:id]", function( $category, $id ) { echo "Category: " . $category . "<br>"; echo "ID: " . $id; });
ตัวอย่าง
ผมลองทำตัวอย่างโปรเจ็กท์ง่ายๆ อันหนึ่งเอาไว้ โดยอิงจากท่าประจำที่หลายๆ คนมักทำกัน นั่นคือแยกหน้าเอาไว้แล้ว include
เข้ามาแสดงผล (แต่จริงๆ เราสามารถทำ Controller และเรียกใช้ template ได้ด้วย แต่นั่นเดี๋ยวไว้เป็นตัวอย่างในหัวข้อต่อไป)
https://github.com/WP63/php-sample-router
ในตัวอย่างนี้ให้ดูในหน้า public/index.php
ที่มีการกำหนด route ต่างๆ เอาไว้ และ include หน้าที่ต้องการเข้ามาจาก src/Pages
เพิ่มเติม: AltoRouter
Leave a Reply