View und Templates

Die View stellt die Daten dar, die im Controller verarbeitet wurden.

 

Twig Template Engine

Wir erinnern uns noch einmal an das Template, dass wir zuvor erstellt hatten:

# /templates/base/index.html.twig
<html>
    <head>
        <title>Ringhorn Tutorial</title>
    </head>
    <body>
    {% if eingabe %}
        <p>Deine Eingabe war: {{ eingabe }}</p>
    {% else %}
        <p>Es wurde kein Parameter an die Route angehängt.</p>
    {% endif %}
    <a href="{{ route('base/index') }}">Seitenaufruf ohne Parameter</a>
    <a href="{{ route('base/index',{'parameter':'hello-world'}) }}">Seitenaufruf mit Parameter</a>
    </body>
</html>

Das ist zwar ganz hübsch und nett, jedoch wird es auf dauer nervig, ständig das gesamte HTML-Gerüst schreiben zu müssen. Einfacher wäre es doch, wenn sich darum ein übergeordnetes Template kümmert.

Übergeordnete Templates

Dir ist sicherlich schon die Datei base.html.twig im Verzeichnis /templates aufgefallen. Schauen wir sie uns einmal genauer an:

# /templates/base.html.twig
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>Ringhorn tutorial</title>
        {% block stylesheets %}
            {{ get_entry_link_tags('app')|raw }}
        {% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}
            {{ get_entry_script_tags('app')|raw }}
        {% endblock %}
    </body>
</html>

Bestimmt hast du schon die {% block %}-Funktion entdeckt. Diese platziert sozusagen einen Container, dem wir unsere Inhalte zuordnen können. Bauen wir einmal das Template vom Anfang so um, dass es die eben definierten sogenannten Blocks verwendet.

# /templates/base/index.html.twig
{% extends('base.html.twig')%}

{% block body %}
    {% if eingabe %}
        <p>Deine Eingabe war: {{ eingabe }}</p>
        {% else %}
            <p>Es wurde kein Parameter an die Route angehängt.</p>
    {% endif %}
    <a href="{{ route('base/index') }}">Seitenaufruf ohne Parameter</a>
    <a href="{{ route('base/index',{'parameter':'hello-world'}) }}">Seitenaufruf mit Parameter</a>
{% endblock %}

Dank der Funktion {% extends() %} können wir unserem Template nun sagen, welche Eltern-Datei verwendet werden soll. So müssen wir nun nicht mehr das HTML-Gerüst schreiben.

Du kannst wie im Film Inception weitere Dateien mit extends und blocks verschachteln. Aber nur so lange, bis dir schwindelig wird.

 

Formulare

Formulare gehören in fast jede Website oder Webapp. Andernfalls wird einem schnell langweilig.

Formular erstellen

Wie ein Formular in HTML aussieht, weißt du zwar längst, der Vollständigkeit halber zeige ich es trotzdem in der neu erstellten Datei /templates/base/new.html.wtig:

# /templates/base/new.html.twig
{% extends 'base.html.twig' %}

{% block body %}
    <form method="post">
        <label>
            Titel
            <input type="text" name="title">
        </label>
        <label>
            Beschreibung
            <input type="text" name="description">
        </label>
        <button type="submit">Speichern</button>
    </form>
{% endblock %}

Mit dem erstellten Formular soll also etwas gespeichert werden. Tun wir mal so, als wollten wir einen Blog erstellen. Die Leute lieben Blogs. Viele dürften sogar schon abhängig sein.

Wir haben also die Felder title und description, die wir speichern, aber auch bearbeiten wollen. Die eignen sich prima für Kategorien. Das merken wir uns mal. Denn zunächst sind wir - du hast es wohl schon gemerkt - auf ein kleines Problem gestoßen.

Wir haben ein Formular fürs Speichern und benötigen ja jetzt noch eins für das Bearbeiten. Jedoch sind beide Formulare identisch, naja, bis auf den Submit-Button, der logischer einmal mit "speichern" und einmal mit "aktualisieren" beschriftet werden möchte.

Wie wäre es damit: Wir speichern das Formular in einer separaten Datei und includieren sie mit verschiedenen Parametern in das new- und edit-Template.

Zuerst das Formular-Template:

# /templates/base/_form.html.twig
<form method="post">
    <label>
        Titel
        <input type="text" name="title">
    </label>
    <label>
        Beschreibung
        <input type="text" name="description">
    </label>
    <button type="submit">{{ button|default('Speichern') }}</button>
</form>

Wir haben für den Text des Submit-Buttons die Variable "button" eingesetzt, die, sollte sie nicht definiert sein, per default auf "Speichern" gesetzt wird. Die Datei selbst hab ich mit einem vorangestellten Unterstrich versehen, damit mir signalisiert wird: "Aha, diese Datei ist kein eigentständiges Template".

Dann schauen wir uns einmal die Templates für das Erstellen und Bearbeiten an:

# /templates/base/new.html.twig
{% extends 'base.html.twig' %}

{% block body %}
    {{ include('base/_form.html.twig') }}
{% endblock %}

Wir haben nun das Template mit dem Formular per include() in die Datei new.html.twig geladen. Das wars bereits. Für die Datei edit.html.twig wollen wir jedoch, dass der Submit-Button mit "Aktualisieren" beschriftet wird. Das erreichen wir, indem wir dem include() einen zweiten Parameter als Array hinzufügen. Hier ordnen wir der Variable button den Wert "Aktualisieren" zu.

# templates/base/edit.html.twig
{% extends 'base.html.twig' %}

{% block body %}
    {{ include('base/_form.html.twig', {'button':'Aktualisieren'}) }}
{% endblock %}

War auch kein Hexenwerk. Und nicht nur das. Auch die Templates werden ordentlicher und wo es geht, werden gemeinsame Ressourcen genutzt.

Formular überprüfen

Widmen wir uns wieder der Methode new unseres BaseControllers zu. Nach einem Klick auf "Speichern" wollen wir mit den Formulardaten schließlich auch etwas anstellen. Doch zuerst müssen wir prüfen, ob es sich beim Aufruf der Methode auch um einen POST-Request handelt. Dies erreichen wir mit einer konditionellen Abfrage und der Request-Methode isPostRequest(). Dank getQuery() können wir die Daten der jeweiligen Formfelder auslesen und weiterverarbeiten. Bevor wir uns um die Datenbankanbindung kümmern, speichern wir die Formulardaten in einem Array, das wir zurück an das Template schicken. Im folgenden Beispiel ist $resonse unser Array. Solange kein Formular abgesendet wurde, ist es jedoch mit null initialisiert:

/**
 * @meta  route="/new"
 */
public function new(){

    $response = null;

    if ($this->request->isPostRequest()) {

        $response['title']=$this->request->getQuery('title');
        $response['description']=$this->request->getQuery('description');

    }

    $this->view->render('base/new.html.twig', [
        'response' => $response
    ]);
}

Wieder zurück im Template können wir nur mal so für Spaß das gespeicherte Array ausgeben, um zu sehen, ob tatsächlich alles geklappt hat:

# /template/base/new.html.wtig
{% extends 'base.html.twig' %}

{% block body %}
    {{ include('base/_form.html.twig') }}
    {% if response %}
        <ul>
            {%  for key, value in response %}
                <li><b>{{ key }}:</b> {{ value }}</li>
            {% endfor %} 
        </ul>        
    {% endif %}
{% endblock %}

Wunderbar. Nachdem wir also das Formular abgesendet haben, hat der Controller einen POST-Request festgestellt und die Formulardaten als Array in $response gespeichert. Doch genau genommen haben wir bisher noch gar nicht geprüft, ob tatsächlich das Formular abgesendet wurde. Der POST-Request hätte auch woanders herkommen können. Wir müssen also die Bedingung der IF-Schleife im Controller um isFormSubmitted() erweitern.

    /**
     * @meta  route="/new"
     */
    public function new(){

        $response = null;

        if ($this->request->isPostRequest() && $this->request->isFormSubmitted()) {

            $response['title']=$this->request->getQuery('title');
            $response['description']=$this->request->getQuery('description');

        }

        $this->view->render('base/new.html.twig', [
            'response' => $response
        ]);
    }

Sende nun das Formular erneut ab und schau, was passiert. Du wirst feststellen, dass erstmal gar nichts passiert. Das ist aber auch richtig so, denn eine entscheidende Sache fehlt noch in unserem Formular.

 

CSRF-Tokens

Um sicherzustellen, dass der POST-Request seinen Ursprung in unserem Formular hat nutzen wir sogenannte CSRF-Tokens. Bei jedem Aufruf der Controller Methode wird ein neuer zufälliger Token generiert und in deiner aktuellen Session gespeichert. Diesen übergeben wir zunächst an das Template, indem wir zum Beispiel wie unten angegeben, die Session als Objekt übergeben. Möchtest du nur den CSRF-Token übergeben, kannst du das mit $this->session->get('csrf_token') bewerkstelligen.

/**
     * @meta  route="/new"
     */
    public function new(){

        $response = null;

        if ($this->request->isPostRequest() && $this->request->isFormSubmitted()) {

            $response['title']=$this->request->getQuery('title');
            $response['description']=$this->request->getQuery('description');

        }

        $this->view->render('base/new.html.twig', [
            'session' => $this->session,
            'response' => $response
        ]);
    }

Haben wir die Session als Objekt an das Template übergeben, können wir auch dort die Methode get aufrufen. Wir nutzen dafür die Aneinanderreihung mit . statt ->. Nun fügen wir ein Input-Feld als Hidden-Type ein und setzen den Wert mit {{ session.get('csrf_token') }} sowie den Namen auf "csrf_token". Nachdem das Formular abgesendet wird, vergleicht der Controller diesen Token mit dem zuletzt in deiner Session generierten Token. Stimmen sie überein, ist das ein deutlicher Hinweis darauf, dass tatsächlich dein Formular abgesendet wurde.

Durch CSRF-Token stellst du nicht nur die Herkunft der Daten sicher, sondern vermeidest auch doppeltes Absenden, da nach dem Aktualisieren der Seite die Tokens nicht mehr übereinstimmen.

# /templates/base/_form.htl.twig
<form method="post">
    <label>
        Titel
        <input type="text" name="title">
    </label>
    <label>
        Beschreibung
        <input type="text" name="description">
    </label>
    <input type="hidden" name="csrf_token" value="{{ session.get('csrf_token') }}">
    <button type="submit">{{ button|default('Speichern') }}</button>
</form>

Du bist bestimmt schon seit einer ganzen Weile sehr ungeduldig und willst endlich auch ein paar Daten speichern und abrufen. Dann schlage ich vor, tun wir das. Im nächsten Kapitel.

Kommentare
Noch keine Kommentare vorhanden.