dhtmlxScheduler mit PHP:Laravel

Diese Anleitung beschreibt die Integration von dhtmlxScheduler in eine Laravel Anwendung.

Es gibt auch Anleitungen zur Server-seitigen Integration mit anderen Plattformen:

Den kompletten Beispielcode findest du auf GitHub oder folge den untenstehenden Schritt-für-Schritt-Anweisungen.

Der vollständige Quellcode ist auf GitHub verfügbar.

Schritt 1. Initialisierung eines Projekts

Projekt erstellen

Beginne mit dem Erstellen einer neuen Laravel-Anwendung mit Composer:

composer create-project laravel/laravel scheduler-howto-laravel

Dieser Vorgang dauert einen Moment, um alle notwendigen Dateien herunterzuladen und einzurichten. Nach Abschluss kannst du die Einrichtung mit folgendem Befehl überprüfen:

cd scheduler-howto-laravel
php artisan serve

Nun solltest du die Standard-Willkommensseite von Laravel sehen:

Schritt 2. Scheduler zur Seite hinzufügen

Eine View hinzufügen

Als Nächstes fügst du eine neue Seite hinzu, die dhtmlxScheduler enthält. Erstelle eine neue View-Datei mit dem Namen scheduler.blade.php im Verzeichnis resources/views:

resources/views/scheduler.blade.php

<!DOCTYPE html>
<head>
   <meta http-equiv="Content-type" content="text/html; charset=utf-8">
 
   <script src="https://cdn.dhtmlx.com/scheduler/edge/dhtmlxscheduler.js"></script>
   <link href="https://cdn.dhtmlx.com/scheduler/edge/dhtmlxscheduler.css"
        rel="stylesheet">
 
   <style type="text/css">
       html, body{
           height:100%;
           padding:0px;
           margin:0px;
           overflow: hidden;
       }
</style> </head> <body> <div id="scheduler_here" class="dhx_cal_container" style='width:100%; height:100%;'> <div class="dhx_cal_navline"> <div class="dhx_cal_prev_button">&nbsp;</div> <div class="dhx_cal_next_button">&nbsp;</div> <div class="dhx_cal_today_button"></div> <div class="dhx_cal_date"></div> <div class="dhx_cal_tab" name="day_tab"></div> <div class="dhx_cal_tab" name="week_tab"></div> <div class="dhx_cal_tab" name="month_tab"></div> </div> <div class="dhx_cal_header"></div> <div class="dhx_cal_data"></div> </div> <script type="text/javascript">
   scheduler.init("scheduler_here");
</script> </body>

Dies erstellt eine grundlegende HTML-Struktur, bindet die dhtmlxScheduler-Ressourcen vom CDN ein und initialisiert den Scheduler mit der init Methode.

Beachte, dass sowohl für den Body als auch für den Scheduler-Container eine Höhe von 100% festgelegt ist. Da der Scheduler sich an die Größe seines Containers anpasst, ist diese Angabe notwendig.

Die Standardroute ändern

Damit die neue Seite erreichbar ist, passe die Standardroute an, damit der Scheduler beim Aufruf der App angezeigt wird.

Bearbeite routes/web.php, um die Root-Route zu ändern:

routes/web.php

<?php
 
Route::get('/', function () {
    return view('scheduler');
});

Starte die App neu und prüfe, ob die Scheduler-Seite geladen wird:

Schritt 3. Eine Datenbank vorbereiten

Der Scheduler ist derzeit leer. Im nächsten Schritt erfolgt die Anbindung an eine Datenbank und das Befüllen mit Daten.

Eine Datenbank erstellen

Stelle sicher, dass du die Datenbankverbindung in der .env-Datei konfigurierst, zum Beispiel:

.env

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=scheduler-test
DB_USERNAME=root
DB_PASSWORD=

Erstelle anschließend die Model-Klassen und Migrationen mit Artisan:

php artisan make:model Event --migration

Dieser Befehl erzeugt Migrationsdateien im Ordner database/migrations. Definiere darin das Datenbankschema entsprechend der erwarteten Tabellenstruktur für Scheduler.

Hier ist der Migrationscode für die Events-Tabelle:

database/migrations/_create_events_table.php

<?php
 
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class CreateEventsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('events', function (Blueprint $table) {
            $table->increments('id');
            $table->string('text');
            $table->dateTime('start_date');
            $table->dateTime('end_date');
            $table->timestamps();
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('events');
    }
}

Führe die Migration aus, um die Tabelle zu erstellen:

php artisan migrate

Falls du auf einen Fehler wie "Syntax error or access violation: 1071 Specified key was too long; max key length is 1000 bytes" mit älteren MySQL-Versionen stößt, folge den untenstehenden Schritten.

Um das zu beheben, öffne app/Providers/AppServiceProvider.php und ergänze die AppServiceProvider-Klasse wie folgt:

<?php
 
namespace App\Providers;
 
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;  
class AppServiceProvider extends ServiceProvider
{
   public function boot()
   {
       Schema::defaultStringLength(191);    }
   ...
}

Weitere Informationen zu diesem Fehler findest du hier.

Erzeuge als Nächstes Beispieldaten mit einer Seeder-Klasse:

php artisan make:seeder EventsTableSeeder

Füge Beispiel-Events in EventsTableSeeder hinzu:

database/seeds/EventsTableSeeder.php

<?php
use Illuminate\Database\Seeder;
class EventsTableSeeder extends Seeder
{
   public function run()
   {
       DB::table('events')->insert([
           ['id'=>1, 'text'=>'Event #1', 'start_date'=>'2018-12-05 08:00:00',
                'end_date'=>'2018-12-05 12:00:00'],
           ['id'=>2, 'text'=>'Event #2', 'start_date'=>'2018-12-06 15:00:00',
                'end_date'=>'2018-12-06 16:30:00'],
           ['id'=>3, 'text'=>'Event #3', 'start_date'=>'2018-12-04 00:00:00',
                'end_date'=>'2018-12-20 00:00:00'],
           ['id'=>4, 'text'=>'Event #4', 'start_date'=>'2018-12-01 08:00:00',
                'end_date'=>'2018-12-01 12:00:00'],
           ['id'=>5, 'text'=>'Event #5', 'start_date'=>'2018-12-20 08:00:00',
                'end_date'=>'2018-12-20 12:00:00'],
           ['id'=>6, 'text'=>'Event #6', 'start_date'=>'2018-12-25 08:00:00',
                'end_date'=>'2018-12-25 12:00:00']
       ]);
   }
}

Rufe diesen Seeder in DatabaseSeeder.php auf:

database/seeds/DatabaseSeeder.php

<?php
 
use Illuminate\Database\Seeder;
 
class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $this->call(EventsTableSeeder::class);
    }
}

Fülle die Datenbank schließlich mit:

php artisan db:seed

Model-Klassen definieren

Die Daten werden über Eloquent Model-Klassen verarbeitet. Das zuvor erstellte Event-Modell ist bereits einsatzbereit und benötigt keine Anpassungen für die Zusammenarbeit mit dem Scheduler.

Schritt 4. Daten laden

Nachdem die Datenbank eingerichtet und die Models definiert sind, ist der nächste Schritt, die Daten in den Scheduler zu laden. Da der Client die Daten in einem bestimmten Format erwartet, erstelle eine Controller-Action, die JSON im erforderlichen Format ausgibt.

Führe diesen Befehl aus, um den Controller zu erstellen:

php artisan make:controller EventController

Öffne app/Http/Controllers/EventController.php und ergänze die index-Methode:

app/Http/Controllers/EventController.php

<?php
namespace App\Http\Controllers;
use App\Event;  
class EventController extends Controller
{
    public function index(){         $events = new Event();
 
        return response()->json([
            "data" => $events->all()
        ]);
    }
}

Registriere eine Route, damit der Client auf diese Action zugreifen kann. Füge dies zur api.php Routen-Datei hinzu:

routes/api.php

<?php
 
use Illuminate\Http\Request;
 
Route::get('/data', 'EventController@index');

Aktualisiere abschließend die Scheduler-View, um die Daten von diesem Endpunkt zu laden:

resources/views/scheduler.blade.php

scheduler.config.date_format = "%Y-%m-%d %H:%i:%s";
scheduler.init("scheduler_here", new Date(2018, 11, 3), "week");
 
scheduler.load("/api/data", "json");

Die scheduler.load-Methode sendet eine AJAX-Anfrage an die angegebene URL und erwartet eine JSON-Antwort im oben gezeigten Format.

Durch die Angabe des date_format-Werts weiß der Scheduler, welches Datumsformat erwartet wird, und kann die Daten korrekt interpretieren.

Nun sollte der Scheduler die aus der Datenbank geladenen Events anzeigen:

Geladene Events

Dynamisches Laden

Derzeit werden alle Events beim Start des Schedulers auf einmal geladen. Das ist unproblematisch bei kleinen Datenmengen. Für Anwendungen wie Buchungssysteme oder Planungswerkzeuge, bei denen die Datenmenge im Laufe der Zeit wächst, kann das Laden aller Datensätze auf einmal jedoch ineffizient und langsam werden.

Dynamisches Laden löst dieses Problem, indem jeweils nur die Events angefordert werden, die im aktuellen Datumsbereich sichtbar sind. Beim Navigieren zu anderen Daten ruft der Scheduler nur die relevanten Daten ab.

Um dynamisches Laden zu aktivieren, ergänze resources/views/scheduler.blade.php um folgende Zeile:

resources/views/scheduler.blade.php

scheduler.config.date_format = "%Y-%m-%d %H:%i:%s";
 
scheduler.setLoadMode("day");  
scheduler.init("scheduler_here", new Date(2018, 5, 6), "week");
scheduler.load("/api/events", "json");

Passe den Controller an, um Events anhand des angeforderten Datumsbereichs zu filtern:

app/Http/Controllers/EventController.php

class EventController extends Controller
{
    public function index(Request $request){
        $events = new Event();
 
        $from = $request->from;
        $to = $request->to;
 
        return response()->json([
            "data" => $events->
                where("start_date", "<", $to)->
                where("end_date", ">=", $from)->get()
        ]);
    }
}

Schritt 5. Änderungen speichern

Bisher kann der Scheduler Daten vom Backend lesen. Im nächsten Schritt wird das Speichern von Änderungen in der Datenbank ermöglicht.

Der Client arbeitet im REST-Modus und sendet POST-, PUT- und DELETE-Anfragen für Event-Operationen. Weitere Informationen zu Request- und Routenformaten findest du hier.

Nun musst du einen Controller erstellen, der diese Aktionen verarbeitet, entsprechende Routen definieren und das Speichern von Daten auf Client-Seite aktivieren.

Controller hinzufügen

Beginnen wir mit dem Anlegen der Controller. Für jedes Model wird ein RESTful Resource Controller erstellt, der Methoden zum Hinzufügen, Löschen und Aktualisieren des Modells enthält.

Controller für Events

<?php
 
namespace App\Http\Controllers;
 
use Illuminate\Http\Request;
use App\Event;
 
class EventController extends Controller
{
   public function index(Request $request){
       $events = new Event();
 
       $from = $request->from;
       $to = $request->to;
 
       return response()->json([
           "data" => $events->
               where("start_date", "<", $to)->
               where("end_date", ">=", $from)->get()
       ]);
   }
 
   public function store(Request $request){
 
       $event = new Event();
 
       $event->text = strip_tags($request->text);
       $event->start_date = $request->start_date;
       $event->end_date = $request->end_date;
       $event->save();
 
       return response()->json([
           "action"=> "inserted",
           "tid" => $event->id
       ]);
   }
 
   public function update($id, Request $request){
       $event = Event::find($id);
 
       $event->text = strip_tags($request->text);
       $event->start_date = $request->start_date;
       $event->end_date = $request->end_date;
       $event->save();
 
       return response()->json([
           "action"=> "updated"
       ]);
   }
 
   public function destroy($id){
       $event = Event::find($id);
       $event->delete();
 
       return response()->json([
           "action"=> "deleted"
       ]);
   }
}

Und hier ist die entsprechende Route:

routes/api.php

<?php
 
use Illuminate\Http\Request;
 
Route::resource('events', 'EventController');

Einige Hinweise zu diesem Code:

  • Wenn eine neue Aufgabe hinzugefügt wird, antwortet der Server mit deren ID in der tid-Eigenschaft des Antwortobjekts.
  • Der progress-Parameter hat einen Standardwert. Viele Anfrageparameter sind optional, sodass sie nicht an den Server gesendet werden, wenn sie auf der Client-Seite nicht gesetzt wurden.
  • Die JSON-Antwort kann zusätzliche Eigenschaften enthalten, auf die alle im Client-Handler zugegriffen werden kann.

Speichern von Daten auf der Client-Seite aktivieren

Als Nächstes richten wir die Client-Seite ein, damit sie mit der gerade erstellten API funktioniert:

resources/views/scheduler.blade.php

scheduler.config.date_format = "%Y-%m-%d %H:%i:%s";
scheduler.setLoadMode("day");  
scheduler.init("scheduler_here", new Date(2018, 11, 3), "week");
 
scheduler.load("/api/events", "json"); var dp = scheduler.createDataProcessor("/api/events"); dp.init(scheduler);
dp.setTransactionMode("REST");

Damit erhalten Sie einen voll interaktiven Scheduler, in dem Sie Events anzeigen, hinzufügen, aktualisieren und löschen können.

CRUD operations

Weitere Funktionen finden Sie in unseren Guides.

Wiederkehrende Events

Um wiederkehrende Events (wie tägliche Wiederholungen) zu unterstützen, müssen Sie eine Erweiterung zu scheduler.blade.php hinzufügen, das Model anpassen und den Events-Controller aktualisieren.

Beginnen Sie damit, die Recurring-Erweiterung in scheduler.blade.php zu aktivieren:

resources\views\scheduler.blade.php

<!DOCTYPE html>
...
<body>
    ...
    <script type="text/javascript">
        scheduler.plugins({
            recurring: true         });
 
        scheduler.config.date_format = "%Y-%m-%d %H:%i:%s";
        scheduler.init("scheduler_here", new Date(2018, 11, 3), "week");
</script> </body>

Anschließend aktualisieren Sie das Model.

Wenn Sie neu starten, hier das komplette Schema:

Schema::create('events', function (Blueprint $table) {
    $table->increments('id');
    $table->string('text');
    $table->dateTime('start_date');
    $table->dateTime('end_date');
 
    $table->string('rec_type')->nullable();
    $table->bigInteger('event_length')->nullable();
    $table->string('event_pid')->nullable();
 
    $table->timestamps();
});

Alternativ können Sie diese Migration erstellen:

php artisan make:migration add_recurrings_to_events_table --table=events


<?php
 
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class AddRecurringsToEventsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('events', function (Blueprint $table) {
            $table->string('rec_type')->nullable();
            $table->bigInteger('event_length')->nullable()->default(null);
            $table->string('event_pid')->nullable();
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('events', function (Blueprint $table) {
            $table->dropColumn('rec_type');
            $table->dropColumn('event_length');
            $table->dropColumn('event_pid');
        });
    }
}

Führen Sie dann die Migration aus:

php artisan migrate

Nun aktualisieren Sie den Controller.

Das Laden der Daten erfordert keine Änderungen, aber die Schreibaktionen müssen aktualisiert werden, da das Bearbeiten von Serien besondere Schritte erfordert.

Stellen Sie zuerst sicher, dass die neuen Eigenschaften des Event-Models in den Methoden "store" und "update" enthalten sind:

public function store(Request $request){
 
    $event = new Event();
 
    $event->text = strip_tags($request->text);
    $event->start_date = $request->start_date;
    $event->end_date = $request->end_date;
    $event->rec_type = $request->rec_type;
    $event->event_length = $request->event_length;
    $event->event_pid = $request->event_pid;
    $event->save();
 
    return response()->json([
        "action"=> "inserted",
        "tid" => $event->id
    ]);
}
 
public function update($id, Request $request){
    $event = Event::find($id);
 
    $event->text = strip_tags($request->text);
    $event->start_date = $request->start_date;
    $event->end_date = $request->end_date;
    $event->rec_type = $request->rec_type;
    $event->event_length = $request->event_length;
    $event->event_pid = $request->event_pid;
    $event->save();
 
    return response()->json([
        "action"=> "updated"
    ]);
}

Es gibt drei weitere Fälle zu berücksichtigen.

Die wiederkehrende Serie selbst wird als einzelner Datensatz gespeichert, während gelöschte Instanzen innerhalb der Serie als einzelne Datensätze gespeichert werden, die mit der Serie verknüpft und als 'deleted' markiert sind. Wenn der Server auf ein solches Element trifft, sollte er mit dem Status "deleted" antworten. Diese Datensätze können erkannt werden, indem geprüft wird, ob $event->rec_type == "none":

public function store(Request $request){
 
    $event = new Event();
 
    $event->text = strip_tags($request->text);
    $event->start_date = $request->start_date;
    $event->end_date = $request->end_date;
    $event->rec_type = $request->rec_type;
    $event->event_length = $request->event_length;
    $event->event_pid = $request->event_pid;
    $event->save();
 
    $status = "inserted";
    if($event->rec_type == "none"){
        $status = "deleted";
    }
 
    return response()->json([
        "action"=> $status,
        "tid" => $event->id
    ]);
}

Modifizierte Vorkommen werden ebenfalls als einzelne Datensätze gespeichert, die mit der wiederkehrenden Serie verknüpft und mit einem Zeitstempel versehen sind, um das Rendern des ursprünglichen Vorkommens zu verhindern. Wenn ein Benutzer eine modifizierte Instanz löscht, sollte statt des Entfernens rec_type auf "none" gesetzt werden:

public function destroy($id){
    $event = Event::find($id);
 
    // Lösche die modifizierte Instanz der wiederkehrenden Serie
    if($event->event_pid){
        $event->rec_type = "none";
        $event->save();
    }else{
        // Lösche eine normale Instanz
        $event->delete();
    }
 
    $this->deleteRelated($event);
    return response()->json([
        "action"=> "deleted"
    ]);
}

Schließlich sollten beim Aktualisieren oder Löschen einer wiederkehrenden Serie alle modifizierten Vorkommen ebenfalls entfernt werden. Da modifizierte Vorkommen über Zeitstempel mit dem Original verknüpft sind, ist dieser Schritt notwendig:

private function deleteRelated($event){
  if($event->event_pid && $event->event_pid !== "none"){
    Event::where("event_pid", $event->id)->delete();
  }
}
 
public function update($id, Request $request){
        $event = Event::find($id);
 
        $event->text = strip_tags($request->text);
        $event->start_date = $request->start_date;
        $event->end_date = $request->end_date;
        $event->rec_type = $request->rec_type;
        $event->event_length = $request->event_length;
        $event->event_pid = $request->event_pid;
        $event->save();
        $this->deleteRelated($event);         return response()->json([
        "action"=> "updated"
    ]);
}
 
public function destroy($id){
    $event = Event::find($id);
 
    // Lösche die modifizierte Instanz der wiederkehrenden Serie
    if($event->event_pid){
        $event->rec_type = "none";
        $event->save();
    }else{
        // Lösche eine normale Instanz
        $event->delete();
    }
    $this->deleteRelated($event);    return response()->json([
          "action"=> "deleted"
    ]);
}

Wiederkehrende Serien parsen

Ein wiederkehrendes Event wird als einzelner Datensatz in der Datenbank gespeichert, kann aber vom Scheduler auf der Client-Seite in einzelne Vorkommen aufgeteilt werden. Wenn Sie die Daten einzelner Events auf dem Server benötigen, empfiehlt es sich, eine Hilfsbibliothek zum Parsen wiederkehrender Events in PHP zu verwenden.

Sie finden eine fertige Bibliothek auf GitHub.

Anwendungssicherheit

Scheduler selbst bietet keinen eingebauten Schutz gegen Bedrohungen wie SQL-Injections, XSS oder CSRF-Angriffe. Die Sicherheit Ihrer Anwendung liegt in der Verantwortung der Backend-Entwickler. Weitere Details finden Sie im entsprechenden Artikel.

Fehlerbehebung

Wenn Sie die Schritte zur Integration des Schedulers mit PHP befolgt haben, aber keine Events angezeigt werden, lesen Sie den Artikel Fehlerbehebung bei Backend-Integrationsproblemen. Dort finden Sie Hinweise zur Identifikation und Behebung häufiger Probleme.

Wie geht es weiter?

An diesem Punkt haben Sie einen voll funktionsfähigen Scheduler. Der vollständige Code ist auf GitHub verfügbar, wo Sie ihn für Ihre Projekte klonen oder herunterladen können.

Zusätzlich können Sie Guides zu den vielen Funktionen des Schedulers oder Tutorials zur Integration des Schedulers mit anderen Backend-Frameworks erkunden.

Nach oben