Перейти к основному содержимому

dhtmlxGantt с PHP: Laravel

Этот учебник описывает, как добавить dhtmlxGantt в приложение Laravel.

Есть инструкции, предназначенные для построения серверной интеграции с помощью других платформ:

заметка

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

Вы можете посмотреть видео-руководство, которое показывает, как создать диаграмму Gantt с использованием PHP Laravel.

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

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

Создайте новое приложение с помощью Composer:

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

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

cd gantt-laravel-app
php artisan migrate

Теперь вы можете запустить сервер:

php artisan serve

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

how_to_start_laravel_blank_page

Шаг 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 с использованием метода .

Обратите внимание, что мы также задали 100% высоту для тела документа и для контейнера gantt. Gantt будет использовать размер своего контейнера, поэтому нужны некоторые начальные размеры.

Изменение маршрута по умолчанию

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

Перейдите в routes/web.php и измените маршрут по умолчанию:

routes/web.php
<?php

Route::get('/', function () {
return view('gantt');
});

Запустите приложение снова, чтобы убедиться, что всё сработало:

how_to_start_laravel_empty_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

Пока мы здесь, можно сгенерировать тестовые данные для нашего приложения. Сгенерируйте seeder класс с помощью команды artisan:

php artisan make:seeder TasksTableSeeder
php artisan make:seeder LinksTableSeeder

Теперь откройте папку database/seeders и добавьте данные в TasksTableSeeder:

database/seeds/TasksTableSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class TasksTableSeeder extends Seeder
{
public function run(): void
{
DB::table('tasks')->truncate();
DB::table('tasks')->insert([
['id'=>1, 'text'=>'Project #1', 'start_date'=>'2025-04-01 00:00:00',
'duration'=>5, 'progress'=>0.8, 'parent'=>0],
['id'=>2, 'text'=>'Task #1', 'start_date'=>'2025-04-06 00:00:00',
'duration'=>4, 'progress'=>0.5, 'parent'=>1],
['id'=>3, 'text'=>'Task #2', 'start_date'=>'2025-04-05 00:00:00',
'duration'=>6, 'progress'=>0.7, 'parent'=>1],
['id'=>4, 'text'=>'Task #3', 'start_date'=>'2025-04-07 00:00:00',
'duration'=>2, 'progress'=>0, 'parent'=>1],
['id'=>5, 'text'=>'Task #1.1', 'start_date'=>'2025-04-05 00:00:00',
'duration'=>5, 'progress'=>0.34, 'parent'=>2],
['id'=>6, 'text'=>'Task #1.2', 'start_date'=>'2025-04-11 00:00:00',
'duration'=>4, 'progress'=>0.5, 'parent'=>2],
['id'=>7, 'text'=>'Task #2.1', 'start_date'=>'2025-04-07 00:00:00',
'duration'=>5, 'progress'=>0.2, 'parent'=>3],
['id'=>8, 'text'=>'Task #2.2', 'start_date'=>'2025-04-06 00:00:00',
'duration'=>4, 'progress'=>0.9, 'parent'=>3]
]);
}
}

Затем повторите то же для LinksTableSeeder:

database/seeders/LinksTableSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class LinksTableSeeder extends Seeder
{
public function run(): void
{
DB::table('links')->truncate();
DB::table('links')->insert([
['id' => 1, 'source' => 2, 'target' => 3, 'type' => 0],
['id' => 2, 'source' => 3, 'target' => 4, 'type' => 1]
]);
}
}

И вызывайте seeders таблиц из DatabaseSeeder.php:

database/seeds/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use App\Models\Task;
use App\Models\Link;
use Database\Seeders\TasksTableSeeder;
use Database\Seeders\LinksTableSeeder;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call(TasksTableSeeder::class);
$this->call(LinksTableSeeder::class);
}
}

После этого можно заполнить базу данных из командной строки:

php artisan db:seed

Определение классов моделей

Данные управляются через Eloquent model классы. Мы уже сгенерировали классы для задач и связей на предыдущем этапе. Они готовы к использованию и не требуют изменений для работы с gantt.

Однако мы можем добавить в модель Task открытый атрибут до JSON-ответа. Это развернёт дерево проекта, когда задачи будут загрузены на клиентскую сторону. В противном случае все ветви будут по умолчанию закрыты:

Модель Task будет выглядеть так, как в примере:

/app/Models/Task.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
protected $appends = ["open"];

public function getOpenAttribute(){
return true;
}
}

А модель Link не требует изменений:

/app/Models/Link.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Link extends Model
{
}

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

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

app/Http/Controllers/GanttController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use App\Models\Task;
use App\Models\Link;

class GanttController extends Controller
{
public function get(): JsonResponse
{
$tasks = Task::all();
$links = Link::all();

return response()->json([
"tasks" => $tasks,
"links" => $links
]);
}
}

И зарегистрируем маршрут, чтобы клиент мог вызвать это действие. Учтите, что мы добавим маршрут в файл маршрутов api.php:

routes/api.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\GanttController;

Route::get('/data', [GanttController::class, 'get']); /*!*/

Теперь настройте Laravel так, чтобы корректно загружать API-маршруты. Создайте RouteServiceProvider.php командой:

php artisan make:provider RouteServiceProvider

Затем обновите файл следующим содержимым:

Providers/RouteServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
Route::prefix('api')
->middleware('api')
->group(base_path('routes/api.php'));
}
}

И, наконец, вызовите это действие из представления:

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-запрос к указанному URL и ожидает JSON-ответ, как мы определили ранее.

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

Если сейчас проверить приложение, вы увидите, что в нашем gantt-диаграмме появились задачи:

how_to_start_laravel_complete

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

Пока что наш gantt может читать данные с бэкенда. Давайте сделаем его записывать изменения обратно в базу данных.

Клиентская сторона будет работать в REST-режиме, то есть будет отправлять POST/PUT/DELETE-запросы для действий с задачами и связями. Формат запросов и все маршруты, которые будет использовать gantt, можно найти здесь.

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

Добавление контроллеров

Начнем с контроллеров. Создадим один RESTful resource controller для каждой модели. Он будет содержать методы для добавления/удаления и обновления модели.

Контроллер для задач

app/Http/Controllers/TaskController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Task;

class TaskController extends Controller
{
public function store(Request $request)
{
$data = $request->only(['text', 'start_date', 'duration', 'parent']);
$data['progress'] = $request->input('progress', 0);

$task = Task::create($data);

return response()->json([
'action' => 'inserted',
'tid' => $task->id,
]);
}

public function update(Request $request, $id)
{
$task = Task::findOrFail($id);

$data = $request->only(['text', 'start_date', 'duration', 'parent']);
$data['progress'] = $request->input('progress', 0);

$task->update($data);

return response()->json([
'action' => 'updated',
]);
}

public function destroy($id)
{
$task = Task::findOrFail($id);
$task->delete();

return response()->json([
'action' => 'deleted',
]);
}
}

Конфигурация модели Task

Чтобы методы контроллера работали, необходимо сконфигурировать модель Task для массового присваивания. Защита Laravel от массового присваивания требует явно указать, какие атрибуты можно заполнять с помощью методов create() и update(). Обновите модель Task, чтобы включить свойство $fillable:

app/Models/Task.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
protected $fillable = ['text', 'start_date', 'duration', 'progress', 'parent']; /*!*/
protected $appends = ["open"];

public function getOpenAttribute(){
return true;
}
}

Массив $fillable указывает, какие поля могут быть массово заполняемыми. Это механизм безопасности, который защищает от обновления нежелательных полей через пользовательский ввод.

И маршрут к нему:

routes/api.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\GanttController;
use App\Http\Controllers\TaskController;

Route::get('/data', [GanttController::class, 'get']);
Route::resource('task', TaskController::class); /*!*/

Несколько замечаний по этому коду:

  • Когда новая задача вставляется, мы возвращаем её id клиенту в свойстве tid в ответном объекте
  • Мы задаём значение по умолчанию параметра progress. Многие параметры запроса являются необязательными, что означает: если для задачи на клиентской стороне они не заданы, они не будут отправлены на сервер
  • Ответ JSON может содержать любое количество дополнительных свойств, к которым можно обратиться из клиентского обработчика

Теперь давайте реализуем то же для контроллера Link.

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

app/Http/Controllers/LinkController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Link;

class LinkController extends Controller
{
public function store(Request $request)
{
$link = Link::create($request->only(['type', 'source', 'target']));

return response()->json([
'action' => 'inserted',
'tid' => $link->id,
]);
}

public function update(Request $request, $id)
{
$link = Link::findOrFail($id);
$link->update($request->only(['type', 'source', 'target']));

return response()->json([
'action' => 'updated',
]);
}

public function destroy($id)
{
$link = Link::findOrFail($id);
$link->delete();

return response()->json([
'action' => 'deleted',
]);
}
}

Актуализируем модель Link аналогично Task для массового присваивания:

app/Models/Link.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Link extends Model
{
protected $fillable = ['type', 'source', 'target']; /*!*/
}

И её маршруты:

routes/api.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\GanttController;
use App\Http\Controllers\TaskController;
use App\Http\Controllers\LinkController;

Route::get('/data', [GanttController::class, 'get']);
Route::resource('task', TaskController::class);
Route::resource('link', LinkController::class); /*!*/

Включение сохранения данных на клиентской стороне

Наконец, мы настроим клиентскую часть для использования только что реализованного API:

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

const dp = gantt.createDataProcessor({ /*!*/
url: "/api", /*!*/
mode: "REST" /*!*/
}); /*!*/

Теперь у вас полностью интерактивная диаграмма Gantt с возможностью просматривать, добавлять, обновлять и удалять задачи и связи.

how_to_start_laravel_crud

Пожалуйста, смотрите другие наши решения для более подробных возможностей dhtmlxGantt, а также руководства по интеграции Gantt с другими фреймворками бэкенда.

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

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

Давайте добавим эту возможность в наше приложение.

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

Сначала нужно разрешить пользователям менять порядок задач в интерфейсе. Откройте представление Index и обновите конфигурацию gantt:

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 Illuminate\Http\JsonResponse;
use App\Models\Task;
use App\Models\Link;

class GanttController extends Controller
{
public function get(): JsonResponse
{
$tasks = Task::orderBy('sortorder')->get(); /*!*/
$links = Link::all();

return response()->json([
"tasks" => $tasks,
"links" => $links
]);
}
}

2 . Новые задачи должны получать начальное значение sortorder:

app/Http/Controllers/TaskController.php
public function store(Request $request)
{
$data = $request->only(['text', 'start_date', 'duration', 'parent']);
$data['progress'] = $request->input('progress', 0);
$data['sortorder'] = Task::max('sortorder') + 1;

$task = Task::create($data);

return response()->json([
'action' => 'inserted',
'tid' => $task->id,
]);
}

3 . Наконец, когда пользователь меняет порядок задач, порядковые номера должны обновляться:

app/Http/Controllers/TaskController.php
public function update(Request $request, $id)
{
$task = Task::findOrFail($id);

$data = $request->only(['text', 'start_date', 'duration', 'parent']);
$data['progress'] = $request->input('progress', 0);

$task->update($data);

if ($request->has('target')) {
$this->updateOrder($id, $request->input('target'));
}

return response()->json([
'action' => 'updated',
]);
}

private function updateOrder($taskId, $target)
{
$nextTask = false;
$targetId = $target;

if (str_starts_with($target, 'next:')) {
$targetId = substr($target, strlen('next:'));
$nextTask = true;
}

if ($targetId === 'null') {
return;
}

$targetTask = Task::find($targetId);
if (!$targetTask) {
return;
}

$targetOrder = $targetTask->sortorder;
if ($nextTask) {
$targetOrder++;
}

Task::where('sortorder', '>=', $targetOrder)->increment('sortorder');

$updatedTask = Task::find($taskId);
$updatedTask->sortorder = $targetOrder;
$updatedTask->save();
}

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

Gantt не предоставляет средств защиты от различных угроз, таких как SQL-инъекции, XSS и CSRF-атаки. Важным является то, что ответственность за безопасность приложения лежит на разработчиках, реализующих бэкенд. Подробнее читайте в соответствующей статье: guides/app-security.md).

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

Если вы выполнили вышеуказанные шаги по интеграции Gantt с PHP, но Gantt не рендерит задачи и связи на странице, посмотрите статью . Она описывает способы выявления корня проблемы.

Что дальше

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

Вы также можете ознакомиться с нашими решениями по разнообразным функциям gantt или с руководствами по интеграции Gantt с другими бэкэнд-фреймворками (интеграционные решения).

Need help?
Got a question about the documentation? Reach out to our technical support team for help and guidance. For custom component solutions, visit the Services page.