Dieses Tutorial enthält alle wichtigen Details, die Sie benötigen, um einen Scheduler mit PHP zu erstellen, ohne auf Frameworks zurückzugreifen.
Das Setup verwendet MySQL zur Datenspeicherung und das PDO-Interface für den Datenbankzugriff. Um mitzumachen, benötigen Sie PHP 5.4 oder höher mit aktivierter PDO_MYSQL-Erweiterung sowie MySQL oder MariaDB.
Falls Sie an einer serverseitigen Integration mit anderen Plattformen oder Frameworks interessiert sind, stehen Ihnen folgende Tutorials zur Verfügung:
Sie können auch die komplette Demo auf GitHub ansehen und den Schritt-für-Schritt-Anweisungen folgen, um die Anwendung zu erstellen.
Der vollständige Quellcode ist auf GitHub verfügbar.
Beginnen Sie mit dem Anlegen eines neuen Verzeichnisses für Ihre Anwendung.
Erstellen Sie einen leeren Ordner und nennen Sie ihn scheduler-howto-php-plain
.
Erstellen Sie als Nächstes eine Seite, die den Scheduler enthält.
Legen Sie im Ordner scheduler-howto-php-plain
eine Datei namens index.html
an und fügen Sie folgenden Inhalt hinzu:
scheduler-howto-php-plain/index.html
<!doctype html>
<html>
<head>
<title> Getting started with dhtmlxScheduler</title>
<meta 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" type="text/css" charset="utf-8">
<style> html, body{
margin:0px;
padding:0px;
height:100%;
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"> </div>
<div class="dhx_cal_next_button"> </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> scheduler.init('scheduler_here', new Date(2019,0,20), "week");
scheduler.load("data/api.php");
</script>
</body>
</html>
Wenn Sie die App ausführen, sollte der Scheduler auf der Seite erscheinen:
Zu diesem Zeitpunkt ist der Scheduler noch leer. Der nächste Schritt ist das Einrichten einer Datenbank und deren Verbindung mit der Anwendung.
Sie können die Datenbank entweder über Ihren bevorzugten MySQL-Client (wie phpMyAdmin) oder über die Kommandozeile anlegen. Verwenden Sie folgenden SQL-Code, um eine neue Datenbank und eine Tabelle für Kalenderereignisse zu erstellen:
CREATE DATABASE IF NOT EXISTS `scheduler_howto_php`;
USE `scheduler_howto_php`;
DROP TABLE IF EXISTS `events`;
CREATE TABLE `events` (
`id` int(11) AUTO_INCREMENT,
`start_date` datetime NOT NULL,
`end_date` datetime NOT NULL,
`text` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
Wenn Sie die MySQL-Konsole verwenden möchten, speichern Sie den obigen SQL-Code in einer Datei dump.sql und führen Sie dann aus:
$ mysql -uuser -ppass scheduler < mysql_dump.sql
Nachdem die Datenbank bereit ist, können Sie Daten in den Scheduler laden.
Erstellen Sie einen neuen Ordner namens data
in Ihrem Projektverzeichnis.
Definieren Sie zunächst die Datenbankverbindungsdaten in einer Konfigurationsdatei data/config.php
:
data/config.php
<?php
$dsn = "mysql:host=localhost;dbname=scheduler_howto_php";
$username = "root";
$password = "";
$options = array(
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'",
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
);
Stellen Sie sicher, dass Sie „localhost“, „scheduler_howto_php“, „root“ und „“ an Ihre tatsächlichen Datenbankeinstellungen anpassen.
Fügen Sie als Nächstes ein PHP-Skript hinzu, das vom Client aufgerufen wird, um Daten aus der Datenbank zu laden und Änderungen am Scheduler zu speichern.
Erstellen Sie eine Datei namens api.php
im Ordner data
und beginnen Sie mit dem Öffnen einer Datenbankverbindung:
data/api.php
<?php
require_once("config.php");
$db = new PDO($dsn, $username, $password, $options);
Implementieren Sie dann eine Funktion, die Scheduler-Ereignisse aus der Datenbank abruft:
data/api.php
function read($db, $requestParams){
$queryParams = [];
$queryText = "SELECT * FROM `events`";
$query = $db->prepare($queryText);
$query->execute($queryParams);
$events = $query->fetchAll();
return $events;
}
Erstellen Sie anschließend einen Request-Handler, der auf eingehende Anfragen reagiert:
data/api.php
switch ($_SERVER["REQUEST_METHOD"]) {
case "GET":
$result = read($db, $_GET);
break;
case "POST":
// we'll implement this later
break;
default:
throw new Exception("Unexpected Method");
break;
}
header("Content-Type: application/json");
echo json_encode($result);
Fügen Sie Ihrer Datenbank einige Ereignisse hinzu, damit diese im Scheduler angezeigt werden.
Im Moment lädt der Scheduler alle Ereignisse auf einmal aus der Datenbank. Das ist in Ordnung, solange die Datenmenge gering bleibt. Bei Anwendungen wie Buchungs- oder Planungssystemen, in denen sich mit der Zeit viele alte Einträge ansammeln, kann die Menge der übertragenen Daten jedoch erheblich wachsen. Nach einigen Monaten aktiver Nutzung kann die Anwendung bei jedem Laden der Seite mehrere Megabyte an Ereignissen anfordern.
Dies lässt sich vermeiden, indem Sie das dynamische Laden aktivieren. Der Scheduler sendet dann den aktuell angezeigten Datumsbereich als Parameter, sodass der Server nur die Ereignisse zurückgibt, die in diesen Bereich fallen. Jedes Mal, wenn der Nutzer zu einem anderen Zeitraum navigiert, lädt der Scheduler nur die relevanten Daten nach.
Um das dynamische Laden auf der Client-Seite zu aktivieren, verwenden Sie die Option setLoadMode und setzen Sie sie auf "day", "week" oder "month". Fügen Sie z. B. Folgendes zu Ihrem Client-Code hinzu:
index.html
scheduler.init("scheduler_here", new Date(2019, 0, 20), "week");
scheduler.setLoadMode("day");
// load data from the backend
scheduler.load("data/api.php");
Auf der Server-Seite können Sie dies umsetzen, indem Sie die Funktion read
wie folgt anpassen:
data/api.php
function read($db, $requestParams){
$queryParams = [];
$queryText = "SELECT * FROM `events`";
// handle dynamic loading
if (isset($requestParams["from"]) && isset($requestParams["to"])) { $queryText .= " WHERE `end_date`>=? AND `start_date` < ?;"; $queryParams = [$requestParams["from"], $requestParams["to"]]; } $query = $db->prepare($queryText);
$query->execute($queryParams);
$events = $query->fetchAll();
return $events;
}
Bisher kann der Scheduler Daten vom Backend lesen. Der nächste Schritt ist, das Speichern von Änderungen in der Datenbank zu ermöglichen.
Der Client arbeitet im JSON-Modus und sendet POST-Anfragen, um Aktionen an den Ereignissen auszuführen. Details zum Anfrageformat und zu den Routen finden Sie in Serverseitige Integration.
Fügen Sie Funktionen hinzu, um Ereignisse in der Datenbank zu erstellen, zu aktualisieren und zu löschen.
In data/api.php
ergänzen Sie Folgendes:
data/api.php
// create a new event
function create($db, $event){
$queryText = "INSERT INTO `events` SET
`start_date`=?,
`end_date`=?,
`text`=?";
$queryParams = [
$event["start_date"],
$event["end_date"],
$event["text"]
];
$query = $db->prepare($queryText);
$query->execute($queryParams);
return $db->lastInsertId();
}
// update an event
function update($db, $event, $id){
$queryText = "UPDATE `events` SET
`start_date`=?,
`end_date`=?,
`text`=?
WHERE `id`=?";
$queryParams = [
$event["start_date"],
$event["end_date"],
$event["text"],
$id
];
$query = $db->prepare($queryText);
$query->execute($queryParams);
}
// delete an event
function delete($db, $id){
$queryText = "DELETE FROM `events` WHERE `id`=? ;";
$query = $db->prepare($queryText);
$query->execute([$id]);
}
Aktualisieren Sie anschließend den POST-Request-Handler, damit diese Funktionen genutzt werden:
data/api.php
$db = new PDO($dsn, $username, $password, $options);
switch ($_SERVER["REQUEST_METHOD"]) {
case "GET":
$result = read($db, $_GET);
break;
case "POST": $requestPayload = json_decode(file_get_contents("php://input")); $id = $requestPayload->id; $action = $requestPayload->action; $body = (array) $requestPayload->data; $result = [ "action" => $action ]; if ($action == "inserted") {; $databaseId = create($db, $body); $result["tid"] = $databaseId; } elseif($action == "updated") { update($db, $body, $id); } elseif($action == "deleted") { delete($db, $id); } break; default:
throw new Exception("Unexpected Method");
break;
}
header("Content-Type: application/json");
echo json_encode($result);
Wenn ein neues Ereignis erstellt wird, wird dessen Datenbank-ID im tid-Feld der Antwort an den Client zurückgesendet. Die Antwort im JSON-Format kann bei Bedarf zusätzliche Eigenschaften enthalten, die im Client-Handler ausgewertet werden können.
Als Nächstes wird die Client-Seite so eingerichtet, dass sie mit der gerade erstellten API arbeitet:
index.html
scheduler.init("scheduler_here", new Date(2019, 0, 20), "week");
scheduler.setLoadMode("day");
// Daten vom Backend laden
scheduler.load("data/api.php");
// Änderungen an das Backend senden
var dp = scheduler.createDataProcessor({ url: "data/api.php", mode: "JSON" });
Wenn Sie die Anwendung jetzt neu starten, können Sie im Scheduler Ereignisse erstellen, löschen und bearbeiten. Alle Änderungen bleiben auch nach dem Neuladen der Seite erhalten.
An diesem Punkt haben Sie einen einfachen Scheduler, der seine Ereignisse in einer MySQL-Datenbank speichert.
Um wiederkehrende Ereignisse zu ermöglichen (z. B. „Termin täglich wiederholen“), muss eine entsprechende Erweiterung auf der Scheduler-Seite eingebunden werden:
...
<body>
...
<script> scheduler.plugins({
recurring: true });
scheduler.init('scheduler_here', new Date(2019,0,20), "week");
...
</script>
</body>
Die Tabelle „events“ benötigt zusätzliche Spalten, um Informationen zu wiederkehrenden Ereignissen zu speichern. Hier ist eine SQL-Abfrage zum Erstellen einer Tabelle, die wiederkehrende Ereignisse unterstützt:
CREATE DATABASE IF NOT EXISTS `scheduler_howto_php`;
USE `scheduler_howto_php`;
DROP TABLE IF EXISTS `events`;
CREATE TABLE `events` (
`id` int(11) AUTO_INCREMENT,
`start_date` datetime NOT NULL,
`end_date` datetime NOT NULL,
`text` varchar(255) DEFAULT NULL,
`event_pid` int(11) DEFAULT 0,
`event_length` bigint(20) unsigned DEFAULT 0,
`rec_type` varchar(25) DEFAULT '',
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
Alternativ können Sie die bestehende events-Tabelle aus dem vorherigen Schritt wie folgt aktualisieren:
ALTER TABLE `events` ADD COLUMN `event_pid` int(11) DEFAULT '0';
ALTER TABLE `events` ADD COLUMN `event_length` bigint(20) unsigned DEFAULT '0';
ALTER TABLE `events` ADD COLUMN `rec_type` varchar(25) DEFAULT '';
Einige Anpassungen in den PHP-Skripten sind notwendig.
Zuerst muss die SQL-Abfrage für die INSERT
-Aktion um die neuen Spalten erweitert werden.
Anschließend muss ein Sonderfall für wiederkehrende Ereignisse beachtet werden: Das Löschen eines einzelnen Vorkommnisses einer Serie erfordert das Anlegen eines neuen Datensatzes, daher ruft der Client hierfür die insert-Aktion auf:
data/api.php
function create($db, $event){
$queryText = "INSERT INTO `events` SET
`start_date`=?,
`end_date`=?,
`text`=?,
`event_pid`=?, `event_length`=?, `rec_type`=?"; $queryParams = [
$event["start_date"],
$event["end_date"],
$event["text"],
// Spalten für wiederkehrende Ereignisse
$event["event_pid"] ? $event["event_pid"] : 0, $event["event_length"] ? $event["event_length"] : 0, $event["rec_type"] ];
$query = $db->prepare($queryText);
$query->execute($queryParams);
return $db->lastInsertId();
}
Auch der Handler für POST
-Anfragen muss angepasst werden, da der Client vom Server nach dem Einfügen eines übersprungenen Vorkommnisses einen „deleted“-Status erwartet:
data/api.php
switch ($_SERVER["REQUEST_METHOD"]) {
case "GET":
$result = read($db, $_GET);
break;
case "POST":
$requestPayload = json_decode(file_get_contents("php://input"));
$id = $requestPayload->id;
$action = $requestPayload->action;
$body = (array) $requestPayload->data;
$result = [
"action" => $action
];
if ($action == "inserted") {
$databaseId = create($db, $body);
$result["tid"] = $databaseId;
// Einzelnes Vorkommnis aus einer Serie löschen
if ($body["rec_type"] === "none") {
$result["action"] = "deleted"; }
} elseif($action == "updated") {
update($db, $body, $id);
} elseif($action == "deleted") {
delete($db, $id);
}
break;
default:
throw new Exception("Unexpected Method");
break;
}
Auch die Update-Funktion benötigt entsprechende Änderungen in der SQL-Abfrage. Zusätzlich müssen beim Bearbeiten einer Serie alle geänderten Vorkommnisse dieser Serie gelöscht werden:
data/api.php
function update($db, $event, $id){
$queryText = "UPDATE `events` SET
`start_date`=?,
`end_date`=?,
`text`=?,
`event_pid`=?, `event_length`=?, `rec_type`=? WHERE `id`=?";
$queryParams = [
$event["start_date"],
$event["end_date"],
$event["text"],
$event["event_pid"] ? $event["event_pid"] : 0, $event["event_length"] ? $event["event_length"] : 0, $event["rec_type"], $id
];
if ($event["rec_type"] && $event["rec_type"] != "none") { // Alle geänderten Vorkommnisse müssen beim Bearbeiten einer Serie gelöscht werden //https://docs.dhtmlx.com/scheduler/ server_integration.html#recurringevents $subQueryText = "DELETE FROM `events` WHERE `event_pid`=? ;"; $subQuery = $db->prepare($subQueryText); $subQuery->execute([$id]); } $query = $db->prepare($queryText);
$query->execute($queryParams);
}
Schließlich muss die Funktion für die DELETE
-Aktion zwei Sonderfälle behandeln:
Wenn das Ereignis einen nicht-leeren Wert für event_pid
hat, wird eine bearbeitete Instanz einer Serie gelöscht. Statt den Datensatz zu entfernen, wird rec_type='none'
gesetzt, damit der Scheduler dieses Vorkommnis überspringt.
Beim Löschen einer gesamten Serie sollten auch alle bearbeiteten Instanzen dieser Serie gelöscht werden.
data/api.php
function delete($db, $id){
// Logik speziell für wiederkehrende Ereignisse
// https://docs.dhtmlx.com/scheduler/server_integration.html#recurringevents
$subQueryText = "SELECT * FROM `events` WHERE id=? LIMIT 1;";
$subQuery = $db->prepare($subQueryText);
$subQuery->execute([$id]);
$event = $subQuery->fetch();
if ($event["event_pid"]) {
// Löschen einer bearbeiteten Instanz einer Serie
// Wenn ein Ereignis mit event_pid gelöscht wurde, muss es aktualisiert werden
// mit rec_type==none statt zu löschen.
$subQueryText="UPDATE `events` SET `rec_type`='none' WHERE `id`=?;";
$subQuery = $db->prepare($subQueryText);
$subQuery->execute([$id]);
}else{
if ($event["rec_type"] && $event["rec_type"] != "none") { // Beim Löschen einer Serie alle bearbeiteten Instanzen der Serie löschen
$subQueryText = "DELETE FROM `events` WHERE `event_pid`=? ;";
$subQuery = $db->prepare($subQueryText);
$subQuery->execute([$id]);
}
/*
Ende der Verarbeitung für wiederkehrende Ereignisse
*/
$queryText = "DELETE FROM `events` WHERE `id`=? ;";
$query = $db->prepare($queryText);
$query->execute([$id]);
}
}
dhtmlxScheduler arbeitet auf der Client-Seite und enthält keine integrierten Sicherheitsfunktionen, um flexibel zu bleiben. Da Sicherheit auf der Client-Seite allein nicht ausreicht, sind Backend-Entwickler für die Absicherung der Anwendung verantwortlich. Wichtige Punkte:
SQL-Injections: In diesem Beispiel werden für alle Operationen parametrisierte SQL-Abfragen verwendet, was SQL-Injection-Angriffe verhindert.
XSS-Angriffe: Der Client bereinigt Benutzereingaben weder vor dem Senden an das Backend noch werden Serverdaten vor der Anzeige bereinigt. In diesem Beispiel gibt es keine XSS-Filterung, daher sollten Sie diese hinzufügen, wenn Sie dieses Beispiel in Ihrer Anwendung verwenden möchten.
Um XSS-Angriffe zu verhindern, stellen Sie sicher, dass alle Zeichenketten, die in HTML eingefügt werden, korrekt maskiert sind.
In diesem Beispiel reicht es, die „text“-Eigenschaft der Ereignisse beim Laden auf den Client zu escapen:
data/api.php
function read($db, $requestParams){
$queryParams = [];
$queryText = "SELECT * FROM `events`";
if (isset($requestParams["from"]) && isset($requestParams["to"])) {
$queryText .= " WHERE `end_date`>=? AND `start_date` < ?;";
$queryParams = [$requestParams["from"], $requestParams["to"]];
}
$query = $db->prepare($queryText);
$query->execute($queryParams);
$events = $query->fetchAll(PDO::FETCH_ASSOC);
// Unsicheren Text escapen
foreach($events as $index=>$event){
$events[$index]["text"] = htmlentities($event["text"]);
}
return $events;
}
Wenn das Backend eine Aktion nicht ausführen kann, erwartet der Client als Antwort einen „error“-Status.
Dies kann erreicht werden, indem Methodenaufrufe in einen try-catch-Block eingebettet werden. Ersetzen Sie in der Datei data/app.php
den switch-case
-Block durch Folgendes:
data/api.php
try {
switch ($_SERVER["REQUEST_METHOD"]) {
case "GET":
$result = read($db, $_GET);
break;
case "POST":
$requestPayload = json_decode(file_get_contents("php://input"));
$id = $requestPayload->id;
$action = $requestPayload->action;
$body = (array) $requestPayload->data;
$result = [
"action" => $action
];
if ($action == "inserted") {
$databaseId = create($db, $body);
$result["tid"] = $databaseId;
// Einzelnes Vorkommnis aus einer Serie löschen
if ($body["rec_type"] === "none") {
$result["action"] = "deleted"; }
} elseif($action == "updated") {
update($db, $body, $id);
} elseif($action == "deleted") {
delete($db, $id);
}
break;
default:
throw new Exception("Unexpected Method");
break;
}
} catch (Exception $e) {
http_response_code(500);
$result = [
"action" => "error",
"message" => $e->getMessage()
];
}
Auf der Client-Seite können Fehler mit dem onAfterUpdate-Event des dataProcessor abgefangen werden:
index.html
dp.init(scheduler);
dp.attachEvent("onAfterUpdate", function(id, action, tid, response){
if(action == "error"){
// Hier kann etwas unternommen werden
}
});
Falls Scheduler nach diesen Schritten zur PHP-Integration keine Ereignisse anzeigt, finden Sie Hinweise zur Fehlersuche im Artikel Fehlerbehebung bei Backend-Integrationsproblemen.
Mit einem voll funktionsfähigen Scheduler können Sie sich den vollständigen Code auf GitHub ansehen, wo er zum Klonen oder Herunterladen für Ihre Projekte bereitsteht.
Außerdem finden Sie weitere Informationen in den Anleitungen zu den vielen Funktionen des Schedulers oder in Tutorials zur Integration des Schedulers mit anderen Backend-Frameworks.
Nach oben