dhtmlxGantt с PHP: Slim

Это руководство охватывает настройку диаграммы Ганта на основе PHP с использованием фреймворка Slim 4 и RESTful API на сервере.

Это руководство предназначено для Slim Framework v4.x. Если вы работаете с более старой версией, ознакомьтесь с руководством Slim Framework v3.x.

Также существуют учебные пособия по интеграции с другими платформами и фреймворками:

Этот пример использует Slim 4 для маршрутизации и MySQL для хранения данных. Логика CRUD построена с использованием PDO и предназначена для адаптации к другим фреймворкам.

Полный исходный код доступен на GitHub.

Шаг 1. Настройка проекта

Создание проекта

Мы будем использовать скелетное приложение для фреймворка Slim 4.

Начните с импорта проекта и установки его с помощью Composer:

php composer.phar create-project slim/slim-skeleton gantt-rest-php

Если Composer установлен глобально, вы можете выполнить следующую команду:

composer create-project slim/slim-skeleton gantt-rest-php

Затем убедитесь, что все работает. Перейдите в папку приложения и запустите веб-сервер:

cd gantt-rest-php
php -S 0.0.0.0:8080 -t public public/index.php

После этого откройте http://127.0.0.1:8080 в вашем браузере. Вы должны увидеть страницу по умолчанию Slim.

Шаг 2. Добавление Gantt на страницу

Теперь давайте создадим страницу, содержащую диаграмму Ганта. Этот процесс включает два простых шага.

Создание представления

Создайте файл с именем basic.html в папке app/templates. Этот файл будет содержать диаграмму Ганта и ее необходимую настройку для загрузки данных.

Вот полный код:

app/templates/basic.html

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
 
  <script src="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.js"></script>
  <link href="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.css" rel="stylesheet">
 
  <style type="text/css">
    html, body{
      height:100%;
      padding:0px;
      margin:0px;
      overflow: hidden;
    }
</style> </head> <body> <div id="gantt_here" style='width:100%; height:100%;'></div> <script type="text/javascript">
    gantt.init("gantt_here");
</script> </body> </html>

Этот код создает пустую диаграмму Ганта на странице. Пользователи могут добавлять и редактировать задачи и ссылки, но изменения не сохранятся после перезагрузки страницы.

Настройка маршрутов

Чтобы сделать новую страницу доступной в браузере, добавьте маршрут в app/routes.php:

app/routes.php

$app->get('/', function (Request $request, Response $response) {
$payload = file_get_contents(__DIR__.'/templates/basic.html');
$response->getBody()->write($payload);
return $response;
});

Перезапустите приложение:

command line

php -S 0.0.0.0:8080 -t public public/index.php

Теперь откройте http://127.0.0.1:8080/ в вашем браузере, чтобы увидеть диаграмму Ганта, отображаемую на странице.

Шаг 3. Настройка базы данных

На этом этапе диаграмма Ганта отображается, но не сохраняет данные. Следующий шаг — настроить базу данных и подключить ее к приложению.

Создание базы данных

Вы можете настроить базу данных с помощью предпочитаемого клиента MySQL, например phpMyAdmin, или напрямую через консоль. Ниже приведен пример SQL-кода для создания простой базы данных с двумя таблицами.

CREATE DATABASE  IF NOT EXISTS `gantt`;
USE `gantt`;
 
CREATE TABLE `gantt_links` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `source` int(11) NOT NULL,
  `target` int(11) NOT NULL,
  `type` varchar(1) NOT NULL,
  PRIMARY KEY (`id`)
);
CREATE TABLE `gantt_tasks` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `text` varchar(255) NOT NULL,
  `start_date` datetime NOT NULL,
  `duration` int(11) NOT NULL,
  `progress` float NOT NULL,
  `parent` int(11) NOT NULL,
  PRIMARY KEY (`id`)
);

После настройки базы данных вы можете добавить некоторые тестовые данные в таблицу gantt_tasks. Вот простой пример:

INSERT INTO `gantt_tasks` VALUES ('1', 'Project #1', '2020-03-31 00:00:00', 
  '4', '0.8', '0');
INSERT INTO `gantt_tasks` VALUES ('2', 'Task #1', '2020-03-31 00:00:00', 
  '3', '0.5', '1');
INSERT INTO `gantt_tasks` VALUES ('3', 'Task #2', '2020-04-01 00:00:00', 
  '2', '0.7', '1');
INSERT INTO `gantt_tasks` VALUES ('4', 'Task #3', '2020-04-02 00:00:00', 
  '2', '0', '1');
INSERT INTO `gantt_tasks` VALUES ('5', 'Task #1.1', '2020-04-03 00:00:00', 
  '3', '0.34', '2');
INSERT INTO `gantt_tasks` VALUES ('6', 'Task #1.2', '2020-04-03 13:22:17', 
  '2', '0.5', '2');
INSERT INTO `gantt_tasks` VALUES ('7', 'Task #2.1', '2020-04-04 00:00:00',
  '3', '0.2', '3');
INSERT INTO `gantt_tasks` VALUES ('8', 'Task #2.2', '2020-04-05 00:00:00', 
  '2', '0.9', '3');

Для более подробного примера ознакомьтесь с этим руководством.

С готовой базой данных и тестовыми данными можно переходить к загрузке данных.


Шаг 4: Загрузка данных

Теперь давайте займемся загрузкой данных из базы данных. На стороне клиента данные могут быть загружены с использованием метода gantt.load:

app/templates/basic.html

gantt.config.date_format = "%Y-%m-%d %H:%i:%s"; 
gantt.init("gantt_here");
gantt.load("/data");

Это отправляет AJAX-запрос на указанный URL и ожидает, что ответ будет содержать данные Ганта в формате JSON.

Параметр gantt.config.date_format также устанавливается здесь для определения формата даты, используемого источником данных, что облегчает клиенту разбор дат.

На серверной стороне вам нужно создать обработчик для этого запроса. Откройте файл app/routes.php и добавьте новый маршрут:

app/routes.php

$app->get('/data',  'getGanttData');

Далее реализуйте логику функции getGanttData. Чтобы все было организовано, вы можете разместить всю логику, связанную с Гантом, в отдельном файле. Создайте новый файл app/gantt.php и добавьте следующий код:

app/gantt.php

<?php
 
function getConnection()
{
    return new PDO("mysql:host=localhost;dbname=gantt", "root", "root", 
    [
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
      PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    ]);
}
 
function getGanttData($request, $response, $args) {
  $db = getConnection();
  $result = [
    "data"=> [],
    "links"=> []
  ];
 
  foreach($db->query("SELECT * FROM gantt_tasks") as $row){
    $row["open"] = true;
    array_push($result["data"], $row);
  }
 
  foreach ($db->query("SELECT * FROM gantt_links") as $link){
    array_push($result["links"], $link);
  }
 
  $payload = json_encode($result);
 
  $response->getBody()->write($payload);
  return $response->withHeader("Content-Type", "application/json");
};

Наконец, включите app/gantt.php в app/routes.php:

app/routes.php

<?php
declare(strict_types=1);
 
use App\Application\Actions\User\ListUsersAction;
use App\Application\Actions\User\ViewUserAction;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\App;
use Slim\Interfaces\RouteCollectorProxyInterface as Group;
 
// Add dhtmlxGantt CRUD
require __DIR__ . "/gantt.php";
 
return function (App $app) {
    $app->get("/", function (Request $request, Response $response) {
        $payload = file_get_contents(__DIR__."/templates/basic.html");
        $response->getBody()->write($payload);
        return $response;
    });
 
    $app->get("/data",  "getGanttData");
 
    $app->group("/users", function (Group $group) {
        $group->get("", ListUsersAction::class);
        $group->get("/{id}", ViewUserAction::class);
    });
};

Краткое резюме кода:

  • В app/routes.php определен маршрут для обработки запросов данных.
  • Обработчик считывает все задачи и ссылки из базы данных и отправляет их клиенту в формате JSON.
  • Свойство open добавляется к объектам задач, чтобы дерево задач было развернуто по умолчанию.

После этого вы можете открыть http://127.0.0.1:8080/, чтобы увидеть диаграмму Ганта, заполненную тестовыми данными.


Шаг 5: Сохранение изменений

Следующий шаг включает в себя сохранение изменений, внесенных на стороне клиента, обратно на сервер. Это обычно делается с помощью библиотеки dataProcessor, которая встроена в Гант. Добавьте следующий код в basic.html:

app/templates/basic.html

gantt.config.date_format = "%Y-%m-%d %H:%i:%s";
 
gantt.init("gantt_here");
gantt.load("/data");
 
var dp = new gantt.dataProcessor("/data");dp.init(gantt);dp.setTransactionMode("REST");

Здесь DataProcessor отслеживает действия на стороне клиента, такие как добавление, обновление или удаление данных, и отправляет соответствующие AJAX-запросы на сервер. Режим REST гарантирует использование различных HTTP-методов для различных действий. Вы можете найти подробности о маршрутах здесь.

Чтобы обработать эти маршруты в приложении, обновите app/routes.php:

app/routes.php

<?php
 
declare(strict_types=1);
 
use App\Application\Actions\User\ListUsersAction;
use App\Application\Actions\User\ViewUserAction;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\App;
use Slim\Interfaces\RouteCollectorProxyInterface as Group;
 
// Add dhtmlxGantt CRUD
require __DIR__ . "/gantt.php";
 
return function (App $app) {
    $app->get("/", function (Request $request, Response $response) {
        $payload = file_get_contents(__DIR__."/templates/basic.html");
        $response->getBody()->write($payload);
        return $response;
    });
 
    $app->get("/data",  "getGanttData");
 
    $app->post("/data/task", "addTask");
    $app->put("/data/task/{id}", "updateTask");
    $app->delete("/data/task/{id}", "deleteTask");
 
    $app->post("/data/link", "addLink");
    $app->put("/data/link/{id}", "updateLink");
    $app->delete("/data/link/{id}", "deleteLink");
};

Теперь реализуйте методы, связанные с этими маршрутами, в app/gantt.php. Код для этих методов прост — они обрабатывают создание, обновление и удаление задач и ссылок. При вставке ID нового элемента в базе данных отправляется обратно клиенту.

Клиентская часть Gantt обрабатывает связанные задачи и ссылки по умолчанию, поэтому нет необходимости управлять связями базы данных здесь. Однако, если вы хотите обрабатывать их на серверной стороне, вы можете включить конфигурацию cascade_delete.

С готовым приложением, запустите его и откройте http://127.0.0.1:8080, чтобы увидеть полностью функциональную диаграмму Ганта.


Хранение порядка задач

Gantt позволяет изменять порядок задач с помощью функции перетаскивания. Чтобы поддержать эту функцию, вам нужно хранить порядок задач в базе данных. Вы можете найти больше деталей здесь.

Включение изменения порядка задач на клиенте

Чтобы пользователи могли изменять порядок задач в пользовательском интерфейсе, начните с изменения конфигурации в файле basic.html. Обновите его следующим образом:

app/templates/basic.html

gantt.config.order_branch = true;gantt.config.order_branch_free = true; 
gantt.init("gantt_here");

Затем внесите изменения на серверной стороне, чтобы хранить порядок задач в столбце с именем sortorder. Вот пример того, как может выглядеть обновленная таблица gantt_tasks:

CREATE TABLE `gantt_tasks` (
  `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `text` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `start_date` datetime NOT NULL,
  `duration` int(11) NOT NULL,
  `progress` float NOT NULL DEFAULT 0,
  `parent` int(11) NOT NULL,
  `sortorder` int(11) NOT NULL );

Если у вас уже есть таблица, вы можете просто добавить столбец sortorder:

ALTER TABLE `gantt_tasks` ADD COLUMN `sortorder` int(11) NOT NULL;

Теперь обновите операции CRUD в app/gantt.php.

  1. GET /data должен возвращать задачи, отсортированные по столбцу sortorder:

app/gantt.php

function getGanttData($request, $response, $args) {
  $db = getConnection();
  $result = [
    "data" => [],
    "links" => []
  ];
 
  foreach ($db->query("SELECT * FROM gantt_tasks ORDER BY sortorder ASC") as $row) {
    $row["open"] = true;
    array_push($result["data"], $row);
  }
 
  foreach ($db->query("SELECT * FROM gantt_links") as $link) {
    array_push($result["links"], $link);
  }
  $payload = json_encode($result);
 
  $response->getBody()->write($payload);
  return $response->withHeader("Content-Type", "application/json");
};
  1. При добавлении новой задачи убедитесь, что она получает начальное значение sortorder:

app/gantt.php

function addTask($request, $response, $args) {
  $task = getTask($request->getParsedBody());
  $db = getConnection();
  $maxOrderQuery = "SELECT MAX(sortorder) AS maxOrder FROM gantt_tasks";
  $statement = $db->prepare($maxOrderQuery);
  $statement->execute();
 
  $maxOrder = $statement->fetchColumn();
  if (!$maxOrder) {
    $maxOrder = 0;
  }
 
  $task[":sortorder"] = $maxOrder + 1;
 
  $query = "INSERT INTO gantt_tasks(text, start_date, duration, progress, parent, sortorder) ".
    "VALUES (:text, :start_date, :duration, :progress, :parent, :sortorder)";
  $db->prepare($query)->execute($task);
 
  $result = [
    "action" => "inserted",
    "tid" => $db->lastInsertId()
  ];
  $payload = json_encode($result);
 
  $response->getBody()->write($payload);
  return $response->withHeader("Content-Type", "application/json");
}
  1. Когда задачи меняют порядок, их новый порядок должен быть сохранен. Это можно обработать следующим образом:

app/gantt.php

// update a task
function updateTask($request, $response, $args) {
  $sid = $request->getAttribute("id");
  parse_str(file_get_contents("php://input"), $body);
  $task = [
      "text" => $body["text"],
      "start_date" => $body["start_date"],
      "duration" => $body["duration"],
      "progress" => $body["progress"],
      "parent" => $body["parent"],
      "sortorder" => $body["sortorder"]
  ];
  $db = getConnection();
  $query = "UPDATE gantt_tasks ".
    "SET text = :text, start_date = :start_date, duration = :duration,". 
      "progress = :progress, parent = :parent, sortorder = :sortorder ".
    "WHERE id = :sid";
  $db->prepare($query)->execute(array_merge($task, [":sid" => $sid]));
 
  if (isset($body["target"]) && $body["target"]) {
    updateOrder($sid, $body["target"], $db);
  }
 
  $result = [
    "action" => "updated"
  ];
  $payload = json_encode($result);
 
  $response->getBody()->write($payload);
  return $response->withHeader("Content-Type", "application/json");
}
 
function updateOrder($taskId, $target, $db) {
  $nextTask = false;
  $targetId = $target;
 
  if (strpos($target, "next:") === 0) {
    $targetId = substr($target, strlen("next:"));
    $nextTask = true;
  }
 
  if ($targetId == "null") {
    return;
  }
 
  $sql = "SELECT sortorder FROM gantt_tasks WHERE id = :id";
  $statement = $db->prepare($sql);
  $statement->execute([":id" => $targetId]);
 
  $targetOrder = $statement->fetchColumn();
  if ($nextTask) {
    $targetOrder++;
  }
 
  $sql = "UPDATE gantt_tasks SET sortorder = sortorder + 1 ".
    "WHERE sortorder >= :targetOrder";
  $statement = $db->prepare($sql);
  $statement->execute([":targetOrder" => $targetOrder]);
 
  $sql = "UPDATE gantt_tasks SET sortorder = :targetOrder WHERE id = :taskId";
  $statement = $db->prepare($sql);
  $statement->execute([
    ":targetOrder" => $targetOrder,
    ":taskId" => $taskId
  ]);
}

Готовый к использованию демо-доступен на GitHub.

Использование dhtmlxConnector

В качестве альтернативы вы можете настроить серверную часть PHP с помощью библиотеки dhtmlxConnector. Пошаговое руководство доступно здесь.

Безопасность приложения

Имейте в виду, что сам Gantt не защищает от угроз, таких как SQL-инъекции, XSS или CSRF-атаки. Обеспечение безопасности приложения — это ответственность разработчика. Более подробную информацию можно найти здесь.

Устранение неполадок

Если задачи и ссылки не отображаются после выполнения шагов настройки, ознакомьтесь с руководством по устранению неполадок в этой статье.

Что дальше

Теперь у вас есть полностью функциональная диаграмма Ганта. Полный код доступен на GitHub, где вы можете клонировать или скачать его для своих проектов.

Для получения дополнительных функций и интеграций изучите руководства или учебные пособия по интеграции Gantt с другими серверными фреймворками.

К началу