ในบทความนี้จะเป็นการใช้คำสั่ง MySQL (หรือผู้ใช้ MariaDB ก็ใช้ได้) เพื่อตรวจหาห้องพักที่ว่างหรือยังไม่ถูกจอง. เงื่อนไขพิเศษที่แตกต่างจากที่พบทั่วไปก็คือ ผู้เข้าพักสามารถกำหนดได้ว่าจะระบุวันที่เช็กเอาต์หรือไม่ก็ได้, ถ้าหากไม่ระบุนั่นหมายความว่าจะยังคงเข้าพักอยู่ต่อไปเรื่อยๆ. วิธีการแบบนี้สามารถประยุกต์ใช้กับระบบการเช่าห้องพักระยะยาวได้ เช่น เช่าบ้าน, เช่าคอนโดหรือหอพักทั่วไป ที่ผู้เข้าพักจะต่อสัญญาไปเรื่อยๆ ผู้ดูแลก็ยังไม่ต้องระบุวันเช็กเอาต์ก็ได้.
คำสั่ง SQL จะแบ่งเงื่อนไขผ่านตัวภาษาโปรแกรมก่อน ว่าผู้เข้าพักจะระบุวันที่เช็กเอาต์หรือไม่. ในกรณีตัวอย่างนี้จะใช้ PHP ดังนั้นโค้ดโดยย่อเฉพาะใจความสำคัญจะเป็นดังนี้.
if (empty($checkout)) {
// if not enter check out date.
$sql .= '(
(
`checkin` >= :checkin
AND (`checkout` >= :checkin OR `checkout` IS NULL)
)
OR
(
`checkin` <= :checkin
AND (`checkout` >= :checkin OR `checkout` IS NULL)
)
)';
} else {
// if enter check out date.
$sql .= '(
(
`checkout` IS NULL
AND
(
(
`checkin` >= :checkin
AND `checkin` <= :checkout
)
OR
(
`checkin` <= :checkin
)
)
)
OR
(
(
`checkin` >= :checkin
AND `checkin` <= :checkout
)
OR
(
`checkin` <= :checkin
AND `checkout` >= :checkin
)
)
)';
}
โค้ดเต็ม
ในโค้ดเต็มๆต่อไปนี้จะเขียนในภาษา PHP โดยจะใช้การเชื่อมต่อฐานข้อมูลผ่าน PDO ทั้งนี้หากผู้อ่านจะใช้การเชื่อมต่ออื่น เช่น mysqli ก็จะต้องหาวิธีแปลงด้วยตัวเอง.
/**
* Check availability.
*
* @param string $room Room number.
* @param string $checkin Check in date.
* @param string $checkout Check out date. It can be null or empty if there is no plan to check out yet.
* @return bool Return `true` if selected room and dates is available. Return `false` if unavailable or occupied.
*/
function checkAvailability($room, $checkin, $checkout = null)
{
global $dbh;
$sql = 'SELECT * FROM `room_availability` WHERE `room` = :room';
$sql .= ' AND ';
if (empty($checkout)) {
// if not enter check out date.
$sql .= '(
(
`checkin` >= :checkin
AND (`checkout` >= :checkin OR `checkout` IS NULL)
)
OR
(
`checkin` <= :checkin
AND (`checkout` >= :checkin OR `checkout` IS NULL)
)
)';
} else {
// if enter check out date.
$sql .= '(
(
`checkout` IS NULL
AND
(
(
`checkin` >= :checkin
AND `checkin` <= :checkout
)
OR
(
`checkin` <= :checkin
)
)
)
OR
(
(
`checkin` >= :checkin
AND `checkin` <= :checkout
)
OR
(
`checkin` <= :checkin
AND `checkout` >= :checkin
)
)
)';
}
$sth = $dbh->prepare($sql);
$sth->bindValue(':room', $room);
$sth->bindValue(':checkin', $checkin);
if (!empty($checkout)) {
$sth->bindValue(':checkout', $checkout);
}
$sth->execute();
$result = $sth->fetchAll();
$sth->closeCursor();
unset($sql);
if ((is_object($result) || is_array($result)) && !empty($result)) {
// if found occupied, unavailable.
// mark availability as false.
return false;
} else {
return true;
}
}// endfunciton; checkAvailability
ฟังก์ชั่นดังกล่าวจะเป็นการตรวจหาห้อง และ วันที่เช็กอิน และอาจจะรวมถึงวันที่เช็กเอาต์ด้วย แล้วตรวจหาว่ามีห้องว่างในวันดังกล่าวหรือไม่ โดยใช้วิธีการตรวจหาให้พบ, ถ้าหากพบแสดงว่าห้องนั้นไม่ว่าง และจะคืนค่า false
แต่ถ้าหากไม่พบแสดงว่าห้องนั้นว่างอยู่ และจะคืนค่า true
กลับไป.
ในการเก็บข้อมูลแบบนี้ ถ้าหากผู้เข้าพักไม่ระบุวันที่เช็กเอาต์ การเก็บข้อมูลวันที่เช็กเอาต์จะต้องเป็น null
เท่านั้น! ไม่สามารถเก็บเป็นสตริงว่างเปล่าได้ เพราะในเงื่อนไขการตรวจสอบจะใช้ IS NULL
ร่วมด้วย.
การทดสอบ.
ใช้โค้ดชุดต่อไปนี้ในการทดสอบ.
config.php
<?php
$dsn = 'mysql:host=127.0.0.1;dbname=myDBName;charset=utf8mb4';
$username = '';
$password = '';
$pdoOptions = [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_OBJ,
\PDO::ATTR_STRINGIFY_FETCHES => true,
];
create-table.php
<?php
require 'config.php';
$dbh = new \PDO($dsn, $username, $password, $pdoOptions);
$sql = 'CREATE TABLE IF NOT EXISTS `room_availability` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`room` varchar(5) DEFAULT NULL COMMENT \'room number\',
`checkin` date DEFAULT NULL COMMENT \'check in date\',
`checkout` date DEFAULT NULL COMMENT \'check out date\',
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci AUTO_INCREMENT=1';
$result = $dbh->exec($sql);
echo '<pre>' . $sql . '</pre>';
var_export($result);
$sql = 'SELECT `id` FROM `room_availability`';
$sth = $dbh->prepare($sql);
unset($sql);
$sth->execute();
$result = $sth->fetchAll();
if (empty($result)) {
$sqlArray = [
'INSERT INTO `room_availability` (`room`, `checkin`, `checkout`) VALUES (\'101\', \'2023-02-01\', \'2023-02-28\');',
'INSERT INTO `room_availability` (`room`, `checkin`, `checkout`) VALUES (\'101\', \'2023-03-01\', \'2023-04-30\');',
'INSERT INTO `room_availability` (`room`, `checkin`, `checkout`) VALUES (\'101\', \'2023-06-01\', null);',// no plan to check out.
'INSERT INTO `room_availability` (`room`, `checkin`, `checkout`) VALUES (\'102\', \'2023-02-01\', \'2023-04-30\');',
'INSERT INTO `room_availability` (`room`, `checkin`, `checkout`) VALUES (\'102\', \'2023-05-01\', \'2023-07-31\');',
'INSERT INTO `room_availability` (`room`, `checkin`, `checkout`) VALUES (\'102\', \'2023-09-01\', \'2023-11-30\');',
];
foreach ($sqlArray as $sql) {
$result = $dbh->exec($sql);
echo '<pre>' . $sql . '</pre>';
var_export($result);
}
unset($sql, $sqlArray);
}
unset($dbh, $result, $sth);
list-all.php
<?php
require 'config.php';
$dbh = new \PDO($dsn, $username, $password, $pdoOptions);
$sql = 'SELECT * FROM `room_availability`';
$sth = $dbh->prepare($sql);
unset($sql);
$sth->execute();
$result = $sth->fetchAll();
echo '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@materializecss/materialize@2.0.0-alpha/dist/css/materialize.min.css">' . PHP_EOL;
if (is_iterable($result)) {
echo '<table>' . PHP_EOL;
echo '<thead><tr><th>ID</th><th>room</th><th>check in</th><th>check out</th></tr></thead>' . PHP_EOL;
echo '<tbody>' . PHP_EOL;
foreach ($result as $row) {
echo '<tr>' . PHP_EOL;
echo '<td>' . $row->id . '</td>';
echo '<td>' . (!is_null($row->room) ? $row->room : '<em>NULL</em>') . '</td>';
echo '<td>' . (!is_null($row->checkin) ? $row->checkin : '<em>NULL</em>') . '</td>';
echo '<td>' . (!is_null($row->checkout) ? $row->checkout : '<em>NULL</em>') . '</td>';
echo '</tr>' . PHP_EOL;
}
unset($row);
echo '</tbody>' . PHP_EOL;
echo '</table>' . PHP_EOL;
}
unset($result);
$sth->closeCursor();
unset($dbh, $sth);
tests.php
<?php
require 'config.php';
$dbh = new \PDO($dsn, $username, $password, $pdoOptions);
ini_set('zend.assertions', 1);
ini_set('assert.exception', 0);
assert_options(ASSERT_BAIL, false);
// test on room 101
$room = '101';
$testsArray = [
// tests with check out date specified. -------------------------
[
'checkin' => '2023-01-01',
'checkout' => '2023-01-31',
'expect' => true,// true means available, fase means unavailable.
],
[
'checkin' => '2023-01-01',
'checkout' => '2023-02-28',
'expect' => false,
],// check out stuck
[
'checkin' => '2023-01-01',
'checkout' => '2023-05-05',
'expect' => false,
],
[
'checkin' => '2023-01-01',
'checkout' => '2023-12-31',
'expect' => false,
],
[
'checkin' => '2023-02-01',
'checkout' => '2023-05-31',
'expect' => false,
],// check in stuck
[
'checkin' => '2023-03-01',
'checkout' => '2023-05-31',
'expect' => false,
],
[
'checkin' => '2023-04-01',
'checkout' => '2023-05-31',
'expect' => false,
],
[
'checkin' => '2023-05-01',
'checkout' => '2023-05-31',
'expect' => true,
],
[
'checkin' => '2023-05-01',
'checkout' => '2023-06-01',
'expect' => false,
],// check out stuck with 'no plan to check out' on db.
[
'checkin' => '2023-05-01',
'checkout' => '2023-08-31',
'expect' => false,
],
[
'checkin' => '2023-06-01',
'checkout' => '2023-10-31',
'expect' => false,
],
[
'checkin' => '2023-07-01',
'checkout' => '2023-10-31',
'expect' => false,
],
[
'checkin' => '2023-12-01',
'checkout' => '2023-12-31',
'expect' => false,
],
[
'checkin' => '2024-01-01',
'checkout' => '2024-12-31',
'expect' => false,
],
// test with no plan to check out from user input. --------------
[
'checkin' => '2023-01-01',
'expect' => false,
],// stuck with 'no plan to check out' on db.
[
'checkin' => '2023-05-01',
'expect' => false,
],
[
'checkin' => '2023-08-01',
'expect' => false,
],
];
runTests($room, $testsArray);
// test on room 102
$room = '102';
$testsArray = [
// tests with check out date specified. -------------------------
[
'checkin' => '2023-01-01',
'checkout' => '2023-01-31',
'expect' => true,
],
[
'checkin' => '2023-01-01',
'checkout' => '2023-02-28',
'expect' => false,
],// check out stuck
[
'checkin' => '2023-01-01',
'checkout' => '2023-08-31',
'expect' => false,
],
[
'checkin' => '2023-02-01',
'checkout' => '2023-03-31',
'expect' => false,
],// check in stuck
[
'checkin' => '2023-03-01',
'checkout' => '2023-08-31',
'expect' => false,
],
[
'checkin' => '2023-04-01',
'checkout' => '2023-08-31',
'expect' => false,
],
[
'checkin' => '2023-05-01',
'checkout' => '2023-08-31',
'expect' => false,
],
[
'checkin' => '2023-06-01',
'checkout' => '2023-08-31',
'expect' => false,
],
[
'checkin' => '2023-07-01',
'checkout' => '2023-08-31',
'expect' => false,
],
[
'checkin' => '2023-08-01',
'checkout' => '2023-08-31',
'expect' => true,
],
[
'checkin' => '2023-08-01',
'checkout' => '2023-12-31',
'expect' => false,
],// check out stuck
[
'checkin' => '2023-11-01',
'checkout' => '2023-12-31',
'expect' => false,
],// check in stuck
[
'checkin' => '2023-12-01',
'checkout' => '2023-12-31',
'expect' => true,
],
// test with no plan to check out from user input. --------------
[
'checkin' => '2023-01-01',
'expect' => false,
],// check in stuck
[
'checkin' => '2023-02-01',
'expect' => false,
],
[
'checkin' => '2023-03-01',
'expect' => false,
],
[
'checkin' => '2023-04-01',
'expect' => false,
],
[
'checkin' => '2023-05-01',
'expect' => false,
],
[
'checkin' => '2023-06-01',
'expect' => false,
],
[
'checkin' => '2023-07-01',
'expect' => false,
],
[
'checkin' => '2023-08-01',
'expect' => false,
],// user input 'no plan to check out' stuck with checked in on id 6
[
'checkin' => '2023-09-01',
'expect' => false,
],
[
'checkin' => '2023-10-01',
'expect' => false,
],
[
'checkin' => '2023-11-01',
'expect' => false,
],
[
'checkin' => '2023-12-01',
'expect' => true,
],
[
'checkin' => '2024-01-01',
'expect' => true,
],
];
runTests($room, $testsArray);
// test on room 103 (never checked in)
$room = '103';
$testsArray = [
[
'checkin' => '2023-01-01',
'checkout' => '2023-12-31',
'expect' => true,
],
[
'checkin' => '2023-01-01',
'expect' => true,
],
];
runTests($room, $testsArray);
/**
* Run tests with selected room and check in/check out date in the `$testsArray` data.
*
* @param string $room Room number.
* @param array $testsArray Tests array data.
* @return void
*/
function runTests($room, $testsArray)
{
foreach ($testsArray as $testItem) {
$checkin = $testItem['checkin'];
$checkout = ($testItem['checkout'] ?? null);
$expect = $testItem['expect'];
$testResult = checkAvailability($room, $checkin, $checkout);
echo 'room ' . $room
. ' on ' . $checkin . ' to ' . (!empty($checkout) ? $checkout : '<em><small>no plan to check out</small></em>')
. ' expect ' . (true === $expect ? '<span style="color: green; font-weight: bold;">' : '<span style="color: red;">') . var_export($expect, true) . '</span>'
. ', actual result ' . var_export($testResult, true) . '.';
echo '<br>' . PHP_EOL;
assert($testResult === $expect);
unset($checkin, $checkout, $expect, $testResult);
}
unset($testItem);
}// endfunction; runTests
unset($room, $testsArray);
/**
* Check availability.
*
* @param string $room Room number.
* @param string $checkin Check in date.
* @param string $checkout Check out date. It can be null or empty if there is no plan to check out yet.
* @return bool Return `true` if selected room and dates is available. Return `false` if unavailable or occupied.
*/
function checkAvailability($room, $checkin, $checkout = null)
{
global $dbh;
$sql = 'SELECT * FROM `room_availability` WHERE `room` = :room';
$sql .= ' AND ';
if (empty($checkout)) {
// if not enter check out date.
$sql .= '(
(
`checkin` >= :checkin
AND (`checkout` >= :checkin OR `checkout` IS NULL)
)
OR
(
`checkin` <= :checkin
AND (`checkout` >= :checkin OR `checkout` IS NULL)
)
)';
} else {
// if enter check out date.
$sql .= '(
(
`checkout` IS NULL
AND
(
(
`checkin` >= :checkin
AND `checkin` <= :checkout
)
OR
(
`checkin` <= :checkin
)
)
)
OR
(
(
`checkin` >= :checkin
AND `checkin` <= :checkout
)
OR
(
`checkin` <= :checkin
AND `checkout` >= :checkin
)
)
)';
}
$sth = $dbh->prepare($sql);
$sth->bindValue(':room', $room);
$sth->bindValue(':checkin', $checkin);
if (!empty($checkout)) {
$sth->bindValue(':checkout', $checkout);
}
$sth->execute();
$result = $sth->fetchAll();
$sth->closeCursor();
unset($sql);
if ((is_object($result) || is_array($result)) && !empty($result)) {
// if found occupied, unavailable.
// mark availability as false.
return false;
} else {
return true;
}
}// endfunciton; checkAvailability
unset($dbh);
echo '<hr>' . PHP_EOL;
echo '<h3>Display all data on DB.</h3>' . PHP_EOL;
include 'list-all.php';
PHP รุ่นที่ผู้เขียนใช้ในการทดสอบนั้นจะเป็นรุ่น PHP 8.2 แต่ทั้งนี้พบว่าสามารถทำงานได้ตั้งแต่ PHP 7.1 เรื่อยมา.