การทำ Routing ใน PHP ด้วย AltoRouter

โดยปกติแล้ว 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 ค่าคือ:

  1. $method คือ Request method ที่ระบุด้านบน
  2. $path คือ url path ที่ต้องการ
  3. $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


Posted

in

by

Comments

One response to “การทำ Routing ใน PHP ด้วย AltoRouter”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.