<?php
/**
* TreeAPI.php - คลาสสำหรับจัดการโครงสร้างต้นไม้ในฐานข้อมูล
*/
// นำเข้าไฟล์ config
require_once 'config.php';
class TreeAPI
{
/**
* @var mixed
*/
private $conn;
/**
* สร้างอินสแตนซ์ใหม่ของ TreeAPI
*/
public function __construct()
{
global $conn;
$this->conn = $conn;
}
/**
* เพิ่มโหนดใหม่
*
* @param string $name ชื่อของโหนด
* @param int $level ระดับของโหนด
* @param int|null $parentId ID ของโหนดพ่อ (null สำหรับโหนดระดับบนสุด)
* @param string|null $externalId ID ภายนอกสำหรับเชื่อมโยงกับระบบอื่น
* @param string|null $data ข้อมูลเพิ่มเติมในรูปแบบ JSON
* @return int|false ID ของโหนดที่เพิ่ม หรือ false ถ้าไม่สำเร็จ
*/
public function addNode($name, $level, $parentId = null, $externalId = null, $data = null)
{
// หาลำดับของโหนดตาม parent_id
$nodeOrder = $this->getNextNodeOrder($parentId);
$stmt = $this->conn->prepare("INSERT INTO tree_nodes (name, level, parent_id, node_order, external_id, data) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->bind_param("siisss", $name, $level, $parentId, $nodeOrder, $externalId, $data);
if ($stmt->execute()) {
return $stmt->insert_id;
} else {
return false;
}
}
/**
* หาลำดับถัดไปของโหนดภายใต้พ่อเดียวกัน
*
* @param int|null $parentId ID ของโหนดพ่อ
* @return int ลำดับถัดไป
*/
private function getNextNodeOrder($parentId)
{
if ($parentId === null) {
$query = "SELECT MAX(node_order) AS max_order FROM tree_nodes WHERE parent_id IS NULL";
$result = $this->conn->query($query);
} else {
$stmt = $this->conn->prepare("SELECT MAX(node_order) AS max_order FROM tree_nodes WHERE parent_id = ?");
$stmt->bind_param("i", $parentId);
$stmt->execute();
$result = $stmt->get_result();
}
if ($row = $result->fetch_assoc()) {
return ($row['max_order'] !== null) ? $row['max_order'] + 1 : 0;
}
return 0;
}
/**
* อัพเดตโหนด
*
* @param int $id ID ของโหนดที่ต้องการอัพเดต
* @param array $data ข้อมูลที่ต้องการอัพเดต [key => value]
* @return bool สำเร็จหรือไม่
*/
public function updateNode($id, $data)
{
$allowedFields = ['name', 'level', 'parent_id', 'node_order', 'external_id', 'data'];
$updates = [];
$paramTypes = '';
$paramValues = [];
foreach ($data as $key => $value) {
if (in_array($key, $allowedFields)) {
$updates[] = "$key = ?";
// กำหนดชนิดของพารามิเตอร์
if ($key === 'level' || $key === 'parent_id' || $key === 'node_order') {
$paramTypes .= 'i'; // integer
} else {
$paramTypes .= 's'; // string
}
$paramValues[] = $value;
}
}
if (empty($updates)) {
return false;
}
$query = "UPDATE tree_nodes SET ".implode(', ', $updates)." WHERE id = ?";
$paramTypes .= 'i'; // for id
$paramValues[] = $id;
$stmt = $this->conn->prepare($query);
// Bind parameters dynamically
$bindParams = array_merge([$paramTypes], $paramValues);
$bindParams = $this->refValues($bindParams);
call_user_func_array([$stmt, 'bind_param'], $bindParams);
return $stmt->execute();
}
/**
* ช่วยในการ bind parameters แบบไดนามิก
*/
private function refValues($arr)
{
$refs = [];
foreach ($arr as $key => $value) {
$refs[$key] = &$arr[$key];
}
return $refs;
}
/**
* ลบโหนดและลูกหลานทั้งหมด
*
* @param int $id ID ของโหนดที่ต้องการลบ
* @return bool สำเร็จหรือไม่
*/
public function deleteNode($id)
{
// ดึงข้อมูลลูกหลานทั้งหมด
$children = $this->getChildren($id);
// ลบลูกหลานก่อน
foreach ($children as $child) {
$this->deleteNode($child['id']);
}
// ลบโหนดหลัก
$stmt = $this->conn->prepare("DELETE FROM tree_nodes WHERE id = ?");
$stmt->bind_param("i", $id);
return $stmt->execute();
}
/**
* ดึงข้อมูลลูกทั้งหมดของโหนด
*
* @param int $parentId ID ของโหนดพ่อ
* @return array ข้อมูลโหนดลูกทั้งหมด
*/
public function getChildren($parentId)
{
$stmt = $this->conn->prepare("SELECT * FROM tree_nodes WHERE parent_id = ? ORDER BY node_order");
$stmt->bind_param("i", $parentId);
$stmt->execute();
$result = $stmt->get_result();
$children = [];
while ($row = $result->fetch_assoc()) {
$children[] = $row;
}
return $children;
}
/**
* ดึงข้อมูลโหนดตาม ID
*
* @param int $id ID ของโหนด
* @return array|null ข้อมูลของโหนด หรือ null ถ้าไม่พบ
*/
public function getNode($id)
{
$stmt = $this->conn->prepare("SELECT * FROM tree_nodes WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$result = $stmt->get_result();
if ($row = $result->fetch_assoc()) {
return $row;
}
return null;
}
/**
* ดึงข้อมูลโหนดทั้งหมดเป็นโครงสร้างต้นไม้
*
* @return array โครงสร้างต้นไม้ที่สมบูรณ์
*/
public function getTreeStructure()
{
// ดึงโหนดระดับบนสุด
$rootNodes = $this->getRootNodes();
// สร้างโครงสร้างต้นไม้ที่สมบูรณ์
foreach ($rootNodes as &$node) {
$this->buildNodeTree($node);
}
return $rootNodes;
}
/**
* สร้างโครงสร้างต้นไม้ของโหนด
*
* @param array &$node โหนดที่ต้องการสร้างโครงสร้าง
*/
private function buildNodeTree(&$node)
{
$children = $this->getChildren($node['id']);
$node['children'] = [];
foreach ($children as &$child) {
$this->buildNodeTree($child);
$node['children'][] = $child;
}
}
/**
* ดึงโหนดระดับบนสุดทั้งหมด
*
* @return array โหนดระดับบนสุดทั้งหมด
*/
public function getRootNodes()
{
$result = $this->conn->query("SELECT * FROM tree_nodes WHERE parent_id IS NULL ORDER BY node_order");
$rootNodes = [];
while ($row = $result->fetch_assoc()) {
$rootNodes[] = $row;
}
return $rootNodes;
}
/**
* เลื่อนตำแหน่งโหนด
*
* @param int $id ID ของโหนดที่ต้องการเลื่อน
* @param string $direction ทิศทางที่ต้องการเลื่อน ('up' หรือ 'down')
* @return bool สำเร็จหรือไม่
*/
public function moveNode($id, $direction)
{
$node = $this->getNode($id);
if (!$node) {
return false;
}
// หาโหนดพี่น้องทั้งหมด
if ($node['parent_id'] === null) {
$siblings = $this->getRootNodes();
} else {
$siblings = $this->getChildren($node['parent_id']);
}
// หาตำแหน่งปัจจุบัน
$currentPosition = -1;
foreach ($siblings as $index => $sibling) {
if ($sibling['id'] == $id) {
$currentPosition = $index;
break;
}
}
if ($currentPosition === -1) {
return false;
}
// คำนวณตำแหน่งใหม่
if ($direction === 'up' && $currentPosition > 0) {
$targetPosition = $currentPosition - 1;
} elseif ($direction === 'down' && $currentPosition < count($siblings) - 1) {
$targetPosition = $currentPosition + 1;
} else {
return false;
}
// สลับลำดับ
$targetNode = $siblings[$targetPosition];
// อัพเดตลำดับของโหนด
$this->updateNode($id, ['node_order' => $targetNode['node_order']]);
$this->updateNode($targetNode['id'], ['node_order' => $node['node_order']]);
return true;
}
/**
* ย้ายโหนดไปเป็นลูกของโหนดอื่น
*
* @param int $id ID ของโหนดที่ต้องการย้าย
* @param int|null $newParentId ID ของโหนดพ่อใหม่ (null สำหรับย้ายเป็นโหนดระดับบนสุด)
* @param int $newLevel ระดับใหม่ของโหนด
* @return bool สำเร็จหรือไม่
*/
public function moveNodeToParent($id, $newParentId, $newLevel)
{
$node = $this->getNode($id);
if (!$node) {
return false;
}
// คำนวณความแตกต่างของระดับ
$levelDifference = $newLevel - $node['level'];
// อัพเดตโหนด
$nodeOrder = $this->getNextNodeOrder($newParentId);
$this->updateNode($id, [
'parent_id' => $newParentId,
'level' => $newLevel,
'node_order' => $nodeOrder
]);
// อัพเดตระดับของลูกหลาน
$this->updateChildrenLevels($id, $levelDifference);
return true;
}
/**
* อัพเดตระดับของลูกหลาน
*
* @param int $parentId ID ของโหนดพ่อ
* @param int $levelChange การเปลี่ยนแปลงระดับ
*/
private function updateChildrenLevels($parentId, $levelChange)
{
$children = $this->getChildren($parentId);
foreach ($children as $child) {
$newLevel = $child['level'] + $levelChange;
$this->updateNode($child['id'], ['level' => $newLevel]);
// อัพเดตลูกหลานแบบเรียกซ้ำ
$this->updateChildrenLevels($child['id'], $levelChange);
}
}
/**
* นำเข้าข้อมูลต้นไม้จาก JavaScript TreeManager
*
* @param array $treeData ข้อมูลต้นไม้ในรูปแบบของ JavaScript TreeManager
* @return bool สำเร็จหรือไม่
*/
public function importFromTreeManager($treeData)
{
// เริ่มต้น transaction
$this->conn->begin_transaction();
try {
// ลบข้อมูลเก่าทั้งหมด
$this->conn->query("TRUNCATE TABLE tree_nodes");
// นำเข้าข้อมูลใหม่
foreach ($treeData as $rootNode) {
$this->importNode($rootNode, null);
}
// ยืนยัน transaction
$this->conn->commit();
return true;
} catch (Exception $e) {
// ยกเลิก transaction ถ้ามีข้อผิดพลาด
$this->conn->rollback();
return false;
}
}
/**
* นำเข้าโหนดและลูกหลาน
*
* @param array $node ข้อมูลโหนด
* @param int|null $parentId ID ของโหนดพ่อ
* @return int ID ของโหนดที่นำเข้า
*/
private function importNode($node, $parentId)
{
// สร้างโหนดใหม่
$nodeId = $this->addNode(
$node['name'],
$node['level'],
$parentId,
isset($node['external_id']) ? $node['external_id'] : null,
isset($node['data']) ? json_encode($node['data']) : null
);
// นำเข้าลูกหลาน
if (isset($node['children']) && is_array($node['children'])) {
foreach ($node['children'] as $child) {
$this->importNode($child, $nodeId);
}
}
return $nodeId;
}
/**
* ส่งออกข้อมูลต้นไม้เป็นรูปแบบของ JavaScript TreeManager
*
* @return array ข้อมูลต้นไม้ในรูปแบบของ JavaScript TreeManager
*/
public function exportToTreeManager()
{
$treeData = $this->getTreeStructure();
// แปลงรูปแบบให้ตรงกับ TreeManager
$this->formatNodesForTreeManager($treeData);
return $treeData;
}
/**
* แปลงรูปแบบโหนดให้ตรงกับ TreeManager
*
* @param array &$nodes รายการโหนดที่ต้องการแปลง
*/
private function formatNodesForTreeManager(&$nodes)
{
foreach ($nodes as &$node) {
// แปลง id, parent_id เป็นรูปแบบของ TreeManager
$node['id'] = (int) $node['id'];
if ($node['parent_id'] !== null) {
$node['parentId'] = (int) $node['parent_id'];
}
unset($node['parent_id']);
// แปลง node_order เป็น order
$node['order'] = (int) $node['node_order'];
unset($node['node_order']);
// แปลง level เป็น level
$node['level'] = (int) $node['level'];
// แปลงข้อมูล JSON เป็น object ถ้ามี
if (!empty($node['data'])) {
$node['data'] = json_decode($node['data'], true);
}
// แปลงลูกหลาน
if (isset($node['children']) && is_array($node['children'])) {
$this->formatNodesForTreeManager($node['children']);
}
}
}
/**
* ดึงข้อมูลโหนดตาม external_id
*
* @param string $externalId External ID ที่ต้องการค้นหา
* @return array|null ข้อมูลของโหนด หรือ null ถ้าไม่พบ
*/
public function getNodeByExternalId($externalId)
{
$stmt = $this->conn->prepare("SELECT * FROM tree_nodes WHERE external_id = ?");
$stmt->bind_param("s", $externalId);
$stmt->execute();
$result = $stmt->get_result();
if ($row = $result->fetch_assoc()) {
return $row;
}
return null;
}
/**
* ค้นหาโหนดตามชื่อ
*
* @param string $searchTerm คำค้นหา
* @return array ผลลัพธ์การค้นหา
*/
public function searchNodes($searchTerm)
{
$searchTerm = "%$searchTerm%";
$stmt = $this->conn->prepare("SELECT * FROM tree_nodes WHERE name LIKE ? ORDER BY level, node_order");
$stmt->bind_param("s", $searchTerm);
$stmt->execute();
$result = $stmt->get_result();
$nodes = [];
while ($row = $result->fetch_assoc()) {
$nodes[] = $row;
}
return $nodes;
}
}