В этом руководстве описывается, как интегрировать dhtmlxGantt в приложение на Laravel.
Также доступны руководства по серверной интеграции с использованием других платформ:
Полный исходный код доступен на GitHub.
Дополнительно доступно видео, демонстрирующее, как создать диаграмму Gantt с помощью PHP Laravel.
Начните с создания нового приложения Laravel с помощью Composer:
composer create-project laravel/laravel gantt-laravel-app
Этот процесс займет некоторое время для загрузки и настройки всех необходимых файлов. После завершения вы можете убедиться, что всё настроено корректно, выполнив следующие команды:
cd gantt-laravel-app
php artisan serve
На этом этапе вы должны увидеть стандартную приветственную страницу Laravel:
Далее добавьте новую страницу, которая будет содержать dhtmlxGantt. Перейдите в директорию resources/views и создайте новое представление с именем gantt.blade.php:
resources/views/gantt.blade.php
<!DOCTYPE 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-разметку, подключает ресурсы dhtmlxGantt из CDN и инициализирует диаграмму Gantt с помощью метода init.
Обратите внимание, что и тело документа, и контейнер Gantt имеют высоту 100%. Поскольку Gantt подстраивается под размер контейнера, важно задать эти размеры.
После добавления новой страницы необходимо обеспечить к ней доступ через браузер. В этом примере страница с Gantt будет установлена в качестве основной страницы приложения.
Откройте routes/web.php и обновите маршрут по умолчанию следующим образом:
routes/web.php
<?php
Route::get('/', function () {
return view('gantt');
});
Перезапустите приложение и проверьте, что отображается страница с Gantt:
Когда диаграмма Gantt отображается, следующим шагом будет подключение к базе данных и наполнение её данными.
Не забудьте обновить настройки базы данных в файле .env, например:
.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=gantt-test
DB_USERNAME=root
DB_PASSWORD=
Далее создайте классы моделей и миграции с помощью команд Artisan:
php artisan make:model Task --migration
и
php artisan make:model Link --migration
Затем найдите файлы миграций в папке database/migrations
и определите структуру базы данных.
Ожидаемая Gantt структура базы данных описана здесь.
Пример кода миграции для таблицы Tasks:
database/migrations/_create_tasks_table.php
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTasksTable extends Migration
{
public function up()
{
Schema::create('tasks', function (Blueprint $table){
$table->increments('id');
$table->string('text');
$table->integer('duration');
$table->float('progress');
$table->dateTime('start_date');
$table->integer('parent');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('tasks');
}
}
Ниже приведен код миграции для таблицы Links:
database/migrations/_create_links_table.php
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateLinksTable extends Migration
{
public function up()
{
Schema::create('links', function (Blueprint $table) {
$table->increments('id');
$table->string('type');
$table->integer('source');
$table->integer('target');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('links');
}
}
Запустите миграции командой:
php artisan migrate
Для тестирования удобно сгенерировать тестовые данные. Классы сидеров можно создать такими командами Artisan:
php artisan make:seeder TasksTableSeeder
php artisan make:seeder LinksTableSeeder
Затем создайте папку database/seeds, если она отсутствует, откройте её и добавьте тестовые данные в TasksTableSeeder:
database/seeds/TasksTableSeeder.php
<?php
use Illuminate\Database\Seeder;
class TasksTableSeeder extends Seeder
{
public function run()
{
DB::table('tasks')->insert([
['id'=>1, 'text'=>'Project #1', 'start_date'=>'2017-04-01 00:00:00',
'duration'=>5, 'progress'=>0.8, 'parent'=>0],
['id'=>2, 'text'=>'Task #1', 'start_date'=>'2017-04-06 00:00:00',
'duration'=>4, 'progress'=>0.5, 'parent'=>1],
['id'=>3, 'text'=>'Task #2', 'start_date'=>'2017-04-05 00:00:00',
'duration'=>6, 'progress'=>0.7, 'parent'=>1],
['id'=>4, 'text'=>'Task #3', 'start_date'=>'2017-04-07 00:00:00',
'duration'=>2, 'progress'=>0, 'parent'=>1],
['id'=>5, 'text'=>'Task #1.1', 'start_date'=>'2017-04-05 00:00:00',
'duration'=>5, 'progress'=>0.34, 'parent'=>2],
['id'=>6, 'text'=>'Task #1.2', 'start_date'=>'2017-04-11 00:00:00',
'duration'=>4, 'progress'=>0.5, 'parent'=>2],
['id'=>7, 'text'=>'Task #2.1', 'start_date'=>'2017-04-07 00:00:00',
'duration'=>5, 'progress'=>0.2, 'parent'=>3],
['id'=>8, 'text'=>'Task #2.2', 'start_date'=>'2017-04-06 00:00:00',
'duration'=>4, 'progress'=>0.9, 'parent'=>3]
]);
}
}
Далее обновите DatabaseSeeder.php, чтобы вызвать эти сидеры:
database/seeds/DatabaseSeeder.php
<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run()
{
$this->call(TasksTableSeeder::class);
$this->call(LinksTableSeeder::class);
}
}
Наконец, заполните базу данных тестовыми данными с помощью команды:
php artisan db:seed
Данные обрабатываются через модели Eloquent. Ранее созданные классы задач и связей готовы к использованию с Gantt без изменений.
Однако, чтобы дерево проекта было раскрыто по умолчанию при загрузке задач на клиенте, можно добавить атрибут open в JSON-ответ класса Task. Без этого все ветки будут свернуты при первой загрузке.
Пример модели Task:
/app/Task.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
protected $appends = ["open"];
public function getOpenAttribute(){ return true; }}
Модель Link не требует изменений:
/app/Link.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Link extends Model
{
}
Когда база данных и модели готовы, можно загрузить данные в диаграмму Gantt. Так как клиент ожидает даты в определённом формате, создайте действие контроллера, возвращающее JSON в нужном виде:
app/Http/Controllers/GanttController.php
<?php
namespace App\Http\Controllers;
use App\Task;
use App\Link;
class GanttController extends Controller
{
public function get(){
$tasks = new Task();
$links = new Link();
return response()->json([
"data" => $tasks->all(),
"links" => $links->all()
]);
}
}
Добавьте маршрут для этого действия, чтобы клиент мог запрашивать данные. Этот маршрут будет добавлен в файл маршрутов api.php:
routes/api.php
<?php
use Illuminate\Http\Request;
use App\Http\Controllers\GanttController;
Route::get('/data', 'GanttController@get');
Теперь обновите представление для обращения к этому эндпоинту:
resources/views/gantt.blade.php
gantt.config.date_format = "%Y-%m-%d %H:%i:%s";
gantt.init("gantt_here");
gantt.load("/api/data");
Метод gantt.load отправляет AJAX-запрос по указанному адресу и ожидает JSON-ответ в определённом формате.
Также, указание date_format позволяет Gantt корректно интерпретировать формат дат из источника данных на клиенте.
Теперь, при проверке приложения, задачи должны появиться на диаграмме Gantt:
В текущий момент диаграмма Gantt только читает данные с сервера. Следующим шагом будет обеспечение возможности сохранять изменения обратно в базу данных.
Клиент работает в REST-режиме, отправляя POST/PUT/DELETE-запросы для операций с задачами и связями. Форматы запросов и маршруты, используемые Gantt, описаны здесь.
Для поддержки этого необходимо создать контроллеры для обработки CRUD-операций для обеих моделей, определить маршруты и включить возможность сохранения данных на стороне клиента.
Начните с создания RESTful ресурсных контроллеров для обеих моделей. Эти контроллеры будут содержать методы для добавления, удаления и обновления данных.
app/Http/Controllers/TaskController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Task;
class TaskController extends Controller
{
public function store(Request $request){
$task = new Task();
$task->text = $request->text;
$task->start_date = $request->start_date;
$task->duration = $request->duration;
$task->progress = $request->has("progress") ? $request->progress : 0;
$task->parent = $request->parent;
$task->save();
return response()->json([
"action"=> "inserted",
"tid" => $task->id
]);
}
public function update($id, Request $request){
$task = Task::find($id);
$task->text = $request->text;
$task->start_date = $request->start_date;
$task->duration = $request->duration;
$task->progress = $request->has("progress") ? $request->progress : 0;
$task->parent = $request->parent;
$task->save();
return response()->json([
"action"=> "updated"
]);
}
public function destroy($id){
$task = Task::find($id);
$task->delete();
return response()->json([
"action"=> "deleted"
]);
}
}
И маршрут для него:
routes/api.php
<?php
use Illuminate\Http\Request;
Route::get('/data', 'GanttController@get');
Route::resource('task', 'TaskController');
Несколько моментов по этому коду:
Далее создадим аналогичный контроллер для связей.
app/Http/Controllers/LinkController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Link;
class LinkController extends Controller
{
public function store(Request $request){
$link = new Link();
$link->type = $request->type;
$link->source = $request->source;
$link->target = $request->target;
$link->save();
return response()->json([
"action"=> "inserted",
"tid" => $link->id
]);
}
public function update($id, Request $request){
$link = Link::find($id);
$link->type = $request->type;
$link->source = $request->source;
$link->target = $request->target;
$link->save();
return response()->json([
"action"=> "updated"
]);
}
public function destroy($id){
$link = Link::find($id);
$link->delete();
return response()->json([
"action"=> "deleted"
]);
}
}
И соответствующие маршруты:
routes/api.php
<?php
use Illuminate\Http\Request;
Route::get('/data', 'GanttController@get');
Route::resource('task', 'TaskController');
Route::resource('link', 'LinkController');
Наконец, нужно настроить клиентскую часть для работы с только что созданным API:
resources/views/gantt.blade.php
gantt.config.date_format = "%Y-%m-%d %H:%i:%s";
gantt.init("gantt_here");
gantt.load("/api/data");
var dp = new gantt.dataProcessor("/api");dp.init(gantt);dp.setTransactionMode("REST");
Теперь диаграмма Gantt полностью интерактивна — можно просматривать, добавлять, изменять и удалять задачи и связи.
С дополнительными возможностями dhtmlxGantt вы можете ознакомиться в наших руководствах.
На клиенте Gantt поддерживает перетаскивание задач для изменения порядка. Если эта возможность используется, порядок необходимо сохранять в базе данных. Общие сведения об этом приведены здесь.
Добавим эту функцию в приложение.
Чтобы разрешить пользователям менять порядок задач в интерфейсе, обновите конфигурацию gantt в представлении Index следующим образом:
resources/views/gantt.blade.php
gantt.config.order_branch = true;gantt.config.order_branch_free = true;
gantt.init("gantt_here");
На сервере порядок будет храниться в столбце "sortorder". Полная схема таблицы задач может выглядеть так:
Schema::create('tasks', function (Blueprint $table){
$table->increments('id');
$table->string('text');
$table->integer('duration');
$table->float('progress');
$table->dateTime('start_date');
$table->integer('parent');
$table->integer('sortorder')->default(0);
$table->timestamps();
});
Либо можно добавить миграцию к существующей схеме:
php artisan make:migration add_sortorder_to_tasks_table --table=tasks
Миграционный файл будет содержать:
database/migrations/_add_sortorder_to_tasks_table.php
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSortorderToTasksTable extends Migration
{
public function up()
{
Schema::table('tasks', function (Blueprint $table) {
$table->integer('sortorder')->default(0);
});
}
public function down()
{
Schema::table('tasks', function (Blueprint $table) {
$table->dropColumn('sortorder');
});
}
}
Далее выполните миграцию:
php artisan migrate
Теперь обновим CRUD-операции в контроллерах.
1 . Маршрут GET /data должен возвращать задачи, отсортированные по sortorder
:
app/Http/Controllers/GanttController.php
<?php
namespace App\Http\Controllers;
use App\Task;
use App\Link;
class GanttController extends Controller
{
public function get(){
$tasks = new Task();
$links = new Link();
return response()->json([
"data" => $tasks->orderBy('sortorder')->get(), "links" => $links->all()
]);
}
}
2 . При добавлении новой задачи ей нужно присвоить начальное значение sortorder
:
app/Http/Controllers/TaskController.php
public function store(Request $request){
$task = new Task();
$task->text = $request->text;
$task->start_date = $request->start_date;
$task->duration = $request->duration;
$task->progress = $request->has("progress") ? $request->progress : 0;
$task->parent = $request->parent;
$task->sortorder = Task::max("sortorder") + 1;
$task->save();
return response()->json([
"action"=> "inserted",
"tid" => $task->id
]);
}
3 . И, наконец, при изменении порядка задач сервер должен обновлять их порядок:
app/Http/Controllers/TaskController.php
public function update($id, Request $request){
$task = Task::find($id);
$task->text = $request->text;
$task->start_date = $request->start_date;
$task->duration = $request->duration;
$task->progress = $request->has("progress") ? $request->progress : 0;
$task->parent = $request->parent;
$task->save();
if($request->has("target")){ $this->updateOrder($id, $request->target); }
return response()->json([
"action"=> "updated"
]);
}
private function updateOrder($taskId, $target){
$nextTask = false;
$targetId = $target;
if(strpos($target, "next:") === 0){
$targetId = substr($target, strlen("next:"));
$nextTask = true;
}
if($targetId == "null")
return;
$targetOrder = Task::find($targetId)->sortorder;
if($nextTask)
$targetOrder++;
Task::where("sortorder", ">=", $targetOrder)->increment("sortorder");
$updatedTask = Task::find($taskId);
$updatedTask->sortorder = $targetOrder;
$updatedTask->save();
}
Компонент Gantt сам по себе не содержит защиту от угроз, таких как SQL-инъекции, XSS или CSRF-атаки. Обеспечение безопасности приложения остаётся задачей разработчиков серверной части. Подробнее об этом читайте в соответствующей статье.
Если после выполнения всех шагов Gantt не отображает задачи или связи, статья Устранение проблем интеграции с backend поможет выявить и устранить распространённые проблемы.
Теперь Gantt полностью работоспособен. Полный исходный код доступен на GitHub для клонирования или скачивания и использования в проектах.
Дополнительную информацию о возможностях Gantt ищите в наших руководствах, а также есть инструкции по интеграции Gantt с другими серверными фреймворками в решениях.
К началу