1unitedpower: Todo-App mit PHP und SQL

3 25

Todo-App mit PHP und SQL

  1. 0

    1. Teil: Einführung und Vorbereitung

  2. 0

    2. Teil: Domain Model

  3. 0

    3. Teil: Resource Model

  4. 0

    4. Teil: Repository

  5. 0

    5. Teil: View

  6. 0

    6. Teil: Controller

  7. 0

    7. Teil: Anwendung verkabeln

  8. 0
    1. 0
      1. 0
        1. 0
          1. 0
            1. 0
    2. 0
      1. 0
      2. 0
      3. 0
  9. 1
    1. 0
      1. 0
    2. 0
  10. 1
  11. 0
    1. 0

Moin!

Ich habe einen kleinen Artikel darüber geschrieben, wie man mit PHP und SQL eine kleine Todo-App erstellt. Der Fokus liegt auf dem MVC-Entwurfsmuster und dem Repository-Pattern. Ihr dürft ihn gerne für euer Wiki verwenden, ich bin nur nicht so fit mit der Wiki-Syntax und mich darin einzuarbeiten ist mir zu zeitintensiv. Wenn sich niemand findet, oder wenn kein Bedarf daran besteht, ist das für mich auch nicht weiter tragisch.

Den Artikel habe ich für das Forum wieder aufgeteit.

LG

  1. TodoApp Tutorial

    In diesem Tutorial entwickeln wir mit dir eine simple Todo-Liste mit PHP und SQL. Die Anwendung erlaubt es neue Todos auf die Liste zu schreiben und existierende Todos abzuhaken, zu bearbeiten und zu löschen. Du solltest schon etwas Erfahrung mit PHP, SQL und objektorienter Programmierung mitbringen. Du lernst zwei Entwurfsmuster kennen, die sich großer Beliebheit erfreuen und in vielen PHP-Frameworks Anwendung finden. Unsere Varianten sind besonders auf Verständlichkeit ausgelegt. Zum einen lernst du das Model-View-Controller-Pattern kennen, das den Programmablauf in verschiedene Unteraufgaben aufteilt und so für Übersichtlichkeit sorgt. Zum anderen lernst du das Repository-Pattern kennen, das dazu dient das sogenannte Domain Model von der Speicherschicht zu trennen.

    Vorbereitung

    Du brauchst für dieses Tutorial mindestens PHP Version 7.3 und eine SQL-Datenbank. Die Datenbank hat das folgende Schema:

    CREATE TABLE IF NOT EXISTS
        `todo`
        (
            `id` MEDIUMINT NOT NULL AUTO_INCREMENT,
            `description` VARCHAR(255) NOT NULL,
            `state` TINYINT(1) NOT NULL ,
            PRIMARY KEY (id)
        )
    

    Vorgehensweise

    Unsere vorgehensweise ist wie folgt: Wir definieren zuerst woraus ein Todo-Item besteht, das Ergebnis ist unser sogeanntes Domain Model. Danach entwickeln wir die Datenbank-Schicht, die wir zum Speichern und Laden von Todos aus unserer Datenbank brauchen. Dazu gehören das sogenannte Resource Model und das Repository. Wenn wir damit fertig sind, definieren wir den View, also die Komponente, die für die Ausgabe in HTML zuständig ist. Danach können wir den Controller definieren. Der Controller ist dafür da die Formulardaten auszuwerten, er verwendet die Speicherschicht, um die Todos in der Datenbank zu aktualisieren und den View um die Ausgabe vorzubereiten. Erst im letzten Schritt fügen wir alle Komponenten zu einer Anwendung zusammen, was nach dieser intensiven Vorbereitung nun ein Leichtes ist.

    Um dir eine Übersicht zu verschaffen ist hier schon mal die Ordnerstruktur unserer Anwendung gezeigt, am besten legst du die Dateien jetzt schon mal an.

    • src/Domain/TodoItem.php
    • src/Persistence/TodoItemRepository.php
    • src/Persistence/TodoItemResource.php
    • src/View/TodoListView.php
    • src/Controller/TodoController.php
    • public/index.php
  2. Domain Model

    Ein Todo besteht aus einer textuellen Beschreibung der Aufgabe und einem Zustand, der entweder "ausstehend" oder "erledigt" sein kann. Wir definieren dafür die Klasse TodoItem in der Datei src/Domain/TodoItem.php. Die möglichen Zustände definieren wir als Klassen-Konstanten, weil ein Name wie TodoItem::STATE_PENDING aussagekräfitger als die Zahl 0 ist. Das verbessert die Lesbarkeit unseres Quelltextes.

    <?php
    declare(strict_types=1);
    
    namespace SelfHtml\Todo\Domain;
    
    /**
     * Ein TodoItem repräsentiert eine Aufgabe. Zu einer Aufgabe gehört eine Beschreibung und einen Zustand,
     * der anzeigt ob die Aufgabe bereits erledigt wurde oder nicht.
     */
    final class TodoItem
    {
        /**
         * @var int
         */
        private $state;
    
        /**
         * @var string
         */
        private $description;
    
        /**
         * Repräsentiert den Zustand einer noch nicht erfüllten Aufgabe.
         *
         * @var int
         */
        public const STATE_PENDING = 0;
    
        /**
         * Repräsentiert den Zustand einer erledigten Aufgabe.
         *
         * @var int
         */
        public const STATE_DONE = 1;
    
        /**
         * Erzeugt eine Aufgabe mit dem übergebenen Zustand und textueller Beschreibung.
         *
         * @param int $state Zustand der Aufgabe
         * @param string $description Textuelle Beschreibung der Aufgabe
         */
        public function __construct(int $state, string $description)
        {
            $this->state = $state;
            $this->description = $description;
        }
    
        /**
         * Gibt den Zustand der Aufgabe zurück.
         *
         * @return int
         */
        public function getState() : int
        {
            return $this->state;
        }
    
        /**
         * Gibt die textuelle Beschreibung der Aufgabe zurück.
         *
         * @return string
         */
        public function getDescription() : string
        {
            return $this->description;
        }
    }
    
    
  3. Persistenz-Schicht

    Die Persistenz-Schicht dient zum Speichern, Laden und Löschen von Todos in der Datenbank. Sie besteht aus zwei Komponenten: dem sogenannten Resource Model und dem Repository . Das Resource Model kümmert sich um die Verwaltung eines einzelnen Domain Models, also ein Objekt von der Klasse TodoItem, das Repository kümmert sich um die Verwaltung aller Resourcen.

    Resource Model

    Das Resource Model repräsentiert einen Datensatz in der Datenbank. Es ist dafür verantwortlich ein TodoItem aus dem Datensatz auszulesen, den Datensatz mit einem aktualisierten TodoItem zu überschreiben und den Datensatz zu löschen. Für die drei Operationen hat die folgende Klasse die drei korrespondierenden Methoden fetch, update, delete. Weil die Methoden sich sehr ähneln, erklären wir im Anschluss nur die update-Methode genauer, du solltest aber versuchen alle Methoden zu verstehen. Wir haben über jede Methode PHP-Kommentare geschrieben, die dir beim Verstehen helfen sollen.

    <?php
    declare(strict_types=1);
    
    namespace SelfHtml\Todo\Persistence;
    
    use \PDO;
    use \OutOfBoundsException;
    use \SelfHtml\Todo\Domain\TodoItem;
    
    /**
     * Eine TodoItemResource repräsentiert einen existierenden Datensatz, der ein TodoItem enthält.
     * Der Datensatz wird durch eine eindeutige ID identifiziert, in diesem Fall ist
     * das eine von SQL generierte ID. Die TodoItemResource ist dafür verantortwortlich den Datensatz
     * auszulesen, ihn zu überschreiben und zu löschen.
     */
    final class TodoItemResource
    {
        /**
         * @var PDO
         */
        private $pdo;
    
        /**
         * @var int
         */
        private $id;
    
        /**
         * Erzeugt eine Resource mit der angebenenen Datenbank-Verbindung und Datensatz-Id.
         * Der Konstruktur erstellt keinen neuen Datensatz in der Datenbank, er muss
         * stattdessen mit der Id eines bereits existierenden Datensatzes aufgerufen werden.
         * Für die Erstellung neuer Datensätze ist das Reposory verantwortlich.
         *
         * @param PDO $pdo Datenbank-Verbindung
         * @param int $id Datensatz-ID
         */
        public function __construct(PDO $pdo, int $id)
        {
            $this->pdo = $pdo;
            $this->id = $id;
        }
    
        /**
         * Gibt die Datensatz-ID der Ressource zurück.
         *
         * @return int
         */
        public function getId() : int
        {
            return $this->id;
        }
    
        /**
         * Liest das TodoItem aus dem Datensatz aus.
         *
         * @throws OutOfBoundsException falls der zu zulesende Datensatz nicht existiert.
         *
         * @return TodoItem
         */
        public function fetch() : TodoItem
        {
            static $query = 'SELECT `state`, `description` FROM `todo` WHERE `id` = :id';
            $statement = $this->pdo->prepare($query);
            $result = $statement->execute([':id' => $this->id]);
            if ($statement->rowCount() === 0) {
                throw new OutOfBoundsException();
            }
            $row = $statement->fetch(PDO::FETCH_ASSOC);
            $todoItem = new TodoItem((int) $row['state'], $row['description']);
            return $todoItem;
        }
    
        /**
         * Überschreibt den Datensatz mit einem neuen TodoItem.
         *
         * @throws OutOfBoundsException falls der zu aktualisierende Datensatz nicht existiert.
         *
         * @param TodoItem neuer Datensatz
         */
        public function update(TodoItem $todoItem) : void
        {
            static $query = <<<SQL
                UPDATE `todo`
                SET `state` = :state, `description` = :description
                WHERE `id` = :id
                SQL;
            $statement = $this->pdo->prepare($query);
            $statement->execute([
                ':id' => $this->id,
                ':state' => $todoItem->getState(),
                ':description' => $todoItem->getDescription()
            ]);
            if ($statement->rowCount() === 0) {
                throw new OutOfBoundsException();
            }
        }
    
        /**
         * Löscht den Datensatz.
         *
         * @throws OutOufBoundsException falls der zu löschende Datensatz nicht exisitert.
         */
        public function delete() : void
        {
            static $query = <<<SQL
                DELETE FROM `todo`
                WHERE `id` = :id
                SQL;
            $statement = $this->pdo->prepare($query);
            $statement->execute([
                ':id' => $this->id
            ]);
            if ($statement->rowCount() === 0) {
                throw new OutOfBoundsException();
            }
        }
    }
    
    

    Die update-Methode bekommt ein TodoItem als Parameter übergeben, sie soll den entsprechenden Datensatz damit überschreiben. Zurückgeben soll die Methode nichts. In der Variablen $query speichern wir das Grundgerüst der SQL-Abrage, die wir für die Änderung in der Datenbank brauchen. Die SQL-Vorlage enthält die Platzhalter :state, :description, die stellvertrent für den Zustand und die Beschreibung eines Todos stehen und einen dritten Platzhalter :id für die Datensatz-Id. In der darauffolgenden Zeile, wird aus dem Grundgerüst ein Datenbank-Statement erzeugt. Erst im dritten Schritt werden mit execute die tatsächlichen Werte für die Platzhalter eingesetzt und der Schreibvorgang durchgeführt. Dieser Schritt kann schief gehen, wenn der Datensatz in der Zwischenzeit gelöscht wurde. Wir fragen deshalb mit rowCount ab wieviele Datensätze von der Löschanfrage betroffen waren. Sollte kein Datensatz gelöscht worden sein melden wir eine OutOfBoundsException.

  4. Repository

    Das Repository repräsentiert eine Datensatz-Sammlung, es kapselt den Zugriff auf die Ressourcen. Mit dem Repository können wir neue Datensätze erzeugen, eine spezfisiche Ressource auslesen oder alle Ressourcen nacheinander auslesen. Dafür hat unsere Repository-Klasse die drei Methoden get, all und add. Wir erklären im Anschluss die beiden Methoden all und add. Die get-Methode ist etwas leichter zu verstehen und wird deshalb nicht erklärt.

    <?php
    declare(strict_types=1);
    
    namespace SelfHtml\Todo\Persistence;
    
    use \PDO;
    use \Iterator;
    use \SelfHtml\Todo\Domain\TodoItem;
    
    /**
     * Ein TodoItemRepository verwaltet den Zugriff auf TodoItemResources.
     */
    final class TodoItemRepository
    {
        /**
         * @var PDO
         */
        private $pdo;
    
        /**
         * Erzeugt ein Repository mit der angebenenen Datenbank-Verbidnung
         *
         * @param PDO $pdo Datenbank-Verbindung
         */
        public function __construct(PDO $pdo)
        {
            $this->pdo = $pdo;
        }
    
        /**
         * Gibt eine Resource zurück, die den Datensatz mit der angebenenen Id repräsentiert.
         *
         * @param int $id Datensatz-ID
         * @return TodoItemResource
         */
        public function get(int $id) : TodoItemResource
        {
            return new TodoItemResource($this->pdo, $id);
        }
    
        /**
         * Gibt eine Liste aller Resourcen zurück.
         *
         * @return Iterator
         */
        public function all() : Iterator
        {
            static $query = 'SELECT `id` FROM `todo`';
            $statement = $this->pdo->prepare($query);
            $result = $statement->execute();
            foreach ($statement as $row) {
                yield new TodoItemResource($this->pdo, (int) $row['id']);
            }
        }
    
        /**
         * Erzeugt einen neuen Datensatz, der das übergebene Todo-Item repräsentiert.
         * Gibt eine Resource zurück, die den neuen Datensatz repräsentiert.
         *
         * @param TodoItem $todoItem Initialer Inhalt des neuen Datensatzes
         * @return TodoItemResource Resource, die den neuen Datensatz repräsentiert.
         */
        public function add(TodoItem $todoItem) : TodoItemResource
        {
            static $query = <<<SQL
                INSERT INTO `todo`
                SET `state` = :state, `description` = :description
                SQL;
            $statement = $this->pdo->prepare($query);
            $result = $statement->execute([
                ':state' => $todoItem->getState(),
                ':description' => $todoItem->getDescription()
            ]);
            return new TodoItemResource($this->pdo, (int) $this->pdo->lastInsertId());
        }
    }
    
    

    Die all-Methode liest zunächst alle Datensatz-IDs aus der Datenbank aus. Dann iterieren wir in einer foreach-Schleife über alle Ergebnis-Zeilen der SQL-Anfrage. In jedem Schleifendurchlauf erstellen wir eine TodoItemResource-Instanz, die den Datensatz mit der entsprechenden ID repräsentiert. Wir möchten eine Liste dieser Instanzen zurückgeben, das machen wir indem wir das Keyword yield vor jeder Insanz notieren. Das Ergebnis der Methode ist dann ein Iterator, über den man mit einer foreach-Schleife itererieren kann.

    Die add-Methode funktioniert ähnlich wie die update-Methode der TodoItemResource-Klasse. Im Unterschied dazu gibt add aber eine TodoItemResource zurück, die den neu angelegten Datensatz repräsentiert. Dafür brauchen wir die lastInsertId-Methode von unserer Datenbank-Verbindung.

  5. Darstellungs-Schicht

    Als nächstes kümmern wir uns um die Ausgabe in HTML. Wir werden nur ein einziges Formular brauchen, das gleichzeit dem Erstellen, Bearbeiten und Löschen von Einträgen unserer Todo-Liste dient. Für das Bearbeiten und Löschen benutzen wir eine private Hilfsmethode mit dem Namen renderResource, die wir in einer Schleife benutzen, um die Formularfelder für die existierenden Todos zu erzeugen.

    <?php
    declare(strict_types=1);
    
    namespace SelfHtml\Todo\View;
    
    use \Iterator;
    use SelfHtml\Todo\Domain\TodoItem;
    use SelfHtml\Todo\Persistence\TodoItemResource;
    
    /**
     * Repräsentiert die Darstellung einer Todo-Liste.
     */
    final class TodoListView
    {
        /**
         * Erzeugt ein HTML-Formular zum Erstellen, Bearbeiten und Löschen von Todos.
         *
         * @param Iterator $rsources Bereits vorhandene Todos
         * @return string HTML-String
         */
        public function render(Iterator $resources) : string
        {
            $editsHtml = "";
            foreach ($resources as $resource) {
                $editsHtml .= $this->renderResource($resource);
            }
            return <<<HTML
                <!DOCTYPE html>
                <html lang="de">
                    <head>
                        <title>SelfHtml TodoApp</title>
                    </head>
                    <body>
                        <h1>Todo Liste</h1>
                        <form method="POST">
                            $editsHtml
                            <fieldset>
                                <legend>Neuer Eintrag</legend>
                                <label>
                                    <span>Beschreibung</span>
                                    <input name="create[description]" maxlength="255">
                                </label>
                            </fieldset>
                            <button>Speichern</button>
                        </form>
                    </body>
                </html>
                HTML;
        }
    
        /**
         * Erzeugt ein Teilformular zum Bearbeiten und Löschen eines existierendes Todos.
         *
         * @param TodoItemResource
         * @return string
         */
        private function renderResource(TodoItemResource $resource) : string
        {
            try {
                $item = $resource->fetch();
            } catch (OutOfBoundsException $e) {
                return '';
            }
            $state = $item->getState();
            $checked = ($state === TodoItem::STATE_DONE) ? 'checked' : '';
            $id = \htmlspecialchars((string) $resource->getId());
            $description = \htmlspecialchars($item->getDescription());
            return <<<HTML
                <fieldset>
                    <legend>Eintrag bearbeiten</legend>
                    <label>
                        <span>Beschreibung</span>
                        <input name="update[$id][description]" value="$description"  maxlength="255">
                    </label>
                    <label>
                        <span>Erledigt</span>
                        <input name="update[$id][state]" type="checkbox" $checked>
                    </label>
                    <label>
                        <span>Löschen</span>
                        <input name="delete[$id]" type="checkbox">
                    </label>
                </fieldset>
                HTML;
        }
    }
    
    

    Wir müssen daran denken, dass die fetch-Methode fehlschlagen kann, wenn ein Datensatz nicht mehr existiert. In der Hilfsmethode benutzen wir deshalb einen try/catch-Block, um den Fehler abzufangen. Wenn das passiert, dann verlassen wir die Hilfsfunktion vorzeitig und geben schlicht den leeren String zurück.

  6. Controller

    Endlich haben wir alle Komponenten, die wir brauchen, um uns dem Controller zuzuwenden. Der Controller soll die Formulardaten auswerten. Er untersucht sie darauf, ob ein neues Todo eingebenen wurde und ob bestehende Todos bearbeitet oder gelöscht wurden. Für jede dieser Aktionen ruft der Controller dann die Persistenz-Schicht auf, um die Änderungen in der Datenbank wirksam zu machen. Im letzten Schritt benutzt er den View, um die Ausgabe vorzubereiten.

    Für die einzelnen Unteraufgaben hat unser Controller vier private Hilfsmethoden: create, update, delete und showForm. Die einzige öffentliche Methode indexAction führt die Gesamtaufgabe aus, indem sie die einzelnen Hilfsmethoden aufruft. Wir erklären im folgenden nur die update-Methode.

    <?php
    declare(strict_types=1);
    
    namespace SelfHtml\Todo\Controller;
    
    use \OutOfBoundsException;
    use SelfHtml\Todo\Domain\TodoItem;
    use SelfHtml\Todo\Persistence\TodoItemRepository;
    use SelfHtml\Todo\View\TodoListView;
    
    /**
     * Der TodoController ist verantwortlich dafür Formulardaten auszuwerten, entsprechende
     * Änderungen in der Persitenz-Schicht auszulösen und das Bearbeitungs-Formular zu erzeugen.
     */
    final class TodoController
    {
        /**
         * @var TodoItemRepository
         */
        private $repository;
    
        /**
         * @var TodoListView
         */
        private $view;
    
        public function __construct(TodoItemRepository $repository, TodoListView $view)
        {
            $this->repository = $repository;
            $this->view = $view;
        }
    
        /**
         * Wertet die Formulardaten aus, benutzt die Persistenz-Schicht zum Speichern der Änderungen
         * und die Darstellungsschicht, um das Formular zu erzeugen.
         *
         * @param array $formData Die Formulardaten aus dem $_POST-Array
         * @return string das HTML-Formular zum Bearbeiten der Todo-Liste
         */
        public function indexAction(array $formData) : string
        {
            $this->create($formData['create'] ?? []);
            $this->update($formData['update'] ?? []);
            $this->delete($formData['delete'] ?? []);
            return $this->showForm();
        }
    
        /**
         * Erstellt einen neuen Todo-Datensatz.
         *
         * @param array $newTodo Beschreibung eines Datensatzes
         */
        private function create(array $newTodo) : void
        {
            if (isset($newTodo['description']) && $newTodo['description'] !== '') {
                $item = new TodoItem(TodoItem::STATE_PENDING, $newTodo['description']);
                $this->repository->add($item);
            }
        }
    
        /**
         * Überschreibt Datensätze.
         *
         * @param array $updates
         */
        private function update(array $updates) : void
        {
            foreach ($updates as $id => $update) {
                $state = isset($update['state']) ? (TodoItem::STATE_DONE) : (TodoItem::STATE_PENDING);
                $item = new TodoItem($state, $update['description']);
                $resource = $this->repository->get((int) $id);
                try {
                    $resource->update($item);
                } catch (OutOfBoundsException $e) {
                }
            }
        }
    
        /**
         * Löscht Datensätze.
         *
         * @param array $deletions
         */
        private function delete(array $deletions) : void
        {
            foreach ($deletions as $id => $deletion) {
                $resource = $this->repository->get((int) $id);
                try {
                    $resource->delete();
                } catch (OutOfBoundsException $e) {
                }
            }
        }
    
        /**
         * Gibt das HTML-Formular zurück, das zum Neuanlegen, Bearbeiten und Löschen der Todo-Items dient.
         * 
         * @return string
         */
        private function showForm() : string
        {
            $resources = $this->repository->all();
            return $this->view->render($resources);
        }
    }
    
    

    Die update-Methode bekommt in dem Parameter $updates alle Formular-Felder als Array übergeben, deren Name mit update beginnt. Die Struktur des Arrays ergibt sich aus unserem HTML-Formular. Das Array könnte zum Beispiel so aussehen:

    [
        '3' => ['description' => 'Frühstücken', 'state' => 1],
        '5' => ['description' => 'Mittag essen', 'state' => 1]
    ]
    

    Wobei die Schlüssel auf oberster Array-Ebene den Datensatz-IDs entsprechen. Wir itereieren deshalb in einer foreach-Schleife über alle Einträge der Liste. In der Schleife erzeugen wir uns jeweils ein Objekt von der Klasse TodoItem, das die geänderte Aufgabe repräsentiert. Im Anschluss holen wir uns die Ressource, die zu der Datensatz-ID gehört, und rufen die update-Methode auf der Ressource auf. Bei diesem Schritt müssen wir uns wieder daran erinnern, dass das Bearbeiten schief gehen kann, falls der Datensatz nicht mehr existiert. In diesem Fall soll unsere Methode den Fehler abfangen und ignorieren.

  7. Anwendung verkabeln

    Wir haben nun alle notwendigen Komponenten entwickelt, die wir brauchen, um unsere finale Anwendung daraus zu konstruieren, die wir in der public/index.php-Datei speichern. Zuerst binden wir unser Submodule ein, dann stellen wir eine Datenbank-Verbindung her. Du musst natürlich die Datenbank-Zugangsdaten durch deinen eigenen ersezten, und die Datenbank-Tabelle wie ganz oben beschrieben vorher erstellt haben. Wenn das geschehen ist, initialiseren wir die Persistenz-Schicht, indem wir das TodoItemRepository erstellen. Das selbe machen wir für den TodoListView und den TodoController. Dabei müssen wir die entsprechenden Parameter an die Konstruktoren übergeben. Der TodoController bekommt zum Beispiel sowohl das TodoItemRepository als auch den TodoListView übergeben. Dieser Vorgang nennt sich Dependency-Injection. Wenn alles verkapelt ist, rufen wir die indexAction-Methode unseres Controllers auf, und übergeben ihm das $_POST-Array, das die Formulardaten enthält. Dann müssen wir das Formular nur noch mit echo ausgeben.

    <?php
    declare(strict_types=1);
    
    use \SelfHtml\Todo\Persistence\TodoItemRepository;
    use \SelfHtml\Todo\View\TodoListView;
    use \SelfHtml\Todo\Controller\TodoController;
    
    require_once('src/Domain/TodoItem.php');
    require_once('src/Persistence/TodoItemResource.php');
    require_once('src/Persistence/TodoItemRepository.php');
    require_once('src/View/TodoListView.php');
    require_once('src/Controller/TodoController.php');
    
    // Konfiguration auslesen
    $dsn = 'mysql:host=localhost;dbname=todo';
    $username = 'username';
    $password = 'password';
    
    // Persistenz-Schicht initialisieren
    $pdo = new PDO($dsn, $username, $password);
    $repository = new TodoItemRepository($pdo);
    
    // View initialisieren
    $view = new TodoListView();
    
    // Controller initialisieren
    $controller = new TodoController($repository, $view);
    
    // Controller ausführen und den Rückgabewert ausgeben
    echo $controller->indexAction($_POST);
    
    

    Et voilà, fertig ist unsere Todo-App.

    Du kannst die App mit dem Befehl php -S localhost:80 -t public/ starten. Und die Seite dann in deinem Browser aufrufen, indem du zu der Adresse http://localhost navigierst.

  8. Hallo 1unitedpower,

    Ich habe einen kleinen Artikel darüber geschrieben, wie man mit PHP und SQL eine kleine Todo-App erstellt.

    Vielen lieben Dank.

    Was mir noch fehlt, ist, wie diese App dann auch tatsächlich als App genutzt werden kann.

    Bis demnächst
    Matthias

    --
    Pantoffeltierchen haben keine Hobbys.
    ¯\_(ツ)_/¯
    1. Vielen lieben Dank.

      Gerne.

      Was mir noch fehlt, ist, wie diese App dann auch tatsächlich als App genutzt werden kann.

      Ich habe für die Entwicklung den eingebauten PHP-Webserver benutzt. Wenn man sich mit einem Terminal in dem Ordner mit der index.php-Datei befindet kann man den lokalen Webserver mit php -S localhost:80 starten und sich die die Seite im Browser ansehen, indem man zu http://localhost navigiert. Man sollte die Dateien aber auch allesamt in das Document-Root-Verzeichnis eines Apache-Webservers schmeißen können. Beides setzt aber voraus, dass ein SQL-Server schon aufgesetzt ist. Wenn noch kein Wiki-Artikel zu dem Thema existiert, den man an passender Stelle verlinken kann, dann könnte ich bei Zeiten einen Artikel schreiben, der zumindest den eingbauten PHP-Webserver abdeckt und kurz erklärt woher man MariaDB bekommt und wie man ihn unter Windows und Ubuntu installiert.

      1. Servus!

        Wenn noch kein Wiki-Artikel zu dem Thema existiert, den man an passender Stelle verlinken kann, dann könnte ich bei Zeiten einen Artikel schreiben, der zumindest den eingbauten PHP-Webserver abdeckt und kurz erklärt woher man MariaDB bekommt und wie man ihn unter Windows und Ubuntu installiert.

        Du hast Recht - so etwas fehlt uns noch!

        Das wäre zwischen diesen Artikeln angesiedelt:

        Wobei letzterer wsl. überarbeitet werden müsste.

        Vielen Dank im Voraus!

        Herzliche Grüße

        Matthias Scharwies

        --
        25 Jahre SELFHTML → SELF-Treffen 05.-07. Juni 2020 in Mannheim
        1. Hallo Matthias,

          Wobei letzterer wsl. überarbeitet werden müsste.

          heißt die Abkürzung wsl. nun "wahrscheinlich" oder "wesentlich"?
          Oder beides? ;-)

          Ciao,
           Martin

          --
          "Wenn man ein Proton aufmacht, sind drei Quarks drin."
          - Joachim Bublath in der Knoff-Hoff-Show
          1. Servus!

            Wobei letzterer wsl. überarbeitet werden müsste.

            heißt die Abkürzung wsl. nun "wahrscheinlich" oder "wesentlich"?
            Oder beides? ;-)

            Gut erkannt, aber wer nimmt sich die Zeit?

            Herzliche Grüße

            Matthias Scharwies

            --
            25 Jahre SELFHTML → SELF-Treffen 05.-07. Juni 2020 in Mannheim
            1. Hallo,

              Wobei letzterer wsl. überarbeitet werden müsste.

              heißt die Abkürzung wsl. nun "wahrscheinlich" oder "wesentlich"?
              Oder beides? ;-)

              Gut erkannt, aber wer nimmt sich die Zeit?

              ich schau's mir gern mal an, bin aber mit meinem Kenntnisstand bezüglich Apache noch bei 2.0x hängengeblieben. Die Struktur der Konfiguration hat sich ja seitdem erheblich verändert - IMO zum Nachteil, denn ich finde das alte Konzept übersichtlicher, die gesamte Konfiguration in einer Datei zu haben.

              Ciao,
               Martin

              --
              Making mistakes is human.
              Automating them is progress.
    2. Hallo Matthias,

      Was mir noch fehlt, ist, wie diese App dann auch tatsächlich als App genutzt werden kann.

      meinst du damit eine "App" im umgangssprachlichen Sinn, also eine Handy-App?

      Das wäre ja dann eine ganz andere Welt. Smartphone-Apps sind, meinem bescheidenen Wissen zufolge, üblicherweise in Java programmiert.

      Schönes Wochenende,
       Martin

      --
      "Wenn man ein Proton aufmacht, sind drei Quarks drin."
      - Joachim Bublath in der Knoff-Hoff-Show
      1. meinst du damit eine "App" im umgangssprachlichen Sinn, also eine Handy-App?

        Ich kann nicht für Matthias sprechen, aber ich meinte das im weiteren Sinne von Web App, also auch nicht im engeren Sinne von Single Page Application.

      2. Hallo Der Martin,

        meinst du damit eine "App" im umgangssprachlichen Sinn, also eine Handy-App?

        Eigentlich nicht, aber jetzt, wo du es erwähnst … 😉

        Bis demnächst
        Matthias

        --
        Pantoffeltierchen haben keine Hobbys.
        ¯\_(ツ)_/¯
      3. Hallo Martin,

        Smartphone-Apps sind, meinem bescheidenen Wissen zufolge, üblicherweise in Java programmiert.

        Nein. Da gibt es kein „überlicherweise.“ Unter iOS ist die vom Hersteller unterstützte Sprache Swift (bevorzugt) oder Objective-C (veraltet). Unter Android ist die vom Hersteller unterstützte Sprache Kotlin (bevorzugt) oder Java (veraltet).

        That said: oft werden Mobile-Apps heutzutage mit JavaScript und HTML geschrieben und mit Frameworks wie Cordova oder React Native geschrieben. Gerade React Native ist sowas wie der Shooting Star.

        Freundliche Grüße,
        Christian Kruse

  9. Lieber 1unitedpower,

    vielen Dank für den Artikel! Ich habe eine Menge dazugelernt.

    Was mir als Englisch-Lehrer sofort aufgefallen ist, ist das Wort pending, das Du im Artikel konsequent mit einem Rechtschreibfehler verwendet hast. Aber das sind halt wir Lehrer: Wir geilen uns sofort an den unwichtigsten Stellen auf. ;-)

    Im Ernst: Eines habe ich nicht verstanden. Wo genau wird die state-Eigenschaft eines TodoItems konkret verändert und dann verändert gespeichert? Das muss ich übersehen haben, da ich in Sachen MVC nur theoretisch etwas gelesen, aber selbst noch nie praktisch etwas angewendet habe.

    Eine andere Sache würde mich auch interessieren: Ist es aus Performance-Gründen egal, ob man für jeden Datensatz eine eigene Query an den DB-Server sendet, nachdem man mit einer ersten Query alle ID geholt hat, oder wäre eine Art "Sammelquery" performanter, die sich alles holt, um dann aus der Ergebnismenge einzelne Objekte zu instanziieren? Für den konkreten Fall mit ein paar hundert DB-Einträgen ist das im Artikel vorgestellte Modell sicherlich mehr als ausreichend, aber wenn man eine größere Applikation baut, geht man dann trotzdem genau so vor?

    Mir hat in dem Artikel die Übersichtlichkeit sehr gut gefallen. Der Aufbau ist logisch und für jemanden, dem das MVC-Konzept noch unklar ist, wirklich leicht verständlich, wenn man sich die Zeit nimmt, alles nachzuvollziehen.

    Liebe Grüße

    Felix Riesterer

    1. Hallo,

      Was mir als Englisch-Lehrer sofort aufgefallen ist, ist das Wort pending, das Du im Artikel konsequent mit einem Rechtschreibfehler verwendet hast.

      Der Pedant in mir hat da sogar zwei unterschiedliche Rechtschreibfehler entdeckt...

      Gruß
      Kalk

      1. Lieber Tabellenkalk,

        Der Pedant in mir hat da sogar zwei unterschiedliche Rechtschreibfehler entdeckt...

        so oberlehrerhaft wollte ich dann doch nicht sein. Aber ja, Du hast Recht.

        Noch etwas zu meiner Verständnisfrage?

        Liebe Grüße

        Felix Riesterer

    2. Hallo Felix,

      vielen Dank für dein Feedback!

      Was mir als Englisch-Lehrer sofort aufgefallen ist, ist das Wort pending, das Du im Artikel konsequent mit einem Rechtschreibfehler verwendet hast. Aber das sind halt wir Lehrer: Wir geilen uns sofort an den unwichtigsten Stellen auf. ;-)

      Stimmt, das korriegere ich im Anschluss noch.

      Im Ernst: Eines habe ich nicht verstanden. Wo genau wird die state-Eigenschaft eines TodoItems konkret verändert und dann verändert gespeichert? Das muss ich übersehen haben, da ich in Sachen MVC nur theoretisch etwas gelesen, aber selbst noch nie praktisch etwas angewendet habe.

      Das TodoItem, also das Domain Model, ist unveränderlich, es gibt keine Setter-Methoden für den Zustand oder die Beschreibung. Wenn man ein TodoItem in der Datenbank verändern möchte, holt man sich das Resource Model, das den Datensatz repräsentiert, und überschreibt ihn mit einem neuen TodoItem. Das geschieht hier in der update-Methode des Controllers.

      $state = isset($update['state']) ? (TodoItem::STATE_DONE) : (TodoItem::STATE_PENDING);
      $item = new TodoItem($state, $update['description']);
      $resource = $this->repository->get((int) $id);
      $resource->update($item);
      

      Der Hintergrund-Gedanke ist schon fast etwas philosophisch. Das Domain Model ist immateriell, ein Todo-Item ist die Summe seiner Eigenschaften, nicht mehr, nicht weniger. Wenn man eine Eigenschaft ändert, dann erhält man ein komplett neues Todo-Item, das sich von dem vorherigen unterscheidet. Das Resource-Model hingegen manifistiert sich durch einen konkreten Datenbank-Eintrag, der kann sich über die Zeit auch verändern.

      Eine andere Sache würde mich auch interessieren: Ist es aus Performance-Gründen egal, ob man für jeden Datensatz eine eigene Query an den DB-Server sendet, nachdem man mit einer ersten Query alle ID geholt hat, oder wäre eine Art "Sammelquery" performanter, die sich alles holt, um dann aus der Ergebnismenge einzelne Objekte zu instanziieren? Für den konkreten Fall mit ein paar hundert DB-Einträgen ist das im Artikel vorgestellte Modell sicherlich mehr als ausreichend, aber wenn man eine größere Applikation baut, geht man dann trotzdem genau so vor?

      Die Beobachtung ist richtig. Es findet ein Trade-Off zwischen Speicherverbrauch und Rechenleistung statt. Für die Rechenleistung wäre eine Sammelquery in der Tat besser. Für den Speicherverbrauch ist es besser die Datensatz-Inhalte erst zu laden, wenn sie tatsächlich gebraucht werden. In produktionsreifen Frameworks hat man meistens die Wahl zwischen beiden Verfahren. Ich habe mich hier für die speicherfreundliche Variante entschieden, weil sie etwas einfacher zu implementieren und erklären ist.

      Mir hat in dem Artikel die Übersichtlichkeit sehr gut gefallen. Der Aufbau ist logisch und für jemanden, dem das MVC-Konzept noch unklar ist, wirklich leicht verständlich, wenn man sich die Zeit nimmt, alles nachzuvollziehen.

      Danke, das hört man gern.

  10. Moin!

    Ich habe gelesen, dass auf der Mitgliederverammlung auf die Unvollständigkeit des PHP-Bereichs im Wiki hingewiesen wurde. Wenn Interesse besteht, dann könnte ich eine Tutorial-Reihe beisteuern, die das Verständnis von PHP-Frameworks beleuchtet. Ziel wäre es nicht ein bestimmtes PHP-Framework zu erklären, sondern wiederkehrende Entwurfsmuster zu demonstrieren, die sich heute in vielen modernen Frameworks in unterschiedlichen Varianten wiederfinden. Ich möchte vorher aber wissen, ob daran überhaupt Interesse besteht und es müsste sich jemand finden, der die Tutorial-Reihe von Markdown in das Wiki überträgt.

    Die Todo-App würde als gemeinsamer Aufhänger dienen und schrittweise weiterentwickelt werden. Zunächst würde ich das hier vorgestellte Tutorial nochmal aufteilen, um die Schritte etwas kürzer zu fassen. Ganz grob habe ich mir die folgenden Schritte überlegt:

    1. Schritt: Formularvalidierung mit MVC
    2. Schritt: Speicherschicht mit dem Repository-Pattern
    3. Schritt: Auslagerung der Konfiguration in Umgebungsvariablen und Erstellung eines Setup-Skripts, um die Datenbank zu initialisieren.
    4. Schritt: Einzelansichten für das Anlegen, Bearbeiten und Löschen von Todos. Sowie eine Listenansicht zur reinen Anzeige der Todos. Dabei würde ich den sogenannten Router einführen.
    5. Schritt: Exkurs Composer
    6. Schritt: Refactoring der Anwendung aus Schritt 4. Hier würde ich den PSR-7-Standard HTTP mesages interfaces einführen.
    7. Schritt: Refoctoring der Anwendung aus Schritt 6. Diesmal würde ich den PSR-15-Standard HTTP Server Request Handler vorstellen.

    Das ist vermutlich noch nicht die finale Sturktur, aber so ähnlich könnte ich mir das vorstellen.

  11. Hallo,

    Ihr dürft ihn gerne für euer Wiki verwenden,

    @Matthias Scharwies hat jetzt das TODOrial ins Wiki übertragen.

    Sinnvollerweise könnte man bei der Ordnerliste noch einen übergeordneten Ordner TODO erwähnen und am Ende fehlt noch der Aufruf, der dann ja mit TODO erfolgen könnte. Sehe ich das richtig?

    Gruß
    Kalk

    1. @Matthias Scharwies hat jetzt das TODOrial ins Wiki übertragen.

      Cool! Herzlichen Dank!