NoteList PHP – Eine ToDo-Liste mit PHP

NoteList PHP ist eine Web-basierte ToDo-Liste. Vom Umfang her ist das Programm auf das wesentliche beschränkt: Listen-Elemente können hinzugefügt und gelöscht werden, abgehakt, und geändert. Darüber hinaus kann die Reihenfolge der einzelnen Elemente gesteuert werden. Erweiterte Features wie verschiedene Listen, oder Multi-User-Fähigkeit, kommen erst in einer späteren Version.

Technisch ist das Tool recht einfach gehalten und basiert auf reinem PHP im Zusammenspiel mit HTML und CSS. Auf JavaScript und Ajax wurde im Sinne der Übersichtlichkeit verzichtet. Dadurch ist die Architektur sehr schlank geblieben, sodass man den Quellcode leicht verstehen kann.

Projektstruktur

Das Tool ist nach einem einfachen MVC-Entwurfsmuster aufgebaut, und besteht aus nur 4 Dateien: controller.php, database.php, index.php, und style.css.

Alles was der User sieht, befindet sich auf der index.php-Datei. Die Datei ruft mithilfe der Database-Klasse die aktuelle Liste ab, und erzeugt den entsprechenden HTML-Code, inklusive der Steuerelement für Änderungen und Eingaben. Die grafische Gestaltung des ganzen, findet wie üblich in der Datei style.css statt.

Die User-Eingaben werden in der Datei controller.php verarbeitet, in der sich die Controller-Klasse befindet. Die Datei wird am Anfang von index.php eingebunden, und mit einem Aufruf der statischen Methode „handleInput“ geprüft, welcher Befehl aufgerufen wurde. Es werden die entsprechenden Parameter validiert, gefiltert, und anschließend die notwendigen Operationen ausgeführt und die Datenbank aktualisiert. Anschließend wird wird mit dem Ablauf von index.php fortgefahren, der aktuelle Status aus der Datenbank aberufen, und der Render-Prozess erneut angestoßen.

Die Datei database.php enthält die Datenbank-Klasse, die Schnittstelle zur MySQL-Datenbank. Hier gibt es vorgefertigte Methoden für alle notwendigen Abfragen oder Aktualisierungen. Diese dienen zum einen dem Controller für Abfragen und Aktualisierungen, aber auch der View (index.php), um den aktuellen Status abfragen, und die korrekte Darstellung erstellen zu können.

Quellcode

Auszug aus index.php
--------------------
...
Controller::handleRequest();
$note_list = Database::getList();
...

<?php
foreach ($note_list as $item):
    ...
    $value = htmlspecialchars($item["value"]);
    ?>

        <div class="list_item row <?php echo $status == 1 ? 'checked' : 'unchecked' ?>">
            <form method="post" action="index.php" class="row">
                <input type="hidden" name="id" value="<?php echo $id ?>">

                <div class="section row">
                    <input type="submit" class="button" name="order_up" value="&#8679;">
                    <input type="submit" class="button" name="order_down" value="&#8681;">
                </div>
                <div class="section section_main row">
                    <input type="text" name="value" value="<?php echo $value ?>">
                </div>
                <div class="section row">
                    <input type="submit" class="button" name="update_value" value="&#128190;">
                    <input type="submit" class="button" name="delete_item" value="&#10060;">
                    <input type="submit" class="button" name="switch_state" value="&#10004;">
                </div>
            </form>
        </div>
        <!-- /list_item -->
	<?php
endforeach;
?>

Der Aufruf von handleRequest() auf dem Controller sorgt für die Verarbeitung etwaiger User-Inputs. Für die Darstellung wird anschließend die gesamte Liste aus der Datenbank abgefragt und in einem Array gespeichert. Danach wird mit einer foreach-Schleife über das Array iteriert, und die relevanten Werte in Variablen Zwischengespeichert. Der Wert „value“ beinhaltet die vom User eingegebene Notiz in Textform. Insbesondere hier ist es daher wichtig, vor der Ausgabe etwaige HTML-Zeichen zu mittels htmlspecialchars zu maskieren, um etwaigen XSS Attacken Vorschub zu leisten.

Die aus der Datenbank abgefragte ID muss für spätere Server-Anfragen auf dem Client zwischengespeichert werden. Dies geschieht in einem versteckten Formular-Element. Der sichtbare Inhalt wird innerhalb des Formulars in einem Textfeld ausgegeben. Auf diese Weise werden beim Übertragen des Formulars sowohl der aktuelle Wert, als auch die ID des betroffenen Items übertragen, sodass der Server / der Controller die Anfrage korrekt zuordnen kann.

Als Steuerelemente dienen verschiedene Submit-Buttons innerhalb des Formulars. Drückt man einen dieser Knöpfe wird das name-Attribut des benutzten Buttons, sowie die input-Felder des Formulars, per POST-Verfahren an den Server gesendet. Der Controller wertet hier nun die Daten aus, und verarbeitet die Anfrage. Anschließend wird in der View eine neue HTML-Datei gerendert, und zurück an den Client gesendet.

Auszug aus controller.php
--------------------

class Controller {
    public static $message = "";
    public static $note_list = null;

    public static function handleRequest() {
        // Neuen Listeneintrag hinzufügen
        if(isset($_POST['add_entry'])) {
            if(isset($_POST['value'])) {
                self::addEntry($_POST['value']);
            }
        }

        else if(isset($_POST['delete_item'])) {
            if(isset($_POST['id'])) {
                self::deleteEntry(filter_var($_POST['id'], FILTER_SANITIZE_NUMBER_INT));
            }
        }

        else if(...)
        ...
    }

    ...

    private static function addEntry($value) {
        Database::addEntry($value);
        self::$message = "Eintrag erfolgreich hinzugefügt";
    }

    ...

}

Der Controller prüft zunächst, welcher Button gedrückt wurde. Dazu wird geprüft ob einer der entsprechenden Schlüssel im POST-Array vorhanden ist. Falls ja, kümmert er sich um die Validierung der Eingaben. Zunächst werden die Eingaben gefiltert, um sie später sicher verarbeiten zu können. Beim Wert „value“ wird auf die Filterung verzichtet, da es für die weitere Verarbeitung oft sinnvoll ist, die Eingabe in ihrer Rohform zu speichern. Daher ist es wichtig, dass in der View alle Ausgabe-Werte mittels htmlspecialchars maskiert werden, welche zuvor von einem Client zum Server übertragen wurden. Andernfalls riskiert man, dass von einem böswilligen Benutzer Code eingeschleust wird, der dann bei nichts-ahnenden Nutzern wieder ausgegben wird (XSS-Attacken).

Auszug aus database.php
--------------------
...

public static function updateEntry($id, $position, $status, $value) {
        $conn = self::connect();

        $stmt = $conn->prepare("UPDATE list_items SET position = ?, status = ?, value = ? WHERE id = ?");
        $stmt->bind_param("iisi", $position, $status, $value, $id);
        
        if (!$stmt) {
            echo "Error: " . $conn->error;
        }

        $stmt->execute();

        echo $stmt->error;
        $stmt->close();

        self::close($conn);
    }

    ...

In der Database-Klasse befindet sich die Logik zur Aktualisierung und Abfrage der Datenbank. Insbesondere die Abfrage kann sowohl von der View, als auch vom Controller erfolgen. Das hinzufügen und ändern von Werten, erfolgt ausschließlich über den Controller.

Die Filterung der Eingaben im Controller, dient vor allem dem Schutz des Programmablaufs, falls sich z.B. bei einem erwarteten Integer-Wert (der ID) ein Buchstabe eingeschlichen hat. Daher können Text-Eingaben quasi ungefiltert eingelesen und gespeichert werden. Der Schutz vor XSS-Attacken findet dabei bei der Ausgabe der gespeicherten Daten statt. Aber was ist mit SQL-Injections? – Vor diesen würden die üblichen Filtermethoden ohnehin nicht schützen, hier greift ein anderes Sicherungssystem: Prepared Statements.

Wenn man in einer Datenbank User-Eingaben speichert kann es passieren, dass ein User SQL-Code in seine Eingabe eingschleust hat. Diese ist nicht sicher zu erkennen, bzw. von validem Inhalt zu unterscheiden, daher kann dieser nicht einfach heraus gefiltert werden. SQL ist traditionell ein Textbasiertes Datenbanksystem, sodass bei der Interpretation einer Eingabe, nun unter Umständen aber auch nicht zwischen der gewünschten Abfrage, und dem eingeschleusten Code unterschieden werden kann. Dadurch wird unter Umständen schädlicher Code ausgeführt.

Um diesem Problem zu begegnen verwendet man Prepared Statements: Hierbei wird der eigentliche SQL-Befehl vorkompiliert. Er wird also nicht mehr in Textform, sondern in Binärform an die SQL-Datenbank übertragen. Alles was man noch hinzufügt sind die User-Eingaben als zusätzliche Parameter. Sollte sich darin nun schädlicher SQL-Code befinden, hat dieser Schlicht keine Auswirkungen.

PHP Only

Eine Besonderheit dieses Programms ist, dass die Geschäftslogik komplett serverseitig über PHP realisiert wurde. Dies hat zur Folge, dass jede Änderung am Inhalt einen komplett neuen Seitenaufruf zur Folge hat. Es werden also nicht nur die geänderten Elemente aktualisiert, sondern die gesamte Seite neu gerendert, und an den Client zurück geschickt. Dadurch springt die Seite nach dem Betätigen eines Buttons nach oben, und Formular-Elemente verlieren ihren vorherigen Fokus.

Wenn man dieses Verhalten vermeiden möchte, wäre der richtige Ansatz die Verarbeitung zumindest teilweise Client-Seitig mittels JavaScript zu lösen. Mittels Ajax würden die Anfragen asynchron an den Server gesendet, und nur die benötigten Daten übertragen, statt die ganze Seite. Der Seitenaufbau würde also nicht bei jedem Klick erneut stattfinden, sondern nur die tatsächlich geänderten Elemente lokal von JavaScript neu gerendert. – In einem Produktivsystem für den Masseneinsatz würde man sogar noch einen Schritt weiter gehen, und direkt ein entsprechendes Frontend-Framework wie Angular oder React einsetzen.

Das Ziel dieses Projekt war allerdings keine Produktivlösung. Ich wollte mich bewusst möglichst auf eine reine PHP-Lösung fokussieren.

Ausprobieren & Code auf GitHub

Wenn du das Programm selbst testen möchtest kannst du das hier tun. Bitte beachte, dass die Einträge des Programms in der Sandbox alle 5 Minuten zurück gesetzt werden.

Falls du es lokal testen möchtest oder dir den Quellcode anschauen möchtest, lade es dir gerne von GitHub runter. Es dürfte auf so ziemlich jeder lokalen oder Internetbasierten PHP-Umgebung mit MySQL-Datenbank laufen.

Datenbank-Konfiguration

Damit du das Programm ausführen kannst, musst du vorher eine MySQL-Datenbank aufsetzen. Die Zugriffsparameter und den Datenbank- sowie Tabellen-Namen, kannst du in der Datei database.php anpassen. Wichtig ist, dass die von dir angelegte Datenbank-Tabelle folgende Werte und Konfiguration beinhaltet:

Ich wünsche dir viel Erfolg, und vor allem viel Spaß beim ausprobieren.