Datenmodellierung

Daten aus einer Datenbank abrufen und speichern.

 

Datenbank einrichten

Bevor wir richtig loslegen können, benötigen wir eine Datenbank. Solltest du den Leitfaden Getting Started übersprungen haben, empfielt es sich spätestens jetzt, einen Datenbank-Server aufzusetzen. Ich persönlich empfehle XAMPP, da hier bereits ein MySQL-Server integriert ist. Du kannst aber nutzen, was auch immer du möchtest. Hauptsache, es ist ein MySQL-Server.

Environment überprüfen

Schaue nach, ob dein Environment mit den Daten deines MySQL-Servers übereinstimmt. Solltest du XAMPP nutzen, musst du normalerweise nichts weiter unternehmen, aber man weiß ja nie.

# /.env
DB_TYPE=mysql
DB_HOST=localhost
DB_NAME=test
DB_USER=root
DB_PASS=

 

Datenbank erstellen

Die Datenbank test exisitiert bereits, also warum extra eine neue erstellen. Außer, du möchtest eine andere verwenden.

Tabelle erstellen

Tabellen lassen sich auf zwei Wegen erstellen. Entweder klassisch über phpMyAdmin (http://localhost/phpmyadmin) oder mittels der Baldur Console. Letztere ist zur Zeit noch in Arbeit. Sobald Baldur Console brauchbar ist, sage ich Bescheid.

phpMyAdmin

Wir erstellen zunächst eine neue Tabelle mit dem Titel Category und insgesamt fünf Feldern:

  • id, primary key
  • title, unique key
  • description
  • created, timestamp on create
  • updated, timestamp on update, default: null

Entitäten erstellen

Nachdem unsere Tabelle erfolgreich erstellt wurde, legen wir noch eine sogenannte Entity Class an. Diese bildet die Struktur der Tabelle ab und dient uns als Werkzeug zur Verarbeitung im Controller.

Erstelle eine Datei mit dem Namen Category.php im Verzeichnis /src/Entity und befülle sie analog zur MySQL-Tabelle:

<?php


namespace App\Entity;


use DateTime;

class Category
{

    /**
     * @var string
     */
    public string $title;

    /**
     * @var string
     */
    public string $description;

    /**
     * @var DateTime
     */
    public datetime $created;

    /**
     * @var DateTime
     */
    public datetime $updated;

    /**
     * @return string
     */
    public function getTitle(): string
    {
        return $this->title;
    }

    /**
     * @param string $title
     */
    public function setTitle(string $title): void
    {
        $this->title = $title;
    }

    /**
     * @return string
     */
    public function getDescription(): string
    {
        return $this->description;
    }

    /**
     * @param string $description
     */
    public function setDescription(string $description): void
    {
        $this->description = $description;
    }

    /**
     * @return DateTime
     */
    public function getCreated(): DateTime
    {
        return $this->created;
    }

    /**
     * @return DateTime
     */
    public function getUpdated(): DateTime
    {
        return $this->updated;
    }

}

Wie du siehst, benötigen wir für den Primary Key "id" keine eigene Eigenschaft, da dieser von MySQL verwaltet wird. Für die übrigen Felder haben wir zusätzlich noch klassische Getter- und Setter-Methoden erstellt. Diese benötigen wir, um später die Objektinstanz mit Daten zu füttern.

Daten verarbeiten

Zuerst schauen wir uns mal den kompletten CRUD-Controller an (CRUD steht für Create, Read, Update, Delete). Er beinhaltet insgesamt vier Methoden. Diese sind:

  1. index
  2. new
  3. edit
  4. delete

Theoretisch müsste noch eine fünfte hinzukommen. Nämlich eine, um Detailansichten einzelner Einträge anzuzeigen.

<?php

namespace App\Controller;

use App\Entity\Category;
use Btinet\Ringhorn\Controller\AbstractController;

/**
 * @meta route="/base"
 */
class BaseController extends AbstractController
{

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

        $categoryRepository = $this->getRepository(Category::class);
        $categories = $categoryRepository->findAll();

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

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

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

            $category = new Category();
            $em = $this->getEntityManager();

            $category->setTitle($this->request->getQuery('title'));
            $category->setDescription($this->request->getQuery('description'));

            $categoryRepository = $this->getRepository(Category::class);
            $categoryTitle = $categoryRepository->findBy([
                'title' => $category->getTitle()
            ]);

            if($categoryTitle){

                $this->flash->add('Titel existiert bereits.', 'warning');
                $this->redirect('302', 'base/new');

            } else {

                $em->persist($category);
                $this->flash->add('Eintrag wurde gespeichert!', 'success');
                $this->redirect('302', 'base/index');

            }
        }

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

    /**
     * @meta  route="/edit"
     * @param int $id
     */
    public function edit(int $id){

        $categoryRepository = $this->getRepository(Category::class);
        $categoryEntry = $categoryRepository->findBy([
            'id' => $id
        ]);

        if(!$categoryEntry){

            $this->flash->add('Eintrag existiert nicht.', 'warning');
            $this->redirect('302', 'base/index');

        } else {

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

                $category = new Category();
                $em = $this->getEntityManager();

                $category->setTitle($this->request->getQuery('title'));
                $category->setDescription($this->request->getQuery('description'));

                $em->persist($category, $id);
                $this->flash->add('Eintrag wurde aktualisiert!', 'success');
                $this->redirect('302', 'base/index');
            }
        }

        $this->view->render('base/edit.html.twig', [
            'session' => $this->session,
            'flash' => $this->flash,
            'category' => array_pop($categoryEntry)
        ]);
    }

    /**
     * @meta  route="/delete"
     * @param int $id
     */
    public function delete(int $id){

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

            $categoryRepository = $this->getRepository(Category::class);
            $categoryEntry = $categoryRepository->findBy([
                'id' => $id
            ]);

            if(!$categoryEntry){

                $this->flash->add('Eintrag existiert nicht.', 'warning');
                $this->redirect('302', 'base/index');

            } else {

                $category = new Category();
                $em = $this->getEntityManager();
                $em->remove($category, $id);

                $this->flash->add('Eintrag wurde gelöscht!', 'success');
                $this->redirect('302', 'base/index');

            }

        }

        $this->flash->add('Formular ungültig.', 'warning');
        $this->redirect('302', 'base/index');

    }
}

 

Daten speichern

Schauen wir uns zuerst an, wie ein neuer Datensatz erstellt wird und werfen einen Blick auf die Methode new. Aus dem vorherigen Abschnitt weißt du bereits, dass wir prüfen müssen, ob das Formular tatsächlich abgespeichert wurde.

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

Als nächstes instanzieren wir ein Category-Objekt und laden den Entity Manager. Dieser hilft uns, wie der Name schon sagt, die Entitäten zu verwalten.

...
$category = new Category();
$em = $this->getEntityManager();
...

Doch noch ist das Category-Objekt leer. Also initialisieren wir es mit unseren Formulardaten. Erinnern wir uns an die Entity Class, stellen wir fest, dass wir ja Methoden zum Abrufen und Speichern von Daten erstellt haben. Folgend nutzen wir also setTitle() und setDescription() zum Initialisieren der sogenannten Properties.

...
$category->setTitle($this->request->getQuery('title'));
$category->setDescription($this->request->getQuery('description'));
...

Da ich die Tabellenspalte title auf unique gesetzt habe, sollten wir nun feststellen, ob es bereits einen Datensatz mit dem zu speichernden Titel gibt. Dazu benötigen wir das Category Repository, um Abfragen an der MySQL-Tabelle vorzunehmen. Die Methode findBy() hilft uns dabei, Datensätze zu finden, die etwa im folgenden Beispiel einen Titel haben wie unser zu speichernder Datensatz.

...
$categoryRepository = $this->getRepository(Category::class);
$categoryTitleExists = $categoryRepository->findBy([
                'title' => $category->getTitle()
]);
...

Sobald ein Datensatz gefunden wurde, wird dieser als Array in $categoryTitleExists gespeichert. Andernfalls ist der Wert false. In Zukunft sollen die Datensätze als Entity Objekt gespeichert werden.

Existiert bereits ein Datensatz mit dem gewählten Titel, wird eine Fehlermeldung in der Session (Flash Message) gespeichert und ein Redirect ausgelöst. Gibt es keinen doppelten Eintrag wird das Category Objekt mithilfe des Entity Managers in der Datenbank persistent gemacht (gespeichert). Anschließend wird ein Redirect auf die index-Route ausgelöst.

...
if($categoryTitleExists){

    $this->flash->add('Titel existiert bereits.', 'warning');
    $this->redirect('302', 'base/new');

} else {

    $em->persist($category);
    $this->flash->add('Eintrag wurde gespeichert!', 'success');
    $this->redirect('302', 'base/index');

}
...

Sollte gar kein Formular abgesendet worden sein und es sich außerdem um keinen POST-Request handeln, wird das Template mit dem Formular zum Erstellen eines Datensatzes gerendert. Wir übergeben außerdem die Session mit dem CSRF-Token an das Formular. Das kennen wir bereits. Neu ist, dass wir zusätzlich noch die Flash Klasse $this->flash übergeben. Diese benötigen wir, um sogenannte Flash Messages auszugeben. Man könnte sie auch einfach Meldungen oder Alerts nennen.

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

Das Template base/new.html.twig sieht folgendermaßen aus:

{% extends 'base.html.twig' %}

{% block title %}
    <title>Ringhorn Framework: neuer Eintrag</title>
{% endblock %}

{% block body %}
    <h2>Eintrag anlegen</h2>

    {{ flash.show|raw }}

    <p>
        <a href="{{ route('base/index') }}">Zurück zur Übersicht</a>
    </p>

    {{ include('base/_form.html.twig') }}

{% endblock %}

 

Daten abrufen

Das Abrufen von Datensätzen ist ziemlich einfach und dennoch sehr flexibel. Im obigen Beispiel hatten wir auch schon Datensätze zu Vergleichszwecken abgerufen. Doch werfen wir erst einmal einen Blick auf die Methode index() der BaseController-Klasse.

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

    $categoryRepository = $this->getRepository(Category::class);
    $categories = $categoryRepository->findAll();

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

Zu Beginn instanzieren wir das Repository der gewünschten Tabelle. In diesem Fall ist es also das Category Repository. Das erreichst du mit getRepository() und der entsprechenden Entity Class:

$categoryRepository = $this->getRepository(Category::class);

Achte darauf, dass die Repository-Klasse analog zur Entity-Klasse auch existiert. Diese speicherst du im Verzeichnis /src/Repository:

# /src/Repository/CategoryRepository.php
<?php


namespace App\Repository;


use Btinet\Ringhorn\Model\EntityRepository;


class CategoryRepository extends EntityRepository
{

    public function findDuplicate(array $criteria, array $falseCriteria): array
    {

        $conditions = $this->createSqlConditions($criteria, $falseCriteria);

        foreach($falseCriteria as $property => $value){
            $criteria[$property] = $value;
        }

        return $this->db->select('SELECT * FROM '.$this->table.' '.$conditions, $criteria);

    }

}

Deine Repository-Klasse erweitert die EntityRepository-Klasse von Ringhorn. Die oben erstellte Methode ist eine sogenannte CustomMethod, die ich speziell für diese Entity entwickelt habe. Die EntityRepository-Klasse hingegen bietet oft genutzte Methoden, die für alle Repositorys gelten. Schauen wir uns das mal an:

findAll(int $limit = null, int $position = null)

Gibt alle Datensätze einer Tabelle aus. $limit und $position geben das Limit an. Diese Parameter sind optional.

findBy(array $criteria = array(), array $sort = false){...};

// Beispiel:
$categoryRepository->findBy(
            [
                'title' => 'Test',
                'description' => 'Beschreibung'
            ],
            [
                'title' => 'asc'
            ]
        );

Diese Methode findet alle Datensätze mit bestimmten Kriterien. Welche Kriterien das sind, wird in einem Array festgelegt. Wonach und wie sortiert wird, gibt man im zweiten optionalen Array an. Das Beispiel gibt hier alle Datensätze aus, die in der Spalte title 'Test' und in description 'Beschreibung' stehen haben. Außerdem werden die gefundenen Datensätze im Alphabet aufsteigend nach title sortiert werden.

Zurück zum BaseController. Wir hatten bereits den CategoryRepository instanziert. Nun wollen wir alle Kategorien abrufen. Also nutzen wir dafür die findAll()-Methode:

# /src/Controller/BaseController.php

...
pubic function index(){
...
$categories = $categoryRepository->findAll();

Das war's im Kern schon gewesen. Neben dem Speichern und Abrufen von Daten fehlt nur noch das Löschen. Zeige ich dir direkt.

Daten löschen

 

Kommentare
Noch keine Kommentare vorhanden.