TreeAPI.php

16.77 KB
06/05/2025 14:22
PHP
<?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;
    }
}