กำหนด URL structure ได้ตามใจด้วย AltoRouter

อธิบายก่อนว่าเมื่อประมาณต้นปีที่ผ่านมา ผมได้ทำเว็บไซต์เว็บหนึ่งที่ภายในเว็บไซต์จะมี whitepaper ให้ดาวน์โหลด ซึ่งก่อนโหลดจะต้องมีการกรอกฟอร์มก่อน แล้วค่อย redirect ไปที่อีก URL หนึ่ง ซึ่งมันก็เป็น permalink เดิมแหละ แค่ต้องมีพารามิเตอร์เพิ่มเข้ามา

ปกติแล้วเราก็ใช้วิธีเพิ่ม query string เข้าไป เช่น URL เดิมเป็นแบบนี้

https://domain.test/whitepaper/paper-name/

เราก็เติม query string เข้าไปเป็นแบบนี้

https://domain.test/whitepaper/paper-name/?download=true

แล้วในเท็มเพลตเราก็เช็คค่าจาก $_GET['download'] เอาตามปกติ แต่ว่าการใส่ query string นั้นมันดูขัดหูขัดตาเหลือเกิน เราเลยจะแก้ไขให้มันเป็น pretty permalink แบบนี้

https://domain.test/whitepaper/paper-name/download/

ซึ่งเราสามารถทำอะไรแบบนี้ได้ด้วยการใช้ router library อย่าง AltoRouter ที่เราเคยพูดถึงไปก่อนหน้านี้

จริงๆ แล้วเวิร์ดเพรสมี Rewrite API ให้ใช้งานด้วยเช่นกัน แต่การใช้งานจะยุ่งยากกว่าเพราะเราต้องเขียน Regular Expression เพิ่มเข้าไปด้วย ดังนั้นการเอา router สักตัวมาเสียบเข้าไปเพื่อจัดการเรื่องนี้แทนนั้นจะสะดวกกว่ามาก

ติดตั้ง AltoRouter

อย่างที่เราเคยกล่าวไว้ในโพสต์ก่อนหน้านี้ว่า AltoRouter นั้นมีหน้าที่เพียงแค่อ่าน route เท่านั้น ไม่ได้มีการประมวลผล logic ใดๆ เพิ่มเติม และจะคืนค่า callback function กลับออกมาให้เราเรียกใช้เอง นั่นทำให้เราสามารถดัดแปลงเอา callback function เหล่านี้ให้คืนค่ากลับออกมาเป็น query vars ที่ต้องการได้

ขั้นแรกให้เราเปิด terminal ไปที่โฟลเดอร์ธีมของเราก่อน แล้วสั่งติดตั้งแพ็คเกจ altorouter/altorouter ด้วย composer

cd wp-content/themes/mytheme
composer require altorouter/altorouter

ถ้าธีมใครที่ไม่ได้ใช้ composer อยู่ก่อนแล้ว ก็ให้ไปเพิ่มคำสั่ง require ไฟล์ autoload.php ลงใน functions.php ด้วย โดยเปิดไฟล์ functions.php ของธีมขึ้นมา แล้ว require autoload ไปที่บรรทัดบนสุด

<?php
use AltoRouter as Router;

require_once 'vendor/autoload.php';

ฟิลเตอร์ request

อย่างที่ทราบกันว่าเวิร์ดเพรสจะเปิดให้เราเข้าไปแก้ไขข้อมูลต่างๆ ในระหว่างเว็บประมวลผลได้ผ่านทางฟิลเตอร์ และฟิลเตอร์ request นี้จะทำงานตอนที่เวิร์ดเพรสเอา URL มาประมวลผลและแกะออกมาเป็นตัวแปรที่จะใช้เอาใช้คิวรี่เนื้อหามาแสดงแล้ว

ถ้าเราลองฮุคเข้าไปที่ฟิลเตอร์นี้แล้ว var_dump() ค่าออกมาแสดง ก็จะเจอกับ query vars ของเวิร์ดเพรส

<?php
add_filter( 'request', function( $request ) {
  var_dump( $request );

  return $request;
} );

ซึ่งตัวแปรใน query vars นี้เราสามารถเข้าถึงได้จากที่ไหนก็ได้ผ่านฟังก์ชัน get_query_var( $key ) เพื่อดึงค่านั้นๆ มาใช้งาน

ดังนั้นแนวคิดของเราก็คือใช้ AltoRouter ตรวจสอบ URL ว่าตรงตามที่ต้องการหรือไม่ (เป็น /whitepaper/<post-name>/download/) ถ้าใช่ก็เพิ่มตัวแปรเข้าไปใน query var แล้วไปตรวจสอบเอาในหน้าเท็มเพลต

ตรวจ URL ด้วย AltoRouter

เรายังอยู่กันที่ไฟล์ functions.php หลังจากที่เราประกาศ use AltoRouter as Router และ require ไฟล์ autoload.php เข้ามาแล้ว ในฟิลเตอร์ request ให้เรา new Router ขึ้นมา แล้วตรวจสอบแพทเทิร์น URL แบบนี้

<?php
add_filter( 'request', function( $request ) {
  $router = new Router(); 
  
  // กำหนด route ที่ต้องการ
  $router->map( 'GET', '/whitepaper/[:post]/download/?', function( $post ) {
    return [
      'post_type' => 'whitepaper',
      'name' => $post,
      'is_download' => true,
    ];
  });

  // ตรวจสอบว่า URL ปัจจุบันตรงกับแพทเทิร์นใดๆ ที่เราตั้งไว้หรือไม่
  $match = $router->match();

  // ถ้าตรงให้คืนค่า query vars ชุดใหม่จากในฟังก์ชัน
  if ( is_array( $match ) && is_callable( $match['target'] ) ) {
    $request_arguments = call_user_func_array( $match['target'], $match['params'] );

    return $request_arguments;
  }

  // คืนค่า query vars ชุดเดิมในกรณีที่ route ไม่ตรงกับที่กำหนดไว้
  return $request;
} );

จากตัวอย่างนั้นจะเห็นว่าหากแพทเทิร์นของ URL ตรงตามเงื่อนไข เราจะ return ค่า query vars ชุดใหม่ออกมา ซึ่ง query vars ชุดใหม่นี้จะมีคีย์ is_download ติดมาด้วย

อย่างไรก็ตาม ในค่าเริ่มต้นนั้นเวิร์ดเพรสจะยังไม่รู้จัก query var ตัวใหม่ที่เราเพิ่งเพิ่มเข้าไป (คีย์ is_download นั่นแหละ) เราต้องทำให้เวิร์ดเพรสรู้จักมันเสียก่อน ด้วยการฮุคเข้าไปที่ฟิลเตอร์ query_vars แล้วเพิ่มชื่อพารามิเตอร์ของเราลงไป

<?php
add_filter( 'query_vars', function( $vars ) {
  $vars[] = 'is_download';

  return $vars;
} );

จากนั้นในไฟล์เท็มเพลต (ในกรณีนี้จะเป็น single-whitepaper.php) ให้เราตรวจสอบค่า is_download ผ่านฟังก์ชัน get_query_var( 'is_download' ) แล้วแสดงผลออกมาตามที่ต้องการ จะใช้วิธีเปลี่ยนเนื้อหาเอาเฉยๆ หรือแยกออกไปเป็น partial template ก็ตามแต่สะดวก

<?php if ( get_query_var( 'is_download' ) === 'true' ) : ?>
  <h1>Thank you for downloading!</h1>
  <a href="#">Download here</a>
<?php else : ?>
  <?php the_content() ; ?>
<?php endif; ?>

เมื่อเปิดหน้าเว็บด้วย url ปกติ หรือก็คือ /whitepaper/paper-name/ ก็จะขึ้นเป็นหน้าแสดงเนื้อหาปกติ

แต่เมื่อเข้าผ่าน /whitepaper/paper-name/download/ ก็จะขึ้นหน้าให้ดาวน์โหลดแทน

ตัวอย่างโค้ดทั้งหมด

<?php
// functions.php
use AltoRouter as Router;

require_once 'vendor/autoload.php';

add_filter( 'query_vars', function( $vars ) {
  $vars[] = 'is_download';

  return $vars;
} );

add_filter( 'request', function( $request ) {
  $router = new Router(); 

  $router->map( 'GET', '/whitepaper/[:post]/download/?', function( $post ) {
    return [
      'post_type' => 'whitepaper',
      'name' => $post,
      'is_download' => true,
    ];
  });

  // ตรวจสอบว่า URL ปัจจุบันตรงกับแพทเทิร์นใดๆ ที่เราตั้งไว้หรือไม่
  $match = $router->match();

  // ถ้าตรงให้คืนค่า query vars ชุดใหม่จากในฟังก์ชัน
  if ( is_array( $match ) && is_callable( $match['target'] ) ) {
    $request_arguments = call_user_func_array( $match['target'], $match['params'] );

    return $request_arguments;
  }

  // คืนค่า query vars ชุดเดิม
  return $request;
} );
<?php // single-whitepaper.php ?>
<div class="entry-content">
    <?php if ( get_query_var( 'is_download' ) === true ) : ?>
      <div style="text-align: center;">
        <h1>Thank you for downloading!</h1>
        <a href="#">Download here</a>
      </div>
    <?php else : ?>
      <?php the_content() ; ?>
    <?php endif; ?>
</div>

กรณีอื่น: ปรับแต่ง permalink ของ Custom Post Type

แม้เวิร์ดเพรสจะมี Custom Post Type มาให้นานแล้ว แต่เรื่องน่ารำคาญของมันคือเรากำหนด permalink structure ให้ Custom Post Type ไม่ได้เลย บางครั้งเราอยากจะทำ permalink แบบมี taxonomy เข้าไปด้วย เช่น /game/shooting/call-of-duty ก็ไม่สามารถทำได้ เพราะมันจะใช้ได้แค่ /game/call-of-duty เท่านั้น

ปัญหานี้แก้ไขได้ด้วย AltoRouter เช่นเดียวกัน

สมมุติว่าเรามี post type ที่ชื่อว่า game และมี taxonomy ที่ชื่อว่า game_type เราสามารถเขียน map route ได้ดังนี้

  $router->map( 'GET', '/game/[:category]/[:post]/?', function( $category, $post ) {
    return [
      'post_type' => 'game',
      'name' => $post,
      'game_type' => $category,
    ];
  });

อ้อ เรา init router ขึ้นมาทีเดียว เราสามารถเอามาแมพได้กับหลาย URL นะ ไม่ต้อง add_filter ใหม่ทุกครั้ง เหมือนกับการเขียน route ในเฟรมเวิร์กอื่นๆ เลย

ข้อควรระวัง: get_permalink()

โดยปกติแล้วเราจะใช้ฟังก์ชัน get_permalink() ในการดึง URL ของโพสต์ต่างๆ ออกมา อย่างไรก็ตามแม้ว่าเราจะใช้ AltoRouter ในการกำหนด router แล้ว เมื่อเราใช้ get_permalink() ในการดึง URL เราก็จะยังได้ URL แบบเดิมของเวิร์ดเพรสเองอยู่ดี เช่นในตัวอย่างข้างบน ฟังก์ชัน get_permalink() จะคืนค่ากลับออกมาเป็น /game/call-of-duty ไม่ใช่ /game/shooting/call-of-duty แต่อย่างใด

สาเหตุมาจากฟิลเตอร์ request นั้นจะทำงานในขั้นตอนการแปลง URL ออกมาเป็น query vars เท่านั้น ไม่ได้มีผลใดๆ กับการสร้าง permalink ของโพสต์ต่างๆ

ถ้าต้องการให้ฟังก์ชัน get_permalink() คืนค่า URL ที่ตรงกับที่เราต้องการออกมาด้วย เราจะต้องฮุคเข้าไปที่ฟิลเตอร์ post_type_link เพื่อแก้ไข ซึ่งเราจะไว้พูดถึงกันในครั้งต่อไป


Posted

in

by

Comments

Leave a Reply

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