ผมสร้างระบบ Login และ CRUD แบบง่าย ๆ ด้วย PHP + Bootstrap 5 (โปรเจกต์เริ่มต้นสำหรับมือใหม่)

โดย CyberMAN



PHP PROJECT WALKTHROUGH

ผมสร้างระบบ Login และ CRUD แบบง่าย ๆ ด้วย PHP + Bootstrap 5
(โปรเจกต์เริ่มต้นสำหรับมือใหม่)

ก่อนจะกระโดดไปเรียน CodeIgniter 4 หรือ Laravel แบบเต็มตัว บทความนี้พาทำโปรเจกต์ Login + CRUD ด้วย PHP ล้วน ๆ ให้เห็นภาพว่า Framework ช่วยอะไรเราบ้าง พร้อมแก้จุดอ่อนด้านความปลอดภัยที่บทเรียนทั่วไปมักมองข้าม

อ่านประมาณ 9 นาที🧩ระดับ: เริ่มต้น🛠PHP + MySQLi + Bootstrap 5

หลายคนเริ่มเรียน PHP จากการทำระบบ Login และ CRUD (Create, Read, Update, Delete) เป็นโปรเจกต์แรก เพราะมันคือแกนกลางของเว็บแอปแทบทุกประเภท ตั้งแต่ระบบจัดการนักเรียน ระบบสมาชิกร้านค้า ไปจนถึง Backoffice ขององค์กร ในบทความนี้เราจะสร้างระบบนี้ด้วย PHP ดิบ ๆ (Vanilla PHP) ร่วมกับ Bootstrap 5 ตั้งแต่ศูนย์ และที่สำคัญคือเราจะเขียนให้ ปลอดภัยกว่าตัวอย่างทั่วไปที่หาอ่านได้ตามเว็บ เพราะ Tutorial ส่วนใหญ่มักสอนแบบ query ตรง ๆ ที่เปิดช่องให้เกิด SQL Injection และเก็บรหัสผ่านแบบไม่เข้ารหัส ซึ่งเป็นนิสัยที่ไม่ควรติดไปใช้ในงานจริง

เป้าหมายของบทความนี้ไม่ใช่แค่ "ทำให้รัน" แต่อยากให้มือใหม่เห็นว่าการเขียน PHP แบบดิบต้องดูแลเรื่องอะไรเองบ้าง เมื่อเทียบกับตอนที่เราย้ายไปใช้ CodeIgniter 4 หรือ Laravel ที่มีโครงสร้างพวกนี้เตรียมไว้ให้แล้ว

01สิ่งที่ต้องเตรียมก่อนเริ่ม

  • Local server: XAMPP หรือ Laragon (มี Apache + MySQL + PHP ในตัว)
  • พื้นฐาน PHP: ตัวแปร, ฟังก์ชัน, if/else, array เบื้องต้น
  • พื้นฐาน SQL: CREATE TABLE, SELECT, INSERT, UPDATE, DELETE
  • Bootstrap 5: ใช้ผ่าน CDN ไม่ต้องติดตั้งอะไรเพิ่ม

02สร้างฐานข้อมูลและไฟล์เชื่อมต่อ

เริ่มจากสร้างฐานข้อมูลชื่อ pcm_demo และตาราง users สำหรับ Login กับตาราง students สำหรับฝึก CRUD

schema.sql
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(50) NOT NULL UNIQUE,
  password VARCHAR(255) NOT NULL
);

CREATE TABLE students (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  class VARCHAR(20) NOT NULL,
  marks INT NOT NULL
);

จากนั้นสร้างไฟล์เชื่อมต่อฐานข้อมูล โดยใช้ MySQLi แบบ Object-Oriented และเปิด exception mode ไว้ เพื่อให้ error แสดงชัดเจนตอนพัฒนา

config/db.php
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'pcm_demo');

try {
    $conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
    $conn->set_charset('utf8mb4');
} catch (mysqli_sql_exception $e) {
    die('Connection failed: ' . $e->getMessage());
}
💡 ทำไมต้อง mysqli_reportการเปิด MYSQLI_REPORT_STRICT ทำให้ทุก query ที่ผิดพลาดโยน Exception ออกมาทันที แทนที่จะปล่อยให้ query ล้มเหลวเงียบ ๆ ซึ่งช่วยจับบั๊กได้เร็วกว่าการเช็ค if ($result === false) ทุกจุด

03ระบบ Login ที่ปลอดภัยด้วย password_hash()

จุดที่ Tutorial ทั่วไปมักพลาดคือเก็บรหัสผ่านเป็น plain text หรือเทียบรหัสผ่านด้วย == ตรง ๆ ในที่นี้เราจะใช้ password_hash() ตอนสมัครสมาชิก และ password_verify() ตอน Login พร้อม Prepared Statement เพื่อกัน SQL Injection

register.php
<?php
require 'config/db.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username']);
    $hashed   = password_hash($_POST['password'], PASSWORD_DEFAULT);

    $stmt = $conn->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
    $stmt->bind_param("ss", $username, $hashed);
    $stmt->execute();

    header('Location: login.php?registered=1');
    exit;
}
login.php
<?php
session_start();
require 'config/db.php';
$error = null;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username']);
    $password = $_POST['password'];

    $stmt = $conn->prepare("SELECT id, password FROM users WHERE username = ?");
    $stmt->bind_param("s", $username);
    $stmt->execute();
    $result = $stmt->get_result();
    $user   = $result->fetch_assoc();

    if ($user && password_verify($password, $user['password'])) {
        session_regenerate_id(true);
        $_SESSION['user_id']  = $user['id'];
        $_SESSION['username'] = $username;
        header('Location: students.php');
        exit;
    }
    $error = 'Username หรือ Password ไม่ถูกต้อง';
}
⚠️ จุดที่มือใหม่พลาดบ่อยการเทียบรหัสผ่านด้วย $password == $user['password'] ใช้ไม่ได้กับ hash เพราะ password_hash() สร้าง salt แบบสุ่มทุกครั้ง ต้องใช้ password_verify() เท่านั้น และห้าม query แบบเอา $_POST ไปต่อ string SQL ตรง ๆ เด็ดขาด เพราะนั่นคือช่องโหว่ SQL Injection แบบคลาสสิก

ส่วน Form ฝั่ง HTML ใช้ Bootstrap 5 แบบมาตรฐาน เช่น

login.php (HTML part)
<div class="container mt-5" style="max-width:400px">
  <h3 class="mb-4">เข้าสู่ระบบ</h3>
  <?php if ($error): ?>
    <div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
  <?php endif; ?>
  <form method="post">
    <input name="username" class="form-control mb-3" placeholder="Username" required>
    <input type="password" name="password" class="form-control mb-3" placeholder="Password" required>
    <button class="btn btn-primary w-100">เข้าสู่ระบบ</button>
  </form>
</div>

04CRUD นักเรียนด้วย Bootstrap 5

หลัง Login สำเร็จ เราจะพาไปหน้า students.php ที่แสดงตารางข้อมูล พร้อมปุ่มเพิ่ม/แก้ไข/ลบ ทุกหน้าต้องเช็ค session ก่อนเสมอ ไม่ใช่แค่ซ่อนปุ่มเฉย ๆ

students.php (Read)
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
    header('Location: login.php');
    exit;
}
require 'config/db.php';
$students = $conn->query("SELECT * FROM students ORDER BY id DESC");
?>
<table class="table table-striped">
  <tr><th>ชื่อ</th><th>ห้อง</th><th>คะแนน</th><th></th></tr>
  <?php while ($row = $students->fetch_assoc()): ?>
  <tr>
    <td><?= htmlspecialchars($row['name']) ?></td>
    <td><?= htmlspecialchars($row['class']) ?></td>
    <td><?= $row['marks'] ?></td>
    <td>
      <a href="edit.php?id=<?= $row['id'] ?>" class="btn btn-sm btn-warning">แก้ไข</a>
      <a href="delete.php?id=<?= $row['id'] ?>" class="btn btn-sm btn-danger"
         onclick="return confirm('ลบรายการนี้?')">ลบ</a>
    </td>
  </tr>
  <?php endwhile; ?>
</table>
delete.php (Delete)
<?php
session_start();
if (!isset($_SESSION['user_id'])) { header('Location: login.php'); exit; }
require 'config/db.php';

$id = (int) ($_GET['id'] ?? 0);
$stmt = $conn->prepare("DELETE FROM students WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();

header('Location: students.php');
exit;

ส่วน add.php และ edit.php ก็ใช้แพทเทิร์นเดียวกัน คือ เช็ค session → bind_param ทุก query → redirect หลังบันทึก ทำซ้ำแบบนี้ใน 4 ไฟล์ (add, edit, delete, login) คือสิ่งที่มือใหม่จะเริ่มรู้สึก "เขียนซ้ำเยอะมาก" และนั่นคือจุดเริ่มต้นที่ทำให้เราอยากมี Framework เข้ามาช่วย

05เขียนเองทั้งหมด vs ใช้ Framework

พอลองสร้างระบบ Login + CRUD ด้วย PHP ดิบจนจบ จะเห็นภาพชัดว่าอะไรที่เราต้อง "คิดเอง ทำเอง" ทั้งหมด ลองเทียบให้เห็นภาพกับ CodeIgniter 4 และ Laravel

หัวข้อPHP ดิบ (Vanilla)CodeIgniter 4Laravel
ป้องกัน SQL Injectionต้องเขียน prepared statement เองQuery Builder ป้องกันให้อัตโนมัติEloquent ป้องกันให้อัตโนมัติ
เก็บรหัสผ่านต้องเรียก password_hash() เองมี password helper ในตัวHash facade + bcrypt อัตโนมัติ
โครงสร้างไฟล์ปนกันหมดในไฟล์เดียวMVC แยกชัดเจนMVC + Service/Repository ได้
Validation ฟอร์มเขียน if/else เองทั้งหมดValidation library ในตัวForm Request Validation
ความเร็วในการเริ่มโปรเจกต์เล็กเร็วที่สุด ไม่ต้องตั้งค่าอะไรต้องตั้งค่าเริ่มต้นเล็กน้อยตั้งค่าเริ่มต้นมากกว่า CI4
เหมาะกับโปรเจกต์ใหญ่ขึ้นดูแลยากเมื่อโค้ดเยอะรองรับงานระดับองค์กรได้ดีรองรับงานใหญ่ ระบบซับซ้อนได้ดี

สรุปง่าย ๆ คือ PHP ดิบเหมาะกับการ เข้าใจกลไกเบื้องหลัง ว่า Login ทำงานยังไง CRUD เชื่อมฐานข้อมูลยังไง แต่พอจะทำงานจริงหรือโปรเจกต์ที่มีหลายหน้า หลายฟีเจอร์ Framework อย่าง CodeIgniter 4 หรือ Laravel จะช่วยลดงานซ้ำซ้อนและลดโอกาสพลาดเรื่องความปลอดภัยได้มาก


06สรุป

โปรเจกต์ Login + CRUD ด้วย PHP + Bootstrap 5 เป็นจุดเริ่มต้นที่ดีมากสำหรับมือใหม่ เพราะครอบคลุมทักษะหลักที่ใช้ซ้ำในแทบทุกเว็บแอป ทั้งการเชื่อมต่อฐานข้อมูล การยืนยันตัวตน และการจัดการข้อมูลพื้นฐาน แต่สิ่งสำคัญที่อยากให้จำไว้คือ อย่าก๊อปโค้ดจาก Tutorial ที่ไม่มี prepared statement หรือไม่ hash รหัสผ่าน เพราะนิสัยตอนเริ่มต้นมักติดตัวไปจนถึงงานจริง



PHP CI MANIA - PHP Code Generator 

โปรแกรมช่วยสร้างโค้ด "ลดเวลาการเขียนโปรแกรม"
ราคาสุดคุ้ม  
http://www.phpcodemania.com

Resumable Upload ด้วย tus Protocol: อัปโหลดไฟล์ใหญ่ เน็ตหลุดก็ไม่ต้องเริ่มใหม่

โดย CyberMAN


PHP · CODEIGNITER 4 · LARAVEL

Resumable Upload ด้วย tus Protocol: อัปโหลดไฟล์ใหญ่ เน็ตหลุดก็ไม่ต้องเริ่มใหม่

เคยอัปโหลดไฟล์ 2 GB ค้างอยู่ที่ 90% แล้วเน็ตหลุดไหมครับ? บทความนี้พาไปรู้จัก tus protocol และไลบรารี tus-php ที่ทำให้การอัปโหลดไฟล์ใหญ่ "หยุดแล้วต่อ" ได้จริง พร้อมตัวอย่างโค้ดสำหรับ CodeIgniter 4 และ Laravel

อัปโหลดสำเร็จแล้วหลุดกลางทาง แต่ resume ต่อจากจุดเดิมได้รอคิวอัปโหลด

01ทำไมการอัปโหลดไฟล์ใหญ่ถึง "พัง" ได้ง่าย

ลองนึกภาพผู้ใช้กำลังอัปโหลดวิดีโอขนาด 1.5 GB ผ่านฟอร์ม HTML ธรรมดา ใช้เวลาไปแล้วกว่าครึ่งชั่วโมง แถบโปรเกรสขยับมาถึง 92% แล้วจู่ ๆ Wi-Fi ก็หลุด หรือเบราว์เซอร์แครช ผลลัพธ์คือไฟล์ที่อัปโหลดไปทั้งหมดสูญเปล่า ผู้ใช้ต้องกดอัปโหลดใหม่ตั้งแต่ไบต์แรก

ปัญหานี้เกิดจากธรรมชาติของการอัปโหลดแบบ multipart/form-data ทั่วไป ซึ่งเซิร์ฟเวอร์มองไฟล์ทั้งก้อนเป็นคำขอเดียว ถ้าคำขอนั้นไม่จบสมบูรณ์ เซิร์ฟเวอร์จะไม่รู้เลยว่าไบต์ไหนมาถึงแล้วบ้าง และต้องทิ้งข้อมูลทั้งหมดไป ยิ่งผู้ใช้อยู่ในพื้นที่ที่สัญญาณอินเทอร์เน็ตไม่เสถียร เช่น ใช้เน็ตมือถือระหว่างเดินทาง ปัญหานี้ก็ยิ่งเกิดถี่ขึ้น

ทางออกที่หลายระบบใหญ่ ๆ อย่าง Vimeo หรือ Google Drive ใช้กันคือการอัปโหลดแบบ resumable นั่นคือแบ่งไฟล์เป็นชิ้นเล็ก ๆ (chunk) ส่งทีละชิ้น แล้วให้เซิร์ฟเวอร์จดจำว่าไบต์ล่าสุดที่ได้รับคือไบต์ที่เท่าไร เมื่อการเชื่อมต่อกลับมา ฝั่งไคลเอนต์ก็แค่ถามเซิร์ฟเวอร์ว่า "ตอนนี้ได้รับไปถึงไหนแล้ว" แล้วส่งต่อจากจุดนั้น ไม่ต้องเริ่มนับหนึ่งใหม่

02รู้จัก tus: มาตรฐานเปิดสำหรับ Resumable Upload

tus เป็นโปรโตคอลแบบเปิดที่ถูกออกแบบมาเฉพาะสำหรับการอัปโหลดไฟล์แบบ resumable บน HTTP จุดเด่นของ tus คือมันกำหนดมาตรฐานชัดเจนว่าไคลเอนต์กับเซิร์ฟเวอร์ควรคุยกันอย่างไร ทำให้นักพัฒนาไม่ต้องคิดค้น protocol ของตัวเองตั้งแต่ศูนย์ และมีไลบรารีรองรับในหลายภาษา ทั้งฝั่ง server (PHP, Node.js, Go) และฝั่ง client (JavaScript, iOS, Android)

สำหรับฝั่ง PHP มีไลบรารีชื่อ ankitpokhrel/tus-php ซึ่งเป็น implementation ของ tus server แบบ framework-agnostic หมายความว่าเอาไปต่อกับ CodeIgniter, Laravel หรือ PHP ล้วน ๆ ก็ได้ทั้งนั้น

03tus ทำงานอย่างไรเบื้องหลัง

หัวใจของ tus คือแนวคิดเรื่อง offset หรือ "ตำแหน่งไบต์ล่าสุดที่อัปโหลดสำเร็จ" ขั้นตอนคร่าว ๆ มีดังนี้

  1. ไคลเอนต์ส่งคำขอ POST ไปยังเซิร์ฟเวอร์เพื่อ "จอง" การอัปโหลดใหม่ พร้อมแจ้งขนาดไฟล์ทั้งหมด เซิร์ฟเวอร์จะตอบกลับ URL เฉพาะสำหรับไฟล์นั้น
  2. ไคลเอนต์ทยอยส่งข้อมูลเป็นชิ้น ๆ ด้วยคำขอ PATCH ไปยัง URL นั้น พร้อมระบุ offset ปัจจุบันในเฮดเดอร์
  3. ถ้าการเชื่อมต่อขาดระหว่างทาง ไคลเอนต์สามารถส่งคำขอ HEAD ไปถาม URL เดิมเพื่อเช็กว่าตอนนี้เซิร์ฟเวอร์มีข้อมูลถึงไบต์ไหนแล้ว
  4. จากนั้นไคลเอนต์ส่ง PATCH ต่อจาก offset ที่ได้รับ โดยไม่ต้องส่งข้อมูลเดิมซ้ำเลย
จุดสำคัญ: ฝั่งเซิร์ฟเวอร์ต้องมีที่เก็บสถานะการอัปโหลด (เช่น ไฟล์ cache หรือ Redis) เพื่อจำว่าแต่ละ upload ID ไปถึงไบต์ไหนแล้ว ไลบรารี tus-php จัดการส่วนนี้ให้อัตโนมัติ

04ติดตั้งและสร้าง Server ด้วย tus-php

เริ่มจากติดตั้งไลบรารีผ่าน Composer

terminal
composer require ankitpokhrel/tus-php

โค้ดด้านล่างคือ tus server แบบมินิมอล ที่รับคำขอทุก method ของโปรโตคอล tus แล้วจัดการให้เองทั้งหมด

server.php
<?php

require __DIR__ . '/vendor/autoload.php';

use TusPhp\Tus\Server as TusServer;

// ใช้ 'file' cache สำหรับเริ่มต้น (เปลี่ยนเป็น 'redis' ได้เมื่อขึ้น production)
$server = new TusServer('file');

$server->setApiPath('/files')
       ->setUploadDir(__DIR__ . '/uploads');

$response = $server->serve();
$response->send();

exit(0);

เพียงเท่านี้ server.php ตัวนี้ก็พร้อมรับคำขอ OPTIONS, POST, HEAD, PATCH และ DELETE ตามสเปกของ tus แล้ว แต่ในงานจริงเราอยากผูกมันเข้ากับเฟรมเวิร์กที่ใช้อยู่มากกว่า ในส่วนถัดไปจะพาไปดูทั้ง CodeIgniter 4 และ Laravel

05ผูก tus-php เข้ากับ CodeIgniter 4

ใน CI4 เราสามารถสร้าง Controller ขึ้นมาตัวเดียวเพื่อรับทุกคำขอที่เกี่ยวกับการอัปโหลด แล้วส่งต่อให้ tus-php จัดการ จากนั้นแปลง response ของ tus-php กลับมาเป็น response ของ CI4

app/Controllers/UploadController.php
<?php

namespace App\Controllers;

use TusPhp\Tus\Server as TusServer;

class UploadController extends BaseController
{
    public function handle()
    {
        $server = new TusServer('file');
        $server->setApiPath('/upload/files')
               ->setUploadDir(WRITEPATH . 'uploads');

        $response = $server->serve();

        // ส่ง response ที่ tus-php สร้างไว้กลับไปตรง ๆ
        return $this->response
            ->setStatusCode($response->getStatusCode())
            ->setBody($response->getContent());
    }
}

จากนั้นเปิดทางให้ทุก HTTP method ที่ tus ต้องใช้วิ่งเข้ามาที่ Controller เดียวกัน ใน app/Config/Routes.php

app/Config/Routes.php
$routes->match(
    ['get', 'post', 'patch', 'head', 'options', 'delete'],
    'upload/files(.*)',
    'UploadController::handle'
);

เก็บไฟล์ไว้ใต้ WRITEPATH ตามธรรมเนียมของ CI4 ก็ปลอดภัยกว่า เพราะโฟลเดอร์นี้อยู่นอก document root และคุมสิทธิ์การเข้าถึงได้ง่ายกว่าโฟลเดอร์ใน public/

06ผูก tus-php เข้ากับ Laravel

แนวคิดเดียวกันสามารถนำไปใช้กับ Laravel ได้โดยประกาศ route แบบ closure ที่ดักทุก method และทุก path ย่อยภายใต้ /files

routes/web.php
use TusPhp\Tus\Server as TusServer;
use Illuminate\Support\Facades\Route;

Route::match(
    ['get', 'post', 'patch', 'head', 'options', 'delete'],
    '/files/{any?}',
    function () {
        $server = new TusServer('file');
        $server->setApiPath('/files')
               ->setUploadDir(storage_path('app/uploads'));

        $response = $server->serve();

        return response($response->getContent(), $response->getStatusCode())
            ->withHeaders($response->headers->all());
    }
)->where('any', '.*');

ข้อดีของการใช้ storage_path('app/uploads') คือไฟล์ที่กำลังอัปโหลดอยู่จะไม่ถูกเข้าถึงโดยตรงจากเว็บ ต้องผ่าน Controller หรือ route ที่เรากำหนดเองเท่านั้น ตรงกับแนวทาง security ที่ Laravel แนะนำสำหรับไฟล์ที่ยังไม่พร้อมเผยแพร่

07ฝั่ง Client: อัปโหลดแบบ Resumable ด้วย tus-js-client

ฝั่งหน้าเว็บ เราไม่จำเป็นต้องเขียน logic เรื่อง offset หรือ retry เองเลย เพราะมีไลบรารี tus-js-client ที่คุยกับ tus server ตามสเปกให้ครบแล้ว สิ่งที่เราต้องทำคือสร้าง instance ของ tus.Upload แล้วกำหนด endpoint กับ callback ต่าง ๆ

upload-form.html
<input type="file" id="fileInput" />
<progress id="uploadProgress" value="0" max="100"></progress>
<button id="pauseBtn">Pause</button>
upload.js
import * as tus from "tus-js-client";

let upload;

document.getElementById("fileInput").addEventListener("change", (e) => {
  const file = e.target.files[0];

  upload = new tus.Upload(file, {
    endpoint: "/upload/files",
    retryDelays: [0, 3000, 5000, 10000],
    metadata: {
      filename: file.name,
      filetype: file.type,
    },
    onError: (error) => console.error("Upload failed:", error),
    onProgress: (bytesSent, bytesTotal) => {
      const percent = ((bytesSent / bytesTotal) * 100).toFixed(1);
      document.getElementById("uploadProgress").value = percent;
    },
    onSuccess: () => {
      console.log("Upload finished:", upload.url);
    },
  });

  // ถ้ามี upload ที่ค้างไว้ก่อนหน้า ให้ resume ต่อแทนเริ่มใหม่
  upload.findPreviousUploads().then((previousUploads) => {
    if (previousUploads.length) {
      upload.resumeFromPreviousUpload(previousUploads[0]);
    }
    upload.start();
  });
});

document.getElementById("pauseBtn").addEventListener("click", () => {
  if (upload) upload.abort();
});

เมธอด findPreviousUploads() คือกุญแจสำคัญของ "resume" ฝั่งไคลเอนต์ มันจะเช็ก localStorage ของเบราว์เซอร์ว่าไฟล์นี้เคยเริ่มอัปโหลดไว้หรือยัง ถ้าเจอก็เรียก resumeFromPreviousUpload() เพื่อสานต่อจากจุดเดิมทันที โดยที่ผู้ใช้ไม่ต้องทำอะไรเพิ่มเลย แม้จะปิดแท็บแล้วเปิดใหม่ก็ตาม

08REST Endpoint ของ tus Server

เพื่อให้เห็นภาพว่าแต่ละคำขอ HTTP ทำหน้าที่อะไรบ้างในวงจรของ tus สรุปไว้ในตารางนี้

MethodEndpointหน้าที่
OPTIONS/filesถาม server ว่ารองรับฟีเจอร์อะไรบ้าง เช่น ขนาดไฟล์สูงสุด
POST/filesเริ่มอัปโหลดใหม่ แจ้งขนาดไฟล์ทั้งหมด ได้ URL เฉพาะกลับมา
HEAD/files/{id}เช็ก offset ปัจจุบันของไฟล์ที่กำลังอัปโหลดอยู่
PATCH/files/{id}ส่งข้อมูล chunk ต่อจาก offset เดิมไปเรื่อย ๆ จนครบ
DELETE/files/{id}ยกเลิกและล้าง upload ที่ค้างอยู่ ใช้ทำความสะอาด storage

09ข้อดี ข้อจำกัด และเมื่อไหร่ควรใช้

tus เหมาะมากกับงานที่ผู้ใช้ต้องอัปโหลดไฟล์ขนาดใหญ่ เช่น ระบบอัปโหลดวิดีโอ ไฟล์ backup หรือไฟล์ CAD แต่ก็ไม่ใช่ทุกโปรเจกต์ที่จำเป็นต้องใช้ ลองดูข้อพิจารณาเหล่านี้ก่อนตัดสินใจ

  • เหมาะ: ไฟล์ขนาดใหญ่กว่า 50-100 MB ขึ้นไป หรือกลุ่มผู้ใช้ที่เน็ตไม่เสถียร เช่น ใช้งานผ่านมือถือนอกสถานที่
  • เหมาะ: ระบบที่ต้องการให้ผู้ใช้สลับอุปกรณ์ได้ เช่น เริ่มอัปโหลดจากโน้ตบุ๊ก แล้วไปต่อจากมือถือ
  • ไม่จำเป็น: ฟอร์มอัปโหลดรูปโปรไฟล์หรือเอกสารขนาดไม่กี่ MB การใช้ multipart/form-data แบบปกติยังคุ้มค่ากว่าในแง่ความซับซ้อน
  • ข้อควรระวัง: ต้องวางแผนเรื่อง storage สำหรับ chunk ที่ค้างไว้ และตั้งกลไกลบไฟล์ที่ไม่มีใครมา resume ต่อเป็นเวลานาน เพื่อไม่ให้ดิสก์เต็ม

10สรุป

การอัปโหลดไฟล์ใหญ่ไม่จำเป็นต้องเป็นฝันร้ายของผู้ใช้อีกต่อไป ด้วย tus protocol และไลบรารี tus-php เราสามารถเปลี่ยนการอัปโหลดแบบ "พังแล้วเริ่มใหม่ทั้งหมด" ให้กลายเป็น "หยุดตรงไหน ก็ต่อจากตรงนั้น" ได้ โดยที่ไม่ต้องเขียนกลไก chunk และ offset เองตั้งแต่ศูนย์ ไม่ว่าจะใช้ CodeIgniter 4 หรือ Laravel แนวคิดก็เหมือนกันคือสร้าง endpoint เดียวให้รับทุก method ของ tus แล้วปล่อยให้ไลบรารีจัดการส่วนที่เหลือ

สำหรับใครที่กำลังจะเริ่มฟีเจอร์อัปโหลดไฟล์ขนาดใหญ่ในโปรเจกต์ถัดไป ลองเริ่มจากการรันตัวอย่าง server แบบ minimal ในบทความนี้ดูก่อน แล้วค่อยปรับให้เข้ากับ authentication และ business logic ของระบบจริง



PHP CI MANIA - PHP Code Generator 

โปรแกรมช่วยสร้างโค้ด "ลดเวลาการเขียนโปรแกรม"
ราคาสุดคุ้ม  
http://www.phpcodemania.com