Register Register Member Login Member Login Member Login Forgot Password ??
PHP , ASP , ASP.NET, VB.NET, C#, Java , jQuery , Android , iOS , Windows Phone
 

Registered : 109,037

HOME > บทความจากสมาชิก > ตัวอย่างการสร้าง Webboard ด้วย PHP + MySQLi และ Bootstrap



 
Clound SSD Virtual Server

ตัวอย่างการสร้าง Webboard ด้วย PHP + MySQLi และ Bootstrap

ตัวอย่างการสร้าง Webboard ด้วย PHP + MySQLi และ Bootstrap

ตัวอย่างนี้เหมาะสำหรับผู้ที่อยากจะเริ่มต้นเขียน PHP ติดต่อกับ MySQL แต่อยากได้ตัวอย่างโค้ดที่มี comment อธิบายโดยละเอียด แต่ไม่อยากให้เอาไปต่อยอดทันทีนะครับ อยากให้พยายามอ่าน comment จนเข้าใจมากกว่า

ซึ่งตัวอย่างนี้ก็ใช้วิธีการเขียนแบบที่คนส่วนใหญ่คุ้นเคย คือเป็นแบบ procedural ไม่ได้ใช้ class เพราะหลายคนบอกว่า OOP มันยากเกินไปสำหรับผู้เริ่มต้น หรือยังไม่พร้อมที่จะศึกษา

ซึ่งจริงๆ จุดประสงค์ของตัวอย่างนี้คือต้องการให้แนวคิดเริ่มต้นเกี่ยวการเขียน PHP web application แบบแยกส่วนการคำนวณออกจากส่วนแสดงผล และเน้นพื้นฐานการเขียน PHP ที่ถูกต้องและเน้นเรื่องความปลอดภัย หรือเทคนิคอื่นๆ เช่น


  • การกำหนดค่าของตัวแปรก่อนใช้งาน

  • การตรวจสอบการมีอยู่ของตัวแปรก่อนเข้าถึงด้วย isset() และ empty()

  • การตรวจสอบความถูกต้องของข้อมูลก่อน INSERT Data validation

  • การใช้ mysqli::escape_string() เพื่อป้องกัน SQL Injection

  • การใช้ตัวแปรที่อยู่ใน double quote string " เพื่อแทนที่ค่าใน string (String interpolation) แทนการใช้ concat operator .

  • การกำหนด id ให้ element เพื่อใช้ร่วมกับ hash tag ใน URL มีผลให้ browser scroll มายังตำแหน่งที่ต้องการเมื่อ page load



และตั้งใจให้เป็นตัวอย่างพื้นฐานเพื่อเปรียบเทียบกับตัวอย่างต่อไปที่ตั้งใจจะทำโดยใช้โค้ดเดิมนี่แหละ แต่จะทำให้อยู่ในรูปแบบ OOP จะได้สามารถพิจารณากันได้ว่าถ้าใช้ OOP แล้วมันจะดีกว่าอย่างไร

แต่ก็ใช่ว่าตัวอย่างนี้จะไม่ใช้ object เลยเสียทีเดียว เพราะใช้ฟังก์ชั่น mysqli ในแบบ OOP เหตุผลมีอธิบายอยู่ใน Source Code ครับ

โครงสร้างฐานข้อมูลอาจจะเห็นมีบางอย่างที่ยังไม่ได้ใช้งานในตัวอย่างนี้ เช่น user_id ซึ่งตั้งใจดีไซน์เผื่อไว้สำหรับระบบสมาชิกครับ

Download ไฟล์ทั้งหมดได้ ที่นี่

Github Respository
http://github.com/phpinfo-in-th/workshop-webboard

มีข้อสงสัยสอบถามได้ที่
phpinfo.in.th Facebook Page




โครงสร้างฐานข้อมูล
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;


CREATE TABLE IF NOT EXISTS `comment` (
`id` int(10) unsigned NOT NULL,
  `topic_id` int(10) unsigned NOT NULL,
  `user_id` int(10) unsigned NOT NULL DEFAULT '0',
  `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `modified` timestamp NULL DEFAULT NULL,
  `description` text NOT NULL,
  `name` varchar(64) NOT NULL,
  `ip` varchar(45) CHARACTER SET ascii COLLATE ascii_bin NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `topic` (
`id` int(10) unsigned NOT NULL,
  `user_id` int(10) unsigned NOT NULL DEFAULT '0',
  `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `modified` timestamp NULL DEFAULT NULL,
  `last_commented` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `tags` varchar(255) NOT NULL DEFAULT '',
  `title` varchar(255) NOT NULL,
  `description` text NOT NULL,
  `name` varchar(64) NOT NULL,
  `ip` varchar(45) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
  `num_comments` int(10) unsigned NOT NULL DEFAULT '0',
  `last_commented_user_id` int(10) unsigned NOT NULL DEFAULT '0',
  `last_commented_name` varchar(64) NOT NULL DEFAULT '',
  `num_views` int(10) unsigned NOT NULL DEFAULT '0'
) ENGINE=MyISAM DEFAULT CHARSET=utf8;


ALTER TABLE `comment`
 ADD PRIMARY KEY (`id`), ADD KEY `topic_id` (`topic_id`), ADD KEY `user_id` (`user_id`), ADD KEY `created` (`created`), ADD KEY `name` (`name`);

ALTER TABLE `topic`
 ADD PRIMARY KEY (`id`), ADD KEY `user_id` (`user_id`), ADD KEY `created` (`created`), ADD KEY `last_replied` (`last_commented`), ADD KEY `name` (`name`), ADD KEY `num_comments` (`num_comments`), ADD KEY `num_views` (`num_views`);


ALTER TABLE `comment`
MODIFY `id` int(10) unsigned NOT NULL AUTO_INCREMENT;
ALTER TABLE `topic`
MODIFY `id` int(10) unsigned NOT NULL AUTO_INCREMENT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;


inc/mysqli.inc.php (ไฟล์สำหรับ config ฐานข้อมูล แก้ไขข้อมูลให้ตรงตามเครื่องคุณที่ไฟล์นี้)

<?php
if (get_magic_quotes_gpc() || ini_get('register_globals')) {
  
/*
  ตรวจสอบการตั้งค่า magic_quotes_gpc และ register_globals ใน php.ini ว่าเปิดใช้งานอยู่หรือไม่
  ถ้ามีการเปิดใช้งาน เราจะไม่ให้โปรแกรมติดต่อกับฐานข้อมูลโดยเด็ดขาด
  ดู
  http://php.net/manual/en/info.configuration.php#ini.magic-quotes-gpc
  http://php.net/manual/en/ini.core.php#ini.register-globals
  */
  
$FATAL_ERROR 'มีการตั้งค่าที่ไม่ปลอดภัยใน php.ini';
  
require 'inc/main.inc.php';
}
/*
สร้าง object ของ class mysqli
*/
$mysqli new mysqli();
/*
ทำการเชื่อมต่อกับฐานข้อมูล
โดยเราจำเป็นต้องใช้ @ operator เพื่อกัน PHP แสดง warning
เพราะ mysqli::connect() จะทำให้เกิด PHP warning หากมี error
*/
@$mysqli->connect(
  
'localhost'// host
  
'root'// username
  
''// password
  
'phpinfo_in_th_workshop' // default database
);
/*
แต่เราจะมาตรวจสอบ error ตรงนี้แทน
หากมี error ที่เกิดขึ้นระหว่างการเชื่อมต่อ เช่น username หรือ password ผิด
จะมี error message ส่งมาจาก MySQL Server โดยจะไปอยู่ใน mysqli::$connect_error
ก็ให้แสดง error message ที่ได้จาก MySQL Server และจบการทำงาน (ดู inc/main.inc.php)
*/
if ($mysqli->connect_error) {
  
$TITLE $mysqli->connect_error;
  
/*
  ให้ error message อยู่ในรูปแบบ #<error code> - <error message>
  */
  
$FATAL_ERROR "#{$mysqli->connect_errno} - {$mysqli->connect_error}";
  
require 'inc/main.inc.php';
}
/*
กำหนด charset สำหรับการเชื่อมต่อครั้งนี้ให้เป็น utf8
*/
$mysqli->set_charset('utf8');






ไฟล์ PHP หลัก

index.php

<?php
/*
ตัวอย่างนี้จะเน้นการแยกส่วนการทำงาน
คือส่วนที่ติดต่อกับฐานข้อมูลก็จะเป็นตัวสร้างข้อมูลเพื่อส่งไปให้ template แสดงผล
โดยตัวแปรที่จะถูกใช้ใน template (inc/index.inc.php, inc/post.inc.php, inc/view.inc.php)
จะเป็นตัวแปรที่เป็นตัวพิมพ์ใหญ่ทั้งหมด เช่น $PAGE, $ITEMS
ซึ่งเราจะใช้แนวทางนี้ทั้งหมดสำหรับตัวอย่างนี้
*/

/*
ทำการเชื่อมต่อกับฐานข้อมูล ดู (inc/mysqli.inc.php)
*/
require 'inc/mysqli.inc.php';
/*
กำหนดค่า default ให้กับตัวแปร $PAGE
*/
$PAGE empty($_GET['page'])
  ? 
1
  
: (int)$_GET['page'];
/*
จำนวนกระทู้ที่จะแสดงใน 1 หน้า
*/
$ITEMS_PER_PAGE 100;
/*
คำนวณ offset เริ่มต้นที่จะกำหนดใน LIMIT ซึ่ง offset ของแถวแรกเริ่มที่ 0 ไม่ใช่ 1
ถ้าอยู่ที่หน้า 1 ก็จะได้ LIMIT 0, 100
ถ้าอยู่ที่หน้า 3 ก็จะได้ LIMIT 200, 100
*/
$START_OFFSET = ($PAGE 1) * $ITEMS_PER_PAGE;
/*
ส่ง SQL query ไปยัง MySQL Server ด้วย mysqli::query()
หากไม่มี error และชนิดของ query ที่ส่งไปเป็น SELECT หรืออื่นๆ
ที่คืนแถวกลับมาเช่น SHOW DATABASES, SHOW VARIABLES
mysqli::query() จะ return instance ของ class mysqli_result กลับมา
นอกนั้นจะ return true หรือ false
โดยเราจะเลือก SELECT เฉพาะฟิลด์ที่ต้องใช้มาเท่านั้น
จะไม่ใช้ SELECT * เพื่อลดการใช้หน่อยความจำและเพื่อเพิ่มประสิทธิภาพ
และเราจะเรียงลำดับตามวันที่มีผู้แสดงความเห็นล่าสุด (last_commented)
*/
$result $mysqli->query(
  
"
  SELECT
    `id`,
    `created`,
    `last_commented`,
    `title`,
    `name`,
    `num_comments`,
    `last_commented_name`
  FROM `topic`
  ORDER BY `last_commented` DESC
  LIMIT 
{$START_OFFSET}{$ITEMS_PER_PAGE}
  "
);
/*
เมื่อได้ $result ซึ่งจะเป็น instance ของ class mysqli_result
เราก็จะอ่านข้อมูลที่ได้ลง array ด้วย mysqli_result::fetch_assoc()
คืออ่านมาเป็น associative array และใส่เข้าไปใน array $ITEMS เพื่อนำไปใช้ใน template ต่อไป
โดยเราจะอ่านเข้ามาพักในตัวแปร $item ก่อน ซึ่งหากยังมีข้อมูลอยู่ $item จะไม่ใช่ null
จะทำให้เงื่อนไขใน while เป็นจริง $item ก็จะถูกเพิ่มเข้าไปใน $ITEMS
แต่หากอ่านข้อมูลจนหมดแล้ว (หรือไม่มีข้อมูลตั้งแต่ต้น) mysqli_result::fetch_assoc() จะ return null
และทำให้ $item เป็น null เงื่อนไขก็จะเป็นเท็จ และออกจาก while
*/
$ITEMS array();
while ($item $result->fetch_assoc()) {
  
$ITEMS[] = $item;
}
/*
ปล่อยหน่วยความจำที่ $result ใช้
*/
$result->free();
/*
หาจำนวนกระทู้ทั้งหมด ซึ่งในที่นี้ไม่ใช้ SELECT SQL_CALC_FOUND_ROWS ร่วมกับ FOUND_ROWS()
เพราะพิสูจน์จากหลายแหล่งข้อมูลแล้วว่าทำงานช้ากว่า COUNT(*)
*/
$result $mysqli->query('SELECT COUNT(*) FROM `topic`');
/*
mysqli_result::fetch_row() หรือ mysqli_result::fetch_assoc()
จะคืนค่ากลับมาเป็น array เสมอแม้จะมีแค่ฟิลด์เดียวที่ SELECT มาก็ตาม
แต่เนื่องจากเราอยากได้ค่าของฟิลด์แรกใน array ไม่ใช่ตัว array เอง
เราจึงใช้ current() เพื่ออ่านค่าของสมาชิกตัวแรกใน array
*/
$FOUND_ROWS current($result->fetch_row());
/*
ปล่อยหน่วยความจำที่ $result ใช้
*/
$result->free();
/*
หาจำนวนหน้าทั้งหมด โดยหารจำนวนแถวทั้งหมดด้วยจำนวนที่แสดงผลต่อหน้า และปัดเศษขึ้นด้วย ceil()
*/
$NUM_PAGES ceil($FOUND_ROWS $ITEMS_PER_PAGE);
/*
บอก inc/main.inc.php ให้ require ไฟล์ inc/index.inc.php เป็น template
*/
$PAGE_TEMPLATE 'inc/index.inc.php';
require 'inc/main.inc.php';



post.php

<?php
/*
เราใช้ post.php เป็นทั้งไฟล์ที่ทำการแสดงผลและบันทึกข้อมูลกระทู้ใหม่
ดังนั้นเราจะตรวจสอบว่าการเรียกไฟล์นี้นั้นเป็นการบันทึกหรือไม่ด้วยค่าของตัวแปร $_SERVER['REQUEST_METHOD']
ซึ่งมันจะมีค่าเป็น 'POST' หากมีการ submit มาจาก <form> ที่มี method="post"
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  
/*
  ตรวจสอบให้แน่ชัดว่ามีข้อมูลที่จำเป็นส่งมาครบหรือไม่ด้วย isset()
  ซึ่งจะเป็นจริงหากใน $_POST มี key ที่ต้องการครบ
  */
  
if (!isset($_POST['title'], $_POST['description'], $_POST['name'])) {
    
/*
    หากไม่ครบก็ให้ redirect ไปที่ index.php
    */
    
header('Location: index.php');
    
exit;
  }
  
/*
  เราจะ copy $_POST มาไว้ในตัวแปร $DATA
  ด้วยเหตุผลที่ว่าเราจะไม่เปลี่ยนแปลงค่าของ $_POST โดยตรง
  และเพื่อให้เป็นแนวทางเดียวกันกับทุกตัวแปรที่จะส่งไปยัง template
  */
  
$DATA $_POST;
  
/*
  ทำการ trim() (ตัดช่องว่างหน้าและหลัง) ของข้อมูลใน $DATA ทุกตัว
  */
  
foreach ($DATA as $key => $value) {
    
$DATA[$key] = trim($value);
  }
  
/*
  ตรวจสอบว่า $DATA['title'] เป็นค่าว่างหรือไม่
  จะเห็นว่าเราใช้ === เปรียบเทียบกับ empty string โดยไม่ใช้ empty() หรือ !$DATA['title']
  เพราะการเปรียบเทียบด้วยวิธีหลังเป็นการเปรียบเทียบแบบ loose คือมันจะเป็นจริงได้ในหลายกรณีเกินไป
  เช่น เมื่อ $DATA['title'] มีค่าเท่ากับ string '0' ซึ่งไม่ตรงความต้องการของเราแน่ๆ
  */
  
if ($DATA['title'] === '') {
    
/*
    กำหนดค่าให้กับตัวแปร $FORM_ERRORS เพื่อนำไปใช้ใน inc/form_errors.inc.php ต่อไป
    */
    
$FORM_ERRORS['title'] = "กรุณาระบุ 'หัวข้อ'";
  }
  
/*
  และตรวจสอบความยาวของ $DATA['title'] ว่ามีความยาวมากกว่าที่กำหนดหรือไม่
  ด้วย mb_strlen() ซึ่งเราไม่ใช้ strlen()
  เพราะว่า strlen() จะตรวจจำนวน byte ไม่ใช่จำนวนตัวอักษร
  ซึ่งปัจจุบันเราใช้ encoding ชนิด UTF-8 เป็นหลัก ตัวอักษร 1 ตัวอาจจะมีความยาวมากกว่า 1 byte
  อย่างภาษาไทย ทุกตัวอักษรจะมีขนาด 3 bytes
  */
  
elseif (mb_strlen($DATA['title'], 'UTF-8') > 255) {
    
$FORM_ERRORS['title'] = "'หัวข้อ' ต้องมีความยาวไม่เกิน 255 ตัวอักษร";
  }
  
/*
  ทำการตรวจสอบกับข้อมูลอื่นๆ เช่นเดียวกัน
  */
  
if ($DATA['description'] === '') {
    
$FORM_ERRORS['description'] = "กรุณาระบุ 'รายละเอียด'";
  } 
elseif (mb_strlen($DATA['description'], 'UTF-8') > 65535) {
    
$FORM_ERRORS['description'] = "'รายละเอียด' ต้องมีความยาวไม่เกิน 65535 ตัวอักษร";
  }
  
if ($DATA['name'] === '') {
    
$FORM_ERRORS['name'] = "กรุณาระบุ 'ชื่อ'";
  } 
elseif (mb_strlen($DATA['name'], 'UTF-8') > 64) {
    
$FORM_ERRORS['name'] = "'ชื่อ' ต้องมีความยาวไม่เกิน 64 ตัวอักษร";
  }
  
/*
  ถ้าไม่มีตัวแปร $FORM_ERRORS ถูกสร้างขึ้นมาจากการตรวจสอบข้างต้น แสดงว่าไม่มี error
  ข้อมูลทั้งหมดสามารถ INSERT เข้าฐานข้อมูลได้อย่างปลอดภัย
  */
  
if (!isset($FORM_ERRORS)) {
    
/*
    ทำการเชื่อมต่อกับฐานข้อมูล ดู (inc/mysqli.inc.php)
    ซึ่งเราไม่จำเป็นต้องเชื่อมต่อตั้งแต่แรกเพราะยังไม่จำเป็นต้องใช้จนกว่าจะแน่ใจว่าข้อมูลนั้นถูกต้องทั้งหมด
    */
    
require 'inc/mysqli.inc.php';
    
/*
    ส่ง SQL query ไปยัง MySQL Server ด้วย mysqli::query()
    โดยเราจะ escape ข้อมูลที่มาจากภายนอกทั้งหมดด้วย mysqli::escape_string()
    โดยใช้ฟังก์ชั่น sprintf() ช่วย ดู (inc/main.inc.php สำหรับ sprintf())
    */
    
$mysqli->query(
      
/*
      mysqli::escape_string() จะแปลงตัวอักษรพิเศษ เช่น ' ให้เป็น \' หรือ ''
      ซึ่งทำให้ MySQL Server รู้ว่ามันเป็นข้อมูล ไม่ใช่ delimeter
      หากเราไม่ใช้ mysqli::escape_string() และผ่านข้อมูลไปเป็น SQL query โดยตรง
      อาจจะทำให้เกิด error หรือ SQL Injection ขึ้นได้
      และนี่คือข้อดีของการใช้ mysqli ในแบบ OOP คือจะเห็นว่าเราสามารถแทนที่
      $mysqli->escape_string() ลงไปใน double quote string ได้เลย
      แต่ถ้าเราใช้ mysqli_escape_string() ที่เป็น procedural style
      จะไม่สามารถทำแบบนี้ได้
      */
      
"
      INSERT INTO `topic`
      (
        `last_commented`,
        `title`,
        `description`,
        `name`,
        `ip`
      )
      VALUES
      (
        NOW(),
        '
{$mysqli->escape_string($DATA['title'])}',
        '
{$mysqli->escape_string($DATA['description'])}',
        '
{$mysqli->escape_string($DATA['name'])}',
        '
{$_SERVER['REMOTE_ADDR']}'
      )
      "
    
);
    
/*
    redirect ไปยัง index.php โดยส่ง query string ชื่อ highlight_id
    ที่มีค่าเป็น id ของแถวที่เพิ่ง INSERT เข้าไปในตาราง topic ไปด้วย
    เพื่อใช้เน้นสีพื้นหลังของกระทู้ใหม่ (ดู inc/index.inc.php)
    */
    
header('Location: index.php?highlight_id=' $mysqli->insert_id);
    
/*
    จบการทำงาน
    */
    
exit;
  }
  
/*
  หากมี error ก็จะแสดงผลให้ผู้ใช้แก้ไขข้อมูลให้ถูกต้อง
  */
else {
  
/*
  หากไม่ใช่การ POST ก็ให้กำหนดค่า default สำหรับ $DATA ซึ่งจะถูกใช้งานใน inc/post.inc.php
  โดยให้เป็นค่าว่างทั้งหมด
  */
  
$DATA array(
    
'title' => '',
    
'description' => '',
    
'name' => '',
  );
}
$TAGS array('PHP''JavaScript''SQL''HTML''CSS');
/*
บอก inc/main.inc.php ให้ require ไฟล์ inc/post.inc.php เป็น template
*/
$TITLE 'ตั้งกระทู้ใหม่';
$PAGE_TEMPLATE 'inc/post.inc.php';
require 'inc/main.inc.php';



view.php

<?php
require 'inc/mysqli.inc.php';
/*
เราใช้ view.php เป็นทั้งไฟล์ที่ทำการแสดงผลและบันทึกข้อมูลความเห็นใหม่
ดังนั้นเราจะตรวจสอบว่าการเรียกไฟล์นี้นั้นเป็นการบันทึกหรือไม่ด้วยค่าของตัวแปร $_SERVER['REQUEST_METHOD']
ซึ่งมันจะมีค่าเป็น 'POST' หากมีการ submit มาจาก <form> ที่มี method="post"
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  
/*
  ตรวจสอบให้แน่ชัดว่ามีข้อมูลที่จำเป็นส่งมาครบหรือไม่ด้วย isset()
  ซึ่งจะเป็นจริงหากใน $_POST มี key ที่ต้องการครบ
  */
  
if (!isset($_POST['topic_id'], $_POST['description'], $_POST['name'])) {
    
/*
    หากไม่ครบก็ให้ redirect ไปที่ index.php
    */
    
header('Location: index.php');
    
exit;
  }
  
/*
  เราจะ copy $_POST มาไว้ในตัวแปร $DATA
  ด้วยเหตุผลที่ว่าเราจะไม่เปลี่ยนแปลงค่าของ $_POST โดยตรง
  และเพื่อให้เป็นแนวทางเดียวกันกับทุกตัวแปรที่จะส่งไปยัง template
  */
  
$DATA $_POST;
  
/*
  $TOPIC_ID จะถูกใช้ใน template
  */
  
$TOPIC_ID = (int)$DATA['topic_id'];
  
/*
  ทำการ trim() (ตัดช่องว่างหน้าและหลัง) ของข้อมูลใน $DATA ทุกตัว
  */
  
foreach ($DATA as $key => $value) {
    
$DATA[$key] = trim($value);
  }
  
/*
  ตรวจสอบว่ามีกระทู้ที่มี id ตาม $TOPIC_ID อยู่จริงหรือไม่
  จะเห็นว่าเราสามารถนำ $TOPIC_ID ไปแทนที่ตรงๆ ได้เลย ไม่ใช้ mysqli::escape_string()
  เพราะก่อนหน้านี้เรากำหนด $TOPIC_ID ด้วย (int)$DATA['topic_id']
  ซึ่ง (int) เป็น cast operator จะทำการแปลงค่าใน $DATA['topic_id'] ให้เป็นตัวเลขจำนวนเต็ม
  หากไม่สามารถแปลงได้ มันจะให้ค่า 0 เสมอ
  */
  
$result $mysqli->query(
    
"
    SELECT `id`
    FROM `topic`
    WHERE `id` = 
{$TOPIC_ID}
    LIMIT 1
    "
  
);
  
/*
  หาก SELECT ไม่เจอ mysqli_result::fetch_row() จะ return null
  */
  
if (!$result->fetch_row()) {
    
header('Location: index.php');
    
exit;
  }
  
/*
  ทำการตรวจสอบความถูกต้องของข้อมูลแบบเดียวกันกับ post.php
  */
  
if ($DATA['description'] === '') {
    
$FORM_ERRORS['description'] = "กรุณาระบุ 'ข้อความ'";
  } 
elseif (mb_strlen($DATA['description'], 'UTF-8') > 65535) {
    
$FORM_ERRORS['description'] = "'ข้อความ' ต้องมีความยาวไม่เกิน 65535 ตัวอักษร";
  }
  
if ($DATA['name'] === '') {
    
$FORM_ERRORS['name'] = "กรุณาระบุ 'ชื่อ'";
  } 
elseif (mb_strlen($DATA['name'], 'UTF-8') > 64) {
    
$FORM_ERRORS['name'] = "'ชื่อ' ต้องมีความยาวไม่เกิน 64 ตัวอักษร";
  }
  
/*
  ถ้าไม่มีตัวแปร $FORM_ERRORS ถูกสร้างขึ้นมาจากการตรวจสอบข้างต้น แสดงว่าไม่มี error
  ข้อมูลทั้งหมดสามารถ INSERT เข้าฐานข้อมูลได้อย่างปลอดภัย
  */
  
if (!isset($FORM_ERRORS)) {
    
$mysqli->query(
      
/*
      mysqli::escape_string() จะแปลงตัวอักษรพิเศษ เช่น ' ให้เป็น \' หรือ ''
      ซึ่งทำให้ MySQL Server รู้ว่ามันเป็นข้อมูล ไม่ใช่ delimeter
      หากเราไม่ใช้ mysqli::escape_string() และผ่านข้อมูลไปเป็น SQL query โดยตรง
      อาจจะทำให้เกิด error หรือ SQL Injection ขึ้นได้
      ยกเว้น $TOPIC_ID ตามที่กล่าวไว้ข้างต้น
      และ $_SERVER['REMOTE_ADDR'] ที่เชื่อถือได้ว่าไม่มีตัวอักษรพิเศษแน่นอน
      และนี่คือข้อดีของการใช้ mysqli ในแบบ OOP คือจะเห็นว่าเราสามารถแทนที่
      $mysqli->escape_string() ลงไปใน double quote string ได้เลย
      แต่ถ้าเราใช้ mysqli_escape_string() ที่เป็น procedural style
      จะไม่สามารถทำแบบนี้ได้
      */
      
"
      INSERT INTO `comment`
      (
        `topic_id`,
        `description`,
        `name`,
        `ip`
      )
      VALUES
      (
        
{$TOPIC_ID},
        '
{$mysqli->escape_string($DATA['description'])}',
        '
{$mysqli->escape_string($DATA['name'])}',
        '
{$_SERVER['REMOTE_ADDR']}'
      )
      "
    
);
    
/*
    อ่าน id ของความเห็นที่เพิ่ง INSERT เข้าไปด้วย mysqli::$insert_id
    */
    
$comment_id $mysqli->insert_id;
    
/*
    ทำการ UPDATE กระทู้
    โดยให้เวลาตอบกระทู้ล่าสุด (last_commented) เป็นเวลาปัจจุบัน เพื่อให้กระทู้ย้ายขึ้นมาบนสุด
    และเพิ่มจำนวนความเห็น (num_comments)
    และกำหนดชื่อผู้แสดงความเห็นล่าสุด (last_commented_name) เป็น $DATA['name']
    */
    
$mysqli->query(
      
"
      UPDATE `topic`
      SET
        `last_commented` = NOW(),
        `num_comments` = `num_comments` + 1,
        `last_commented_name` = '
{$mysqli->escape_string($DATA['name'])}'
      WHERE `id` = 
{$TOPIC_ID}
      "
    
);
    
/*
    refresh view.php ด้วยการ redirect
    โดยกำหนด hash tag #comment-<id ของความเห็นล่าสุด> เข้าไปด้วย
    เพื่อให้ browser scroll มาที่ความเห็นที่ผู้ใช้เพิ่งบันทึกไป
    */
    
header("Location: view.php?topic_id={$TOPIC_ID}#comment-{$comment_id}");
    
exit;
  }
  
/*
  หากมี error ก็จะแสดงผลให้ผู้ใช้แก้ไขข้อมูลให้ถูกต้อง
  */
else {
  
/*
  หากไม่ใช่การ POST ก็ให้กำหนดค่า default สำหรับ $TOPIC_ID จาก $_GET['topic_id']
  และกำหนดค่า default สำหรับ $DATA ซึ่งจะถูกใช้งานใน inc/view.inc.php
  โดยให้เป็นค่าว่างทั้งหมด
  */
  
$TOPIC_ID empty($_GET['topic_id'])
    ? 
0
    
: (int)$_GET['topic_id'];
  
$DATA array(
    
'description' => '',
    
'name' => '',
  );
}
/*
SELECT กระทู้ที่มี id ตาม $TOPIC_ID
*/
$result $mysqli->query(
  
"
  SELECT
    `id`,
    `created`,
    `title`,
    `description`,
    `name`,
    `ip`,
    `num_comments`,
    `num_views`
  FROM `topic`
  WHERE `id` = 
{$TOPIC_ID}
  LIMIT 1
  "
);
$topic $result->fetch_assoc();
/*
หากไม่เจอกระทู้ที่มี id ตาม $TOPIC_ID ตัวแปร $topic ก็จะเป็น null
*/
if (!isset($topic)) {
  
/*
  กำหนดให้ inc/main.inc.php แสดง error
  */
  
$FATAL_ERROR $TITLE "ไม่มีกระทู้หมายเลข {$TOPIC_ID} อยู่ในฐานข้อมูล";
  
require 'inc/main.inc.php';
}
/*
กำหนดตัวแปร $ITEMS ให้เป็น array ของทั้งกระทู้และความเห็น
โดยเราจะไม่แยกมันออกจากกัน เพื่อลดความซ้ำซ้อนของโค้ดแสดงผล  (ดู inc/view.inc.php)
ให้ $topic เป็นสมาชิกตัวแรกของมัน
*/
$ITEMS array($topic);
/*
ปล่อยหน่วยความจำที่ $result ใช้
*/
$result->free();
/*
UPDATE จำนวนผู้เข้าชมของกระทู้
*/
$mysqli->query(
  
"
  UPDATE `topic`
  SET `num_views` = `num_views` + 1
  WHERE `id` = 
{$TOPIC_ID}
  LIMIT 1
  "
);
/*
หากกระทู้มีความเห็น
*/
if ($topic['num_comments']) {
  
/*
  กำหนดค่า default ให้กับตัวแปร $PAGE
  */
  
$PAGE empty($_GET['page'])
    ? 
1
    
: (int)$_GET['page'];
  
/*
  จำนวนความเห็นที่จะแสดงใน 1 หน้า
  */
  
$ITEMS_PER_PAGE 100;
  
/*
  คำนวณ offset เริ่มต้นที่จะกำหนดใน LIMIT ซึ่ง offset ของแถวแรกเริ่มที่ 0 ไม่ใช่ 1
  ถ้าอยู่ที่หน้า 1 ก็จะได้ LIMIT 0, 100
  ถ้าอยู่ที่หน้า 3 ก็จะได้ LIMIT 200, 100
  */
  
$START_OFFSET = ($PAGE 1) * $ITEMS_PER_PAGE;
  
/*
  SELECT และอ่านข้อมูลความเห็นเข้ามาไว้ใน $ITEMS (ดู index.php)
  */
  
$result $mysqli->query(
    
"
    SELECT
      `id`,
      `created`,
      `description`,
      `name`,
      `ip`
    FROM `comment`
    WHERE `topic_id` = 
{$TOPIC_ID}
    ORDER BY `created`
    LIMIT 
{$START_OFFSET}{$ITEMS_PER_PAGE}
    "
  
);
  
while ($comment $result->fetch_assoc()) {
    
$ITEMS[] = $comment;
  }
  
$result->free();
  
$result $mysqli->query(
    
"
    SELECT COUNT(*)
    FROM `comment`
    WHERE `topic_id` = 
{$TOPIC_ID}
    "
  
);
  
$FOUND_ROWS current($result->fetch_row());
  
$result->free();
  
$NUM_PAGES ceil($FOUND_ROWS $ITEMS_PER_PAGE);
else {
  
/*
  ถ้าไม่มีความเห็น ก็กำหนดค่า default ให้กับ $NUM_PAGES เพื่อใช้ใน template ต่อไป
  */
  
$NUM_PAGES 0;
}
/*
บอก inc/main.inc.php ให้ใช้หัวข้อกระทู้เป็น <title>
*/
$TITLE $topic['title'];
/*
บอก inc/main.inc.php ให้ require ไฟล์ inc/view.inc.php เป็น template
*/
$PAGE_TEMPLATE 'inc/view.inc.php';
require 'inc/main.inc.php';






ไฟล์ template

inc/main.inc.php

<?php
/*
ไฟล์ template หลักของทุกหน้า ซึ่งอาจจะ require template รองอีกที
จากตัวแปร $PAGE_TEMPLATE ที่ต้องกำหนดไว้ก่อนเรียกใช้ไฟล์นี้
และจะใช้ Alternative syntax for control structures ใน template
(ดู http://php.net/manual/en/control-structures.alternative-syntax.php)
เช่น
if (1 > 2) {
  ...
}
จะเป็น
if (1 > 2):
  ...
endif;
และในตัวอย่างนี้ จะยังไม่ใช้ JavaScript ใดใดทั้งสิ้น
*/

/*
ฟังก์ชั่นตัวช่วยแปลง unix timestamp หรือ SQL DATETIME ให้เป็นวันที่ภาษาไทยแบบย่อ
เช่น '2014-12-01 04:14:00' จะกลายเป็น '1 ธ.ค. 57 04:14'
*/
function thai_datetime($timestamp)
{
  
static $thaiMonthAbbrs array(
    
'ม.ค.''ก.พ.''มี.ค.''เม.ย.''พ.ค.''มิ.ย.',
    
'ก.ค.''ส.ค.''ก.ย.''ต.ค.''พ.ย.''ธ.ค.',
  );
  
/*
  หาก $timestamp ไม่ใช่ตัวเลข
  */
  
if (!is_numeric($timestamp)) {
    
/*
    ให้ใช้ฟังก์ชั่น strtotime() แปลง $timestamp ให้เป็น unix timestamp
    */
    
$timestamp strtotime($timestamp);
  }
  
/*
  และใช้ฟังก์ชั่น getdate() ดึงข้อมูล array เกี่ยวกับ timestamp นั้นๆ
  เช่น วันที่, เดือน, ปี, ชั่วโมง, นาที, วินาที ฯลฯ
  */
  
$info getdate($timestamp);
  
/*
  เราใช้ฟังก์ชั่น sprintf() เพื่อทำการจัดรูปแบบข้อความ
  โดย argument แรกจะเป็น 'รูปแบบ' และ argument ที่เหลือจะเป็นค่าที่จะส่งไปแทนที่ใน 'รูปแบบ'
  ซึ่งรูปแบบที่จะเป็น ตัวแทนที่ นั้น จะเริ่มต้นด้วย % ตามด้วยตัวอักษร s, d หรืออื่นๆ (ดู PHP Manual)
  */
  
return sprintf(
    
/*
    %d คือให้แทนที่ค่าที่ส่งมา ในรูปแบบตัวเลขจำนวนเต็ม
    %s คือให้แทนที่ค่าที่ส่งมา ในรูปแบบ string หากค่านั้นไม่ใช่ string ก็จะทำการแปลงให้เป็น string
    %02d คือให้แทนที่ค่าที่ส่งมา ในรูปแบบตัวเลขจำนวนเต็ม และเติม 0 เข้าไปข้างหน้าสูงสุด 2 ตัว
    เช่น ค่าที่ส่งมาคือ 1 ก็จะกลายเป็น 01
    */
    
'%d %s %d %02d:%02d',
    
/*
    $info['mday'] จะเป็นตัวเลขวันที่ 1 - 31
    ค่าจะไปแทนที่ %d ตัวแรก
    */
    
$info['mday'],
    
/*
    $info['mon'] จะเป็นตัวเลขเดือน 1 - 12
    ซึ่งเราจะเอาไปใช้เป็น key ในการเลือกค่าจากตัวแปร $thaiMonthAbbrs
    ซึ่ง $thaiMonthAbbrs นั้นมี key เริ่มที่ 0 เราจึงต้องลบ $info['mon'] ด้วย 1 เสียก่อน
    ค่านี้จะไปแทนที่ %s
    */
    
$thaiMonthAbbrs[$info['mon'] - 1], 
    
/*
    แปลง $info['year'] ให้เป็น พ.ศ. โดย + ด้วย 543
    และใช้ substr() ตัดเฉพาะตัวเลข 2 หลักสุดท้ายของ พ.ศ. ออกมา
    ค่าจะไปแทนที่ %d ตัวที่สอง
    */
    
substr($info['year'] + 543, -2),
    
/*
    เลขชั่วโมง
    ค่านี้จะไปแทนที่ %02d ตัวแรก
    */
    
$info['hours'],
    
/*
    เลขนาที
    ค่านี้จะไปแทนที่ %02d ตัวที่สอง
    */
    
$info['minutes']
  );
}

/*
ฟังก์ชั่นตัวช่วยแปลง unix timestamp หรือ SQL DATETIME ให้เป็นช่วงห่างของเวลาภาษาไทย
เช่น 15 นาทีที่แล้ว
และหากช่วงห่างเกินจำนวนวันที่กำหนดไว้ใน $daysBeforeFullDate ก็จะแสดงวันที่เต็ม
โดยเรียกใช้ thai_datetime() อีกทอดหนึ่ง
*/
function thai_time($timestamp$daysBeforeFullDate 3)
{
  
/*
  หาก $timestamp ไม่ใช่ตัวเลข
  */
  
if (!is_numeric($timestamp)) {
    
/*
    ให้ใช้ฟังก์ชั่น strtotime() แปลง $timestamp ให้เป็น unix timestamp
    */
    
$timestamp strtotime($timestamp);
  }
  
/*
    เราจะหาค่าช่วงห่างของเวลาปัจจุบันกับ $timestamp
    โดยเวลาปัจจุบันนั้นหาได้จากฟังก์ชั่น time()
  */
    
$diff time() - $timestamp;
  
/*
    หากความต่างของเวลาปัจจุบันกับ $timestamp น้อยกว่า 10 วินาที
  */
    
if (!$diff) {
        
return 'ไม่กี่วินาทีที่แล้ว';
    }
    
/*
  หากความต่างของเวลาปัจจุบันกับ $timestamp น้อยกว่า 1 นาที
  */
    
if ($diff 60) {
        
return $diff ' วินาทีที่แล้ว';
    }
  
/*
    หากความต่างของเวลาปัจจุบันกับ $timestamp น้อยกว่า 1 ชั่วโมง
  */
    
if ($diff 3600) {
        
return (int)($diff 60) . ' นาทีที่แล้ว';
    }
    
/*
  หากความต่างของเวลาปัจจุบันกับ $timestamp น้อยกว่า 1 วัน
  */
    
if ($diff 86400) {
        
return (int)($diff 3600) . ' ชั่วโมงที่แล้ว';
    }
    
/*
  หากความต่างของเวลาปัจจุบันกับ $timestamp น้อยกว่าจำนวนวันที่กำหนดไว้
    ในตัวแปร $daysBeforeFullDate ที่เราจะใช้เป็นตัวบอกว่า
    ควรจะแสดงวันที่เต็มเมื่อช่วงห่างเกินกี่วัน
  */
    
if ($diff 86400 $daysBeforeFullDate) {
        
return (int)($diff 86400) . ' วันที่แล้ว';
    }
    
/*
  หากช่วงห่างไม่อยู่ในเงื่อนไขข้างต้นเลย ให้แสดงวันที่เต็ม
  */
  
return thai_datetime($timestamp);
}

/*
หากมีการเชื่อมต่อฐานข้อมูล ให้ทำการตัดการเชื่อมต่อเสีย
*/
if (isset($mysqli)) {
  
/*
  ใช้ @ operator ด้วย เพื่อป้องกัน PHP Warning หากก่อนหน้านี้ทำการเชื่อมต่อฐานข้อมูลไม่สำเร็จ
  */
  
@$mysqli->close();
}
/*
กำหนดค่า default ให้กับตัวแปร $HIGHLIGHT_ID
ซึ่งจะถูกใช้ในไฟล์ inc/index.inc.php (และอื่นๆ ในอนาคต)
*/
$HIGHLIGHT_ID isset($_GET['highlight_id'])
  ? 
$_GET['highlight_id']
  : 
null;
/*
กำหนดค่า default ให้กับตัวแปร $TITLE หากไม่ได้กำหนดไว้ก่อนหน้านี้
*/
if (!isset($TITLE)) {
  
$TITLE 'Webboard Workshop';
}
/*
กำหนดตัวแปร $PARENT_FILENAME ให้เป็นชื่อไฟล์ที่ผู้ใช้เรียก
โดยตรวจจาก $_SERVER['SCRIPT_FILENAME'] ซึ่งจะมีค่าเป็นชื่อไฟล์ PHP ที่ผู้ใช้เรียก
เช่น C:\xampp\htdocs\workshop-webboard\index.php
แต่เนื่องจากเราต้องการเพียงส่วนท้ายสุด คือ index.php เราจึงใช้ฟังก์ชั่น pathinfo()
ดึงข้อมูลส่วนนี้ออกมา ซึ่งปกติ pathinfo() จะคืนค่าออกมาเป็น array รายละเอียดของชื่อไฟล์
แต่ถ้าเรากำหนด argument ตัวที่สอง ก็จะดึงเฉพาะส่วนออกมาได้
ซึ่ง PATHINFO_BASENAME หมายถึง ให้เอาเฉพาะชื่อไฟล์และนามสกุลมา
*/
$PARENT_FILENAME pathinfo($_SERVER['SCRIPT_FILENAME'], PATHINFO_BASENAME);
?>
<!DOCTYPE html>
<html>
  <head>
    <title>
<?php
    
/*
    ก่อน echo ค่าของตัวแปรใดใดก็ตามที่ไม่แน่ใจว่าค่าของมันจะเป็นอะไรกันแน่ ออกมาเป็นส่วนหนึ่งของ HTML
    และไม่ต้องการให้ค่าเหล่านั้นมีความหมายพิเศษ เช่น เราอยากแสดงผลคำว่า '<div>'
    แต่หากเรา echo มันออกมาตรงๆ browser ก็จะมองว่ามันเป็น tag <div> ไม่ใช่ข้อความ '<div>'
    ดังนั้นเราจึงจำเป็นต้อง escape ตัวอักษรพิเศษ < > & " '
    ที่อาจจะมีอยู่ในค่าของตัวแปรให้เป็น html entity เสียก่อน ด้วยฟังก์ชั่น htmlspecialchars()
    เช่น <div> ก็จะกลายเป็น &lt;div&gt;
    */
    
echo htmlspecialchars($TITLEENT_QUOTES'UTF-8');
    
?></title>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap-theme.min.css">
    <link rel="stylesheet" href="css/workshop-webboard.css">
  </head>
  <body>
    <div class="container">
      <div class="navbar navbar-default" role="navigation">
        <div class="container-fluid">
          <div class="navbar-header">
            <a class="navbar-brand" href="index.php">
              Webboard Workshop
            </a>
          </div>
          <ul class="nav navbar-nav">
            <li class="
<?php
            
/*
            ตรวจว่าขณะนี้ผู้ใช้อยู่ที่หน้าแรกหรือไม่
            */
            
if ($PARENT_FILENAME === 'index.php') {
              
/*
              ถ้าใช่ ก็ให้เพิ่ม class 'active' เข้าไปใน <li> นี้
              เพื่อเน้นว่าในขณะนี้ ผู้ใช้อยู่ที่หน้านี้
              */
              
echo 'active';
            }
            
?>">
              <a href="index.php">
                <span class="glyphicon glyphicon-home"></span>
                หน้าแรก
              </a>
            </li>
            <li class="
<?php
            
/*
            ทำการตรวจสอบเมนูอื่นเช่นเดียวกัน
            */
            
if ($PARENT_FILENAME === 'post.php') {
              
echo 'active';
            }
            
?>">
              <a href="post.php">
                <span class="glyphicon glyphicon-pencil"></span>
                ตั้งกระทู้ใหม่
              </a>
            </li>
            
<?php
            
/*
            สำหรับเมนูนี้ ถ้าไม่ได้อยู่ที่ view.php ก็จะไม่แสดงผลเลย
            */
            
if ($PARENT_FILENAME === 'view.php'):
            
?>
            <li class="active">
              <a href="#">
                <span class="glyphicon glyphicon-eye-open"></span>
                
<?php
                
echo htmlspecialchars($TITLEENT_QUOTES'UTF-8');
                
?>
              </a>
            </li>
            
<?php
            
endif;
            
?>
          </ul>
        </div>
      </div>
      
<?php
      
/*
      หากมีตัวแปร $FATAL_ERROR ถูกกำหนดไว้ก่อนหน้า
      แสดงว่ามี error ที่ทำให้ไม่สามารถแสดงผลข้อมูลหน้านั้นได้อย่างถูกต้อง
      เช่น ไม่สามารถเชื่อมต่อฐานข้อมูลได้ หรือผู้ใช้ส่ง id ของกระทู้ที่ไม่มีอยู่จริงมาให้
      เราก็จะแสดงผลข้อความที่กำหนดไว้ใน $FATAL_ERROR
      */
      
if (isset($FATAL_ERROR)):
      
?>
      <div class="alert alert-danger">
        
<?php
        
echo htmlspecialchars($FATAL_ERRORENT_QUOTES'UTF-8');
        
?>
      </div>
      
<?php
      
/*
      นอกนั้นจะ require ไฟล์ที่กำหนดไว้ในตัวแปร $PAGE_TEMPLATE
      ซึ่งจะตรวจสอบด้วย isset() ก่อนว่ามีตัวแปรนี้กำหนดไว้หรือไม่
      ถ้าไม่ได้กำหนด ก็จะไม่ require ไฟล์ใดใด และแสดงหน้าเปล่าๆ ที่มีแค่ส่วน navigation
      */
      
elseif (isset($PAGE_TEMPLATE)):
        
/*
        และไม่ได้ตรวจสอบว่าไฟล์ตามค่าของ $PAGE_TEMPLATE นั้นมีอยู่จริงหรือไม่
        หากไม่มีอยู่จริงก็จะเกิด PHP fatal error และจบการทำงาน
        เพราะ require เป็นคำสั่งที่ต่างจาก include (ซึ่งไม่ได้ใช้ในตัวอย่างนี้)
        จะจบการทำงานทันทีหากหาไฟล์ที่กำหนดไม่เจอ
        */
        
require $PAGE_TEMPLATE;
      
endif;
      
?>
    </div>
    
<?php
    
/*
    ตรวจสอบว่ามี key 'REQUEST_TIME_FLOAT' อยู่ใน array $_SERVER หรือไม่
    ซึ่ง $_SERVER['REQUEST_TIME_FLOAT'] จะเป็นเวลาที่ PHP เริ่มต้นทำงาน
    เป็นหน่วย microsecond (1/1000000 วินาที)
    และมีให้ใช้ตั้งแต่ PHP 5.4 เราจึงต้องตรวจสอบการมีอยู่ของมันก่อนใช้งาน
    หากมีตัวแปรนี้อยู่ เราจะแสดงผลเวลาการทำงานของ request นี้ ว่าใช้เวลาไปเท่าไหร่
    โดยเอาค่าจาก microtime(true) ที่จะ return microsecond ปัจจุบันกลับมา
    ไปลบกับ $_SERVER['REQUEST_TIME_FLOAT'] ก็จะได้เวลาที่ request นี้ใช้
    และจัดรูปแบบให้เป็นทศนิยม 4 ตำแหน่งด้วย number_format()
    การจับเวลาเป็นวิธีหนึ่งที่จะบอกเราได้ว่า เราเขียนโปรแกรมได้อย่างมีประสิทธิภาพหรือไม่ในด้านความเร็ว
    */
    
if (isset($_SERVER['REQUEST_TIME_FLOAT'])):
    
?>
    <div class="text-center">
      <span class="label label-info">Time: 
<?php
      
echo number_format(microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'], 4);
      
?>s</span>
    </div>
    
<?php
    
endif;
    
?>
  </body>
</html>
<?php
/*
จบการทำงานเสมอ
ดังนั้น ณ จุดใดก็ตามที่มีการ require หรือ include ไฟล์นี้ ก็มั่นใจได้ว่าจะจบการทำงานแน่นอน
*/
exit;



inc/index.inc.php

<div class="panel panel-default">
  <div class="panel-heading">
    <h4>Title</h4>
  </div>
  <div class="panel-body">
    Annoucement
  </div>
  <table class="table table-condensed table-bordered table-striped table-hover">
    <thead>
      <tr>
        <th class="text-center width-15">กระทู้โดย</th>
        <th class="text-center width-60">หัวข้อ</th>
        <th class="text-center text-info width-10">
          <span class="glyphicon glyphicon-comment" title="จำนวนความเห็น"></span>
        </th>
        <th class="text-center text-info width-15">ความเห็นล่าสุดโดย</th>
      </tr>
    </head>
    <tbody>
      
<?php
      
/********** เริ่ม LOOP แสดงกระทู้ **********/
      
foreach ($ITEMS as $item):
      
?>
      <tr class="
<?php
      
/*
      เพิ่ม class 'success' เข้าไปใน <tr> นี้ หาก id ของกระทู้ ตรงกับ $_GET['highlight_id']
      ซึ่งจะถูกส่งมาจาก post.php (ดู post.php)
      */
      
if ($item['id'] === $HIGHLIGHT_ID) {
        
echo 'success';
      }
      
?>">
        <td>
          <strong>
          
<?php
          
echo htmlspecialchars($item['name'], ENT_QUOTES'UTF-8');
          
?>
          </strong>
          <br>
          <small class="text-muted" title="
<?php
          
/*
          แปลงวันที่ให้เป็นภาษาไทยด้วยฟังก์ชั่น thai_datetime() ที่กำหนดไว้ใน inc/main.inc.php
          โดยใส่ไว้ใน attribute title เพื่อให้แสดงขึ้นมาเมื่อผู้ใช้เอาเมาส์ไปชี้
          */
          
echo thai_datetime($item['created']);
          
?>">
            
<?php
            
/*
            แปลงวันที่ให้เป็นช่วงห่างของเวลาภาษาไทยด้วยฟังก์ชั่น thai_time()
            ที่กำหนดไว้ใน inc/main.inc.php
            */
            
echo thai_time($item['created']);
            
?>
          </small>
        </td>
        <td>
          <a href="view.php?topic_id=
<?php echo $item['id']; ?>">
          
<?php
          
echo htmlspecialchars($item['title'], ENT_QUOTES'UTF-8');
          
?>
          </a>
        </td>
        <td class="text-center text-info">
          
<?php
          
echo $item['num_comments'];
          
?>
        </td>
        <td>
          
<?php
          
/*
          หาก $item['last_commented_name'] ไม่ใช่ค่าว่าง นั่นหมายถึงกระทู้นี้มีผู้แสดงความเห็น
          ก็ให้แสดงชื่อผู้แสดงความเห็น
          */
          
if ($item['last_commented_name'] !== ''):
          
?>
          <strong class="text-info">
            
<?php
            
echo htmlspecialchars($item['last_commented_name'], ENT_QUOTES'UTF-8');
            
?>
          </strong>
          <br>
          <small class="text-muted" title="
<?php
          
echo thai_datetime($item['last_commented']);
          
?>">
            
<?php
            
echo thai_time($item['last_commented']);
            
?>
          </small>
          
<?php
          
endif;
          
?>
        </td>
      </tr>
      
<?php
      
endforeach;
      
/********** จบ LOOP แสดงกระทู้ **********/
      
?>
    </tbody>
  </table>
  
<?php
  
/*
  หากจำนวนหน้ามากกว่า 1 เราจะสร้าง pagination
  */
  
if ($NUM_PAGES 1):
  
?>
  <div class="panel-footer text-center">
    <ul class="pagination">
      
<?php
      
/********** เริ่ม LOOP แสดงหน้าของ pagination **********/
      /*
      โดยให้ $i เริ่มจาก 1 ไปถึงจำนวนหน้าซึ่งคือ $NUM_PAGES
      และหาก $i เท่ากับ $PAGE ที่เป็นหมายเลขหน้าปัจจุบัน
      เราก็จะเพิ่ม class 'active' เข้าไปใน <li> เพื่อให้เน้นว่าเป็นหน้าปัจจุบัน
      ใน href ของ <a> จะกำหนด query string ได้แก่
      page เพื่อส่งต่อไปเป็นค่าใน array $_GET ใน index.php
      ซึ่งค่าเหล่านี้จะทำให้ index.php รู้ว่าจะต้อง SELECT ข้อมูลจากตาราง topic
      โดยเริ่มจาก offset ใด
      */
      
for ($page 1$page <= $NUM_PAGES; ++$page):
      
?>
      <li class="
<?php
      
if ($page === $PAGE) {
        
echo 'active';
      }
      
?>">
        <a href="index.php?page=
<?php echo $page?>">
          
<?php echo $page?>
        </a>
      </li>
      
<?php
      
endfor;
      
/********** จบ LOOP แสดงหน้าของ pagination **********/
      
?>
    </ul>
  </div>
  
<?php
  
endif;
  
?>
</div>


inc/post.inc.php

<?php
/********** เริ่ม FORM ตั้งกระทู้ใหม่ **********/
/*
โดย form นี้จะใช้ method POST ในการส่งข้อมูลไปยัง post.php
ข้อมูลที่จะส่งให้กับ post.php ก็ได้แก่
title เป็น input type=text
description เป็น textarea
และ name เป็น input type=text
*/
?>
<form action="post.php" method="post" class="form-horizontal panel panel-default">
  <div class="panel-heading">
    <h4>
      <span class="glyphicon glyphicon-pencil"></span>
      ตั้งกระทู้ใหม่
    </h4>
  </div>
  <div class="panel-body">
    
<?php
    
/*
    แสดง errors (ถ้ามี)
    ดูคำอธิบายใน inc/form_errors.inc.php
    */
    
require 'inc/form_errors.inc.php';
    
?>
    <div class="form-group 
<?php
    
/*
    ถ้ามี key ชื่อ 'title' อยู่ใน array $FORM_ERRORS
    ให้เพิ่ม class 'has-error' เข้าไปใน <div> นี้
    */
    
if (isset($FORM_ERRORS['title'])) {
      
echo 'has-error';
    }
    
?>">
      <label for="titleInput" class="col-sm-2 control-label">*หัวข้อ</label>
      <div class="col-sm-10">
        <input
          type="text"
          id="titleInput"
          name="title"
          value="
<?php
          
echo htmlspecialchars($DATA['title'], ENT_QUOTES'UTF-8');
          
?>"
          placeholder="หัวข้อ"
          spellcheck="false"
          class="form-control"
        >
      </div>
    </div>
    <div class="form-group 
<?php
    
/*
    ถ้ามี key ชื่อ 'description' อยู่ใน array $FORM_ERRORS
    ให้เพิ่ม class 'has-error' เข้าไปใน <div> นี้
    */
    
if (isset($FORM_ERRORS['description'])) {
      
echo 'has-error';
    }
    
?>">
      <label for="descriptionTextarea" class="col-sm-2 control-label">*รายละเอียด</label>
      <div class="col-sm-10">
        <textarea
          id="descriptionTextarea"
          name="description"
          rows="10"
          placeholder="รายละเอียด"
          spellcheck="false"
          class="form-control"
        >
<?php
        
echo htmlspecialchars($DATA['description'], ENT_QUOTES'UTF-8');
        
?></textarea>
      </div>
    </div>
    <div class="form-group 
<?php
    
/*
    ถ้ามี key ชื่อ 'name' อยู่ใน array $FORM_ERRORS
    ให้เพิ่ม class 'has-error' เข้าไปใน <div> นี้
    */
    
if (isset($FORM_ERRORS['name'])) {
      
echo 'has-error';
    }
    
?>">
      <label for="nameInput" class="col-sm-2 control-label">*ชื่อ</label>
      <div class="col-sm-4">
        <input
          type="text"
          id="nameInput"
          name="name"
          value="
<?php
          
echo htmlspecialchars($DATA['name'], ENT_QUOTES'UTF-8');
          
?>"
          placeholder="ชื่อ"
          spellcheck="false"
          class="form-control"
        >
      </div>
    </div>
    <hr>
    <div class="form-group">
      <div class="col-sm-4 col-sm-offset-4">
        <button type="submit" class="btn btn-primary btn-block">
          ตั้งกระทู้
        </button>
      </div>
    </div>
  </div>
</form>
<?php
/********** จบ FORM แสดงความเห็น **********/



inc/view.inc.php

<?php
/********** เริ่ม LOOP แสดงกระทู้และความเห็น **********/
/*
เนื่องจากเราได้รวมกระทู้และความเห็นเข้าเป็น array เดียวกัน (ดู view.php)
จึงทำให้ไม่ต้องเขียนโค้ด HTML ซ้ำซ้อน 
เพียงแค่ตรวจว่า $no เป็น 0 หรือไม่ ก็จะทราบว่า $item นั้นเป็นกระทู้
*/
foreach ($ITEMS as $no => $item):
  
/*
  กำหนดตัวแปรเพื่อบอกว่า $item ปัจจุบันเป็นกระทู้หรือความเห็น
  */
  
$isTopic $no === 0;
  
/*
  ตรวจเงื่อนไขว่า $no เป็น 1 หรือไม่ หากมันเป็น 1 แสดงว่าขณะนี้ $item คือความเห็นแรก
  ดังนั้นเราจะสร้าง <div id="comments"></div> เพื่อให้ browser scroll มายังจุดนี้
  เมื่อมี hash tag #comments อยู่ใน URL
  */
  
if ($no === 1):
  
?>
    <div id="comments"></div>
    
<?php
    
/*
    หากจำนวนหน้ามากกว่า 1 เราจะสร้าง pagination
    */
    
if ($NUM_PAGES 1):
      
/*
      เนื่องจากเราจะแสดง pagination ทั้งก่อนแสดงความเห็น และหลังแสดงความเห็น
      จึงใช้ ob_start() เพื่อเก็บ output ที่ได้ไปใช้ในภายหลัง โดยไม่ต้องเขียนโค้ดซ้ำซ้อน
      */
      
ob_start();
      
?>
      <div class="text-center">
        <ul class="pagination">
          
<?php
          
/********** เริ่ม LOOP แสดงหน้าของ pagination **********/
          /*
          โดยให้ $i เริ่มจาก 1 ไปถึงจำนวนหน้าซึ่งคือ $NUM_PAGES
          และหาก $i เท่ากับ $PAGE ที่เป็นหมายเลขหน้าปัจจุบัน
          เราก็จะเพิ่ม class 'active' เข้าไปใน <li> เพื่อให้เน้นว่าเป็นหน้าปัจจุบัน
          ใน href ของ <a> จะกำหนด query string ได้แก่
          topic_id และ page เพื่อส่งต่อไปเป็นค่าใน array $_GET ใน view.php
          ซึ่งค่าเหล่านี้จะทำให้ view.php รู้ว่าจะแสดงกระทู้ id อะไร
          และรู้ว่าจะต้อง SELECT ข้อมูลจากตาราง comment โดยเริ่มจาก offset ใด
          และกำหนด hash tag #comments เพื่อให้ browser scroll ไปที่ความเห็นแรก
          */
          
for ($i 1$i <= $NUM_PAGES; ++$i):
          
?>
          <li class="
<?php if ($i === $PAGE) { echo 'active'; } ?>">
            <a href="view.php?topic_id=
<?php
            
echo $TOPIC_ID;
            
?>&page=<?php
            
echo $i;
            
?>#comments">
              
<?php echo $i?>
            </a>
          </li>
          
<?php
          
endfor;
          
/********** จบ LOOP แสดงหน้าของ pagination **********/
          
?>
        </ul>
      </div>
      
<?php
      
/*
      เก็บ output ที่อยู่ใน output buffer หมดหลังเรียกใช้ ob_start()
      เข้ามาไว้ในตัวแปร $PAGINATION พร้อมกับแสดงผล และปิด output buffering
      */
      
$PAGINATION ob_get_flush();
    
endif;
  
endif;
  
?>
  <div id="
<?php
  
/*
  หาก $item ไม่ใช่กระทู้
  กำหนด id ให้กับ <div> เพื่อให้ browser scroll มายังจุดนี้
  เมื่อมี hash tag #comment-<id ของความเห็น> อยู่ใน URL
  */
  
if (!$isTopic) {
    
echo "comment-{$item['id']}";
  }
  
?>" class="panel <?php
  
/*
  หาก $item เป็นกระทู้ ให้ใช้ class 'panel-info' เพื่อเน้นสีว่าเป็นกระทู้
  นอกนั้นใช้ 'panel-default'
  */
  
echo $isTopic
    
'panel-info'
    
'panel-default';
  
?>">
    
<?php
    
/*
    หาก $item เป็นกระทู้ เราจะแสดงหัวข้อกระทู้ด้วย
    */
    
if ($isTopic):
    
?>
    <div class="panel-heading">
      <h4>
        <span class="badge pull-right">
          <span class="glyphicon glyphicon-eye-open"></span>&nbsp;
<?php
          
/*
          แสดงจำนวนการเข้าชมและจำนวนความเห็น
          โดยใช้ number_format() เพื่อใส่ , เข้าไปในตัวเลขให้ดูสวยงาม
          เช่น 1234 จะเป็น 1,234
          */
          
echo number_format($item['num_views']);
          
?>&nbsp;&nbsp;<span class="glyphicon glyphicon-comment"></span>&nbsp;<?php
          
echo number_format($item['num_comments']);
          
?>
        </span>
        
<?php
        
echo htmlspecialchars($item['title'], ENT_QUOTES'UTF-8');
        
?>
      </h4>
    </div>
    
<?php
    
/*
    หาก $item เป็นความเห็น
    */
    
else:
    
?>
    <span class="badge">
    
<?php
    
/*
    แสดงเลขลำดับความเห็น
    */
    
echo $START_OFFSET $no;
    
?>
    </span>
    
<?php
    
endif;
    
?>
    <div class="panel-body">
      
<?php
      
/*
      แสดงรายละเอียดของกระทู้หรือข้อความของความเห็น
      โดยใช้ nl2br() เพื่อเปลี่ยน newline (\n) ให้เป็น tag <br>
      */
      
echo nl2br(htmlspecialchars($item['description'], ENT_QUOTES'UTF-8'));
      
?>
    </div>
    <div class="panel-footer">
      <small class="text-muted">โดย:</small>
      <strong class="text-info">
<?php
      
/*
      แสดงชื่อ
      */
      
echo htmlspecialchars($item['name'], ENT_QUOTES'UTF-8');
      
?></strong>
      <small class="text-muted">เมื่อ:</small>
      <span class="text-info" title="
<?php
      
/*
      แปลงวันที่ให้เป็นภาษาไทยด้วยฟังก์ชั่น thai_datetime() ที่กำหนดไว้ใน inc/main.inc.php
      โดยใส่ไว้ใน attribute title เพื่อให้แสดงขึ้นมาเมื่อผู้ใช้เอาเมาส์ไปชี้
      */
      
echo thai_datetime($item['created']);
      
?>"><?php
      
/*
      แปลงวันที่ให้เป็นช่วงห่างของเวลาภาษาไทยด้วยฟังก์ชั่น thai_time()
      ที่กำหนดไว้ใน inc/main.inc.php
      */
      
echo thai_time($item['created']);
      
?></span>
      <small class="text-muted">จาก:</small>
      <span class="text-warning">
<?php
      
/*
      แสดง IP
      */
      
echo $item['ip'];
      
?></span>
    </div>
  </div>
<?php
endforeach;
/********** จบ LOOP แสดงกระทู้และความเห็น **********/
/*
ตรวจสอบว่ามีตัวแปร $PAGINATION ถูกกำหนดค่าไว้หรือไม่
ถ้ามี ก็ให้แสดงผล pagination อีกครั้ง
*/
if (isset($PAGINATION)) {
  
echo $PAGINATION;
}
/********** เริ่ม FORM แสดงความเห็น **********/
/*
โดย form นี้จะใช้ method POST ในการส่งข้อมูลไปยัง view.php
จะเห็นว่าใน action ของ form มี hash tag #comment-form อยู่ด้วย
ใช้เพื่อให้ browser scroll มาจุดนี้เมื่อมี error เกิดขึ้น เช่น เมื่อไม่ได้ระบุ 'ชื่อ'
ข้อมูลที่จะส่งให้กับ view.php ก็ได้แก่
topic_id เป็น hidden input ซึ่งจะไม่แสดงผลให้ผู้ใช้เห็น
description เป็น textarea
และ name เป็น input type=text
*/
?>
<form action="view.php#comment-form" method="post" id="comment-form" class="form-horizontal panel panel-default">
  <input type="hidden" name="topic_id" value="
<?php echo $TOPIC_ID?>">
  <div class="panel-heading">
    <h4>
      <span class="glyphicon glyphicon-comment"></span>
      แสดงความเห็น
    </h4>
  </div>
  <div class="panel-body">
    
<?php
    
/*
    แสดง errors (ถ้ามี)
    ดูคำอธิบายใน inc/form_errors.inc.php
    */
    
require 'inc/form_errors.inc.php';
    
?>
    <div class="form-group 
<?php
    
/*
    ถ้ามี key ชื่อ 'description' อยู่ใน array $FORM_ERRORS
    ให้เพิ่ม class 'has-error' เข้าไปใน <div> นี้
    */
    
if (isset($FORM_ERRORS['description'])) {
      
echo 'has-error';
    }
    
?>">
      <label for="descriptionTextarea" class="col-sm-2 control-label">*ข้อความ</label>
      <div class="col-sm-10">
        <textarea
          id="descriptionTextarea"
          name="description"
          rows="10"
          placeholder="ข้อความ"
          spellcheck="false"
          class="form-control"
        >
<?php
        
echo htmlspecialchars($DATA['description'], ENT_QUOTES'UTF-8');
        
?></textarea>
      </div>
    </div>
    <div class="form-group 
<?php
    
/*
    ถ้ามี key ชื่อ 'name' อยู่ใน array $FORM_ERRORS
    ให้เพิ่ม class 'has-error' เข้าไปใน <div> นี้
    */
    
if (isset($FORM_ERRORS['name'])) {
      
echo 'has-error';
    }
    
?>">
      <label for="nameInput" class="col-sm-2 control-label">*ชื่อ</label>
      <div class="col-sm-4">
        <input
          type="text"
          id="nameInput"
          name="name"
          value="
<?php
          
echo htmlspecialchars($DATA['name'], ENT_QUOTES'UTF-8');
          
?>"
          placeholder="ชื่อ"
          spellcheck="false"
          class="form-control"
        >
      </div>
    </div>
    <hr>
    <div class="form-group">
      <div class="col-sm-4 col-sm-offset-4">
        <button type="submit" class="btn btn-primary btn-block">
          แสดงความเห็น
        </button>
      </div>
    </div>
  </div>
</form>
<?php
/********** จบ FORM แสดงความเห็น **********/



inc/form_errors.inc.php

<?php
/********** เริ่มการแสดงผลข้อผิดพลาดที่เกิดจากความไม่ถูกต้องของข้อมูลจาก FORM **********/
/*
โดยตรวจสอบจากตัวแปร $FORM_ERRORS ว่าได้ถูกกำหนดค่าไว้แล้วหรือยัง
ซึ่งตัวแปร $FORM_ERRORS นี้จะมีชนิดเป็น array
ประกอบไปด้วย key ของฟิลด์ที่เกิดความผิดพลาด
และมี value เป็น error message ที่เราได้กำหนดไว้ก่อนหน้านี้ (ใน post.php และ view.php)
*/
if (isset($FORM_ERRORS)):
?>
<div class="alert alert-danger">
  <ul class="list-unstyled">
    
<?php
    
/********** เริ่ม LOOP แสดงผล errors **********/
    /*
    วนลูปทุก value ใน array $FORM_ERRORS
    โดยกำหนดแต่ละ value ให้อยู่ในตัวแปร $message
    */
    
foreach ($FORM_ERRORS as $message):
    
?>
    <li>
      <span class="glyphicon glyphicon-exclamation-sign"></span>
      
<?php
      
echo htmlspecialchars($messageENT_QUOTES'UTF-8');
      
?>
    </li>
    
<?php
    
endforeach;
    
/********** จบ LOOP แสดงผล errors **********/
    
?>
  </ul>
</div>
<?php
endif;
/********** จบการแสดงผลข้อผิดพลาดที่เกิดจากความไม่ถูกต้องของข้อมูลจาก FORM **********/








   
Share
Bookmark.   

  By : phpinfo()
  Article : บทความเป็นการเขียนโดยสมาชิก หากมีปัญหาเรื่องลิขสิทธิ์ กรุณาแจ้งให้ทาง webmaster ทราบด้วยครับ
  Score Rating :
  Create Date : 2014-12-12
  Download : No files
Sponsored Links
ThaiCreate.Com Forum


Comunity Forum Free Web Script
Jobs Freelance Free Uploads
Free Web Hosting Free Tools

สอน PHP ผ่าน Youtube ฟรี
สอน Android การเขียนโปรแกรม Android
สอน Windows Phone การเขียนโปรแกรม Windows Phone 7 และ 8
สอน iOS การเขียนโปรแกรม iPhone, iPad
สอน Java การเขียนโปรแกรม ภาษา Java
สอน Java GUI การเขียนโปรแกรม ภาษา Java GUI
สอน JSP การเขียนโปรแกรม ภาษา Java
สอน jQuery การเขียนโปรแกรม ภาษา jQuery
สอน .Net การเขียนโปรแกรม ภาษา .Net
Free Tutorial
สอน Google Maps Api
สอน Windows Service
สอน Entity Framework
สอน Android
สอน Java เขียน Java
Java GUI Swing
สอน JSP (Web App)
iOS (iPhone,iPad)
Windows Phone
Windows Azure
Windows Store
Laravel Framework
Yii PHP Framework
สอน jQuery
สอน jQuery กับ Ajax
สอน PHP OOP (Vdo)
Ajax Tutorials
SQL Tutorials
สอน SQL (Part 2)
JavaScript Tutorial
Javascript Tips
VBScript Tutorial
VBScript Validation
Microsoft Access
MySQL Tutorials
-- Stored Procedure
MariaDB Database
SQL Server Tutorial
SQL Server 2005
SQL Server 2008
SQL Server 2012
-- Stored Procedure
Oracle Database
-- Stored Procedure
SVN (Subversion)
แนวทางการทำ SEO
ปรับแต่งเว็บให้โหลดเร็ว


Hit Link
   







Load balance : Server 05
ThaiCreate.Com Logo
© www.ThaiCreate.Com. 2003-2024 All Rights Reserved.
ไทยครีเอทบริการ จัดทำดูแลแก้ไข Web Application ทุกรูปแบบ (PHP, .Net Application, VB.Net, C#)
[Conditions Privacy Statement] ติดต่อโฆษณา 081-987-6107 อัตราราคา คลิกที่นี่