dhtmlxGantt с PHP: Laravel

В этом руководстве описывается, как интегрировать dhtmlxGantt в приложение на Laravel.

Также доступны руководства по серверной интеграции с использованием других платформ:

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

Дополнительно доступно видео, демонстрирующее, как создать диаграмму Gantt с помощью PHP Laravel.

Шаг 1. Инициализация проекта

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

Начните с создания нового приложения Laravel с помощью Composer:

composer create-project laravel/laravel gantt-laravel-app

Этот процесс займет некоторое время для загрузки и настройки всех необходимых файлов. После завершения вы можете убедиться, что всё настроено корректно, выполнив следующие команды:

cd gantt-laravel-app
php artisan serve

На этом этапе вы должны увидеть стандартную приветственную страницу Laravel:

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

Добавление представления

Далее добавьте новую страницу, которая будет содержать 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:

Шаг 3. Создание моделей и миграций

Когда диаграмма 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
{
}

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

Когда база данных и модели готовы, можно загрузить данные в диаграмму 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:

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

В текущий момент диаграмма 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');

Несколько моментов по этому коду:

  • Когда новая задача добавляется, её id возвращается клиенту в свойстве tid ответа.
  • Для параметра progress задаётся значение по умолчанию. Многие параметры запроса необязательны, поэтому если на клиенте они не заданы, то не будут переданы на сервер.
  • В JSON-ответе могут быть дополнительные свойства, все они доступны из обработчика на клиенте.

Далее создадим аналогичный контроллер для связей.

Контроллер для связей

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 с другими серверными фреймворками в решениях.

К началу