21.09.26 07:46 Uhr

[ab 2026/9] Aufrufe: 66, Hits: 357

WebDB - Von der Webseite zur Datenbank. Mit HTML, PHP, MySQL

Copyright 2024, 2025, 2026 Josef L. Staud

Autor: Josef L. Staud

Stand: August 2026

Umfang des gedruckten Textes: ca. 177 Seiten

Dieser Text richtet sich an die Teilnehmer meiner Seminare und sonstige Interessenten.
Er wird ständig weiterentwickelt und ausgebaut.

Aufbereitung fürs Web

Diese HTML-Seiten wurden mithilfe eines von mir erstellten Programms erzeugt: WebGenerator2026. Es setzt Texte in HTML-Seiten um und ist noch in der Entwicklung. Die "maschinelle" Erstellung erlaubt es, nach jeder Änderung des Textes diesen unmittelbar neu in HTML-Seiten umzusetzen. Da es nicht möglich ist, nach jeder Neuerstellung alle Seiten zu prüfen, ist es durchaus möglich, dass irgendwo auf einer "abgelegenen" Seite Fehler auftreten. Ich bitte dafür um Verzeihung und um Hinweise (hs@staud.info).

Die Veröffentlichung im Web erfolgt ab 2022 in zwei Versionen: Mit und ohne Frame-Technologien. Zu meinem Bedauern wird die Frame-Technologie inzwischen von den Verantwortlichen als unerwünscht angesehen und es häufen sich die Hinweise, dass bestimmte Browser Frame-basierte Seiten nicht mehr korrekt interpretieren können. Deshalb habe ich eine zweite Version meines Programms WebGenerator erstellt, die ohne Frames realisiert ist.

Urheberrecht

Dieser Text ist urheberrechtlich geschützt. Die dadurch begründeten Rechte, insbesondere die der Übersetzung, des Nachdrucks, des Vortrags, der Entnahme von Abbildungen und Tabellen oder der Vervielfältigung auf anderen Wegen und der Speicherung in Datenverarbeitungsanlagen, bleiben, auch bei nur auszugsweiser Verwertung, vorbehalten. Eine Vervielfältigung dieses Textes oder von Teilen dieses Textes ist auch im Einzelfall nur in den Grenzen der gesetzlichen Bestimmungen des Urheberrechtsgesetzes der Bundesrepublik Deutschland vom 9. September 1965 in der jeweils geltenden Fassung zulässig. Sie ist grundsätzlich vergütungspflichtig. Zuwiderhandlungen unterliegen den Strafbestimmungen des Urheberrechtsgesetzes.

Warenzeichen und Markenschutz

Alle in diesem Text genannten Gebrauchsnamen, Handelsnamen, Marken, Produktnamen, usw. unterliegen warenzeichen-, marken- oder patentrechtlichem Schutz bzw. sind Warenzeichen oder eingetragene Warenzeichen der jeweiligen Inhaber. Die Wiedergabe solcher Namen und Bezeichnungen in diesem Text berechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Gesetzgebung zu Warenzeichen und Markenschutz als frei zu betrachten wären und daher von jedermann benutzt werden dürften.

Prof. Dr. Josef L. Staud

hs@staud.info

 

1 Einführung

Hier wird beschrieben, wie eine webbasierte Benutzerschnittstelle mit Datenbankzugriff entsteht. Auf dem langen Weg von den Anforderungen (an die Software und Datenbank) bis zur grafischen Bedienoberfläche ist dies der letzte Schritt, hier fett gesetzt:

Anforderungen -> Datenmodell (Schema) -> Datenbank -> Web-Benutzeroberfläche

Gezeigt wird dies mit Hilfe von XAMPP, phpMyAdmin und MySQL. Damit steht ein lokaler Server auf dem eigenen PC zur Verfügung, zum gefahrlosen Entwickeln, Ausprobieren und Testen. Hier wird eine kleine relationale Datenbank rund um Kunden, Rechnungen und Artikel (KuReAr) genutzt.

Datenbank KuReAr

Konkret geht es also um die Gestaltung der grafischen Bedienoberfläche (meist mit GUI bezeichnet; Graphical User Interface) mit HTML, CSS, MySQL und PHP. Dies wird, beginnend mit sehr einfachen Beispielen, bis zur Gestaltung mit sicherheitsrelevanten Komponenten gezeigt.

Einige Aufgaben zu diesem Bereich finden sich im Text Datenbanktraining in Kapitel 5:

https://www.staud.info/AufgabenDBoF/db_t_1.php#Kapitel5

Dort ist auch die Datenbank KuReAr in einigen Aufgaben vertreten.

1.1 Datenbank KuReAr

Mit der Datenbank KuReAr werden die Ausführungen hier erläutert.

1.1.1 Das Datenmodell

 

Abbildung 1.1-1: Relationales Datenmodell KuReAr. Die Grafik ist mit dem Designer von phpMyAdmin erzeugt.

1.1.2 Ein Dump

Ein SQL-Dump enthält SQL-Anweisungen, mit denen eine Datenbank einschließlich ihrer Relationen, Schlüsselbedingungen und Beispieldaten eingerichtet werden kann.

Der nachfolgende SQL-Dump wurde mit phpMyAdmin erzeugt und für diesen Text angepasst. Zum Einrichten der Datenbank wird er in phpMyAdmin in das SQL-Fenster eingefügt. Anschließend wird die Schaltfläche GO ("Ausführen") angeklickt.

 

CREATE DATABASE IF NOT EXISTS KuReAr

CHARACTER SET utf8mb4

COLLATE utf8mb4_unicode_ci;

USE KuReAr;

--

-- Tabellenstruktur für Tabelle artikel

--

CREATE TABLE artikel (

ArtNr smallint(4) NOT NULL,

beschreibung varchar(10) DEFAULT NULL,

preis decimal(7,2) DEFAULT NULL

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

--

-- Daten für Tabelle artikel

--

INSERT INTO artikel (ArtNr, beschreibung, preis) VALUES

(100, 'Gamer-PC', 2500.00),

(101, 'Büro-PC', 1200.00),

(102, 'Entwickler', 3200.00),

(554, 'SmartPh', 330.00);

--

-- Tabellenstruktur für Tabelle kunden

--

CREATE TABLE kunden (

KuNr smallint(5) NOT NULL,

name char(10) DEFAULT NULL,

vorname char(10) DEFAULT NULL

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

--

-- Daten für Tabelle kunden

--

INSERT INTO kunden (KuNr, name, vorname) VALUES

(1001, 'Steiner', 'Waldemar'),

(1002, 'Hinterhof', 'Rudolf'),

(1009, 'Cäser', 'Filippa'),

(1010, 'Anger', 'Albert'),

(9999, 'Presley', 'Elvis');

-- --------------------------------------------------------

--

-- Tabellenstruktur für Tabelle rechkoepfe

--

CREATE TABLE rechkoepfe (

ReNr smallint(6) NOT NULL,

KuNr smallint(5) DEFAULT NULL,

ReDatum date DEFAULT NULL

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

--

-- Daten für Tabelle rechkoepfe

--

INSERT INTO rechkoepfe (ReNr, KuNr, ReDatum) VALUES

(23001, 1002, '2026-03-27'),

(23010, 1009, '2026-05-20'),

(23020, 1010, '2026-01-07'),

(23050, 1001, '2026-11-19');

-- --------------------------------------------------------

--

-- Tabellenstruktur für Tabelle rechpos

--

CREATE TABLE rechpos (

ReNr smallint(6) NOT NULL,

PosNr tinyint(3) UNSIGNED NOT NULL,

ArtNr smallint(4) DEFAULT NULL,

anzahl smallint(2) DEFAULT NULL

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

--

-- Daten für Tabelle rechpos

--

INSERT INTO rechpos (ReNr, PosNr, ArtNr, anzahl) VALUES

(23001, 1, 101, 2),

(23001, 2, 554, 1),

(23010, 1, 101, 3),

(23050, 1, 102, 2),

(23050, 2, 101, 3);

--

-- Indizes der exportierten Tabellen

--

--

-- Indizes für die Tabelle artikel

--

ALTER TABLE artikel

  ADD PRIMARY KEY (ArtNr);

--

-- Indizes für die Tabelle kunden

--

ALTER TABLE kunden

  ADD PRIMARY KEY (KuNr);

--

-- Indizes für die Tabelle rechkoepfe

--

ALTER TABLE rechkoepfe

  ADD PRIMARY KEY (ReNr),

  ADD KEY KuNr (KuNr);

--

-- Indizes für die Tabelle rechpos

--

ALTER TABLE rechpos

  ADD PRIMARY KEY (ReNr,PosNr),

  ADD KEY ArtNr (ArtNr);

--

-- Constraints der exportierten Tabellen

--

--

-- Constraints der Tabelle rechkoepfe

--

ALTER TABLE rechkoepfe

  ADD CONSTRAINT rechkoepfe_ibfk_1 FOREIGN KEY (KuNr) REFERENCES kunden (KuNr);

--

-- Constraints der Tabelle rechpos

--

ALTER TABLE rechpos

  ADD CONSTRAINT rechpos_ibfk_1 FOREIGN KEY (ArtNr) REFERENCES artikel (ArtNr),

  ADD CONSTRAINT rechpos_ibfk_2 FOREIGN KEY (ReNr) REFERENCES rechkoepfe (ReNr);

1.1.3 Ausgangsdaten in den Relationen

Nach dem Einrichten der Datenbank enthalten die vier Relationen die nachfolgend aufgeführten Beispieldaten. Sie bilden den Ausgangspunkt für die Programme und Datenbankoperationen der folgenden Kapitel.

SELECT * FROM artikel;


ArtNr

beschreibung

preis

100

Gamer-PC

2500.00

101

Büro-PC

1200.00

102

Entwickler

3200.00

554

SmartPh

330.00

 

SELECT * FROM kunden;


KuNr

name

vorname

1001

Steiner

Waldemar

1002

Hinterhof

Rudolf

1009

Cäser

Filippa

1010

Anger

Albert

9999

Presley

Elvis

 

SELECT * FROM rechkoepfe;


ReNr

KuNr

ReDatum

23001

1002

2026-03-27

23010

1009

2026-05-20

23020

1010

2026-01-07

23050

1001

2026-11-19

 

SELECT * FROM rechpos;


ReNr

PosNr

ArtNr

anzahl

23001

1

101

2

23001

2

554

1

23010

1

101

3

23050

1

102

2

23050

2

101

3

1.2 Typografische Kennzeichnung der Modellelemente

Was die zu beschreibenden Elemente in der Datenmodellierung angeht, kann man einen Ausgangspunkt und drei Modellebenen unterscheiden. Der Ausgangspunkt ist der zu modellierende Anwendungsbereich, manchmal auch Weltausschnitt genannt. Die erste Modellebene ist die der Attribute, durch die Objekte und Beziehungen beschrieben werden. Die zweite die Ebene der Basiselemente im jeweiligen Ansatz (Relationen, Entitätstypen, Klassen). Die dritte Ebene ist die des gesamten Datenmodells. Um diesbezüglich im Text die Übersichtlichkeit zu erhöhen wird folgende typographische Festlegung getroffen:

Überblick durch Typographie

  • Bezeichnungen von Anwendungsbereichen werden etwas vergrößert, in Kapitälchen und in Arial gesetzt: Hochschule, Personalwesen, WebShop. In der Web-Version sind sie zusätzlich in roter Farbe gehalten.
  • Bezeichnungen von Datenmodellen und Datenbanken sind in normaler Größe, fett und in Arial gesetzt: Vertrieb, Zoo, WebShop, Datenbanksysteme (Markt für Datenbanksysteme). In der Web-Version zusätzlich in rot.
  • Bezeichnungen von Relationen, Entitätstypen und Klassen (Basiselemente) sind etwas verkleinert und in Arial gesetzt: Angestellte, Abteilungen, Projekte. In der Web-Version zusätzlich in rot.
  • Bezeichnungen von Methoden sind in der Standardgröße in fett und in Arial gesetzt: drucken().
  • Bezeichnungen von Attributen sind etwas verkleinert, fett und in Arial gesetzt: Gehalt, Name, Datum. Bei zusammengesetzten Benennungen wird der nachfolgende Begriff wieder groß begonnen: PersNr (Personalnummer), BezProj (Bezeichnung Projekt).
  • Für die kombinierte Angabe von Attributen und Relationen: Relationenbezeichnung.Attributsbezeichnung, also z.B. Angestellte.PersNr für das Attribut PersNr der Relation Angestellte.
  • Ausprägungen von Attributen werden in normaler Größe und in Courier gesetzt, z.B. Müller für das Attribut Name.
  • Programme und Programmfragmente werden in normaler Größe und Courier angegeben, also z.B.:

$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";

Für die Basiselemente (Relationen, Entitätstypen, Klassen) wird bei der Bezeichnung immer die Mehrzahl gewählt, da ja in der Regel mehrere Objekte bzw. Beziehungen erfasst sind.

 

2 Verbindungsaufbau und erste Abfragen

2.1 MySQLi und PDO im Vergleich

PHP-Programme können über verschiedene Schnittstellen auf relationale Datenbanken zugreifen. Für MySQL beziehungsweise MariaDB werden vor allem zwei PHP-Erweiterungen verwendet:

  • MySQLi
  • PDO

Beide Erweiterungen ermöglichen es, eine Datenbankverbindung aufzubauen, SQL-Anweisungen auszuführen und die Ergebnisse zu verarbeiten. Außerdem unterstützen beide Prepared Statements und Transaktionen.

MySQLi ist speziell für MySQL und die weitgehend kompatible Datenbank MariaDB vorgesehen. PDO stellt dagegen eine einheitliche Programmierschnittstelle für unterschiedliche Datenbanksysteme bereit.

Die Grundideen beider Techniken beschreibt die folgende Tabelle.

PDO und MySQLi - Vergleich

Merkmal

PDO

MySQLi

Vollständige Bezeichnung

PHP Data Objects

MySQL Improved

Unterstützte Datenbanksysteme

Viele DBMS (MySQL, PostgreSQL, SQLite, ...)

Nur MySQL und kompatible Systeme wie MariaDB

Programmierstil

Objektorientiert

Objektorientiert und prozedural

Prepared Statements

Ja

Ja

Transaktionen

Ja

Ja

Wechsel des Datenbanksystems

grundsätzlich erleichtert

nicht vorgesehen

Verwendete Klasse

PDO

mysqli


PDO

PDO stellt eine einheitliche Schnittstelle für den Zugriff auf unterschiedliche Datenbanksysteme bereit. Methoden wie prepare(), execute() und fetch() werden unabhängig vom eingesetzten Datenbanksystem auf ähnliche Weise verwendet.

PDO vereinheitlicht jedoch nicht die unterschiedlichen SQL-Dialekte. Wird das Datenbanksystem gewechselt, müssen daher gegebenenfalls auch SQL-Anweisungen angepasst werden.

Der Verbindungsaufbau für die Datenbank KuReAr kann folgendermaßen erfolgen:

$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";

$pdo = new PDO($dsn,"root","",[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

Mit $dsn (Data Source Name) entsteht ein String, der Treiber, Datenbank und den verwendeten Zeichensatz angibt.

  • $pdo verweist dank new PDO() auf ein Objekt der Klasse PDO.
  • mysql: bedeutet, dass der SQL-Treiber innerhalb von PDO verwendet werden soll.
  • host=localhost zeigt an, dass der Datenbankserver lokal läuft.
  • dbname=KuReAr gibt die zu bearbeitende Datenbank an.
  • charset=utf8mb4 gibt den Zeichensatz an.
  • root gibt den MySQL-Datenbankbenutzer an.
  • "" bedeutet: kein Passwort
  • [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] setzt den Fehlermodus so, dass PDO bei Problemen Exceptions wirft. Ohne diese Option würde PDO oft nur Fehlercodes liefern, die man manuell prüfen müsste. Siehe Anhang.

MySQLi

MySQLi ist eine spezielle Erweiterung nur für MySQL-Datenbanken. Der Verbindungsaufbau geschieht mit folgenden Befehlen (am Beispiel der Datenbank KuReAr):

$con = new mysqli("host", "root", "Passwort", "DB-Bezeichnung");

if ($con->connect_error) {

    die("Verbindungsfehler: " . $con->connect_error);

}

  • $con ist eine PHP-Variable. Sie speichert ein Objekt der Klasse mysqli, das die Verbindung vom jeweiligen Programm zur Datenbank herstellt. Ein Objekt $con verfügt unter anderem über die Eigenschaft $con->connect_error und über Methoden wie $con->query(), $con->execute_query(), $con->close(), $con->prepare. Übrigens: "con" kommt von "connection". Die Bezeichnung dieser Variablen kann aber frei gewählt werden.
  • mysqli: Die entsprechende Klasse von mySQL für den Verbindungsaufbau.
  • new mysqli(): Instantiierung eines mysqli-Objekts, das den jeweiligen Verbindungsaufbau realisiert.
  • Host: Gibt den MySQL-Server an. Beim Testen wird hier oft localhost vorkommen. Dies bedeutet, dass der Server auf demselben Rechner (wie die sonstigen Programme) läuft. Alternativ kann hier z.B. eine IP-Adresse stehen.
  • root: MySQL-Benutzerkonto
  • Passwort: für das Benutzerkonto
  • DB-Bezeichnung: Bezeichnung der Datenbank.

Die folgende If-Anweisung sichert gegen ein Scheitern des Verbindungsaufbaus ab:

  • $con->connect_error prüft, ob beim Verbindungsaufbau ein Fehler aufgetreten ist. Falls es so ist, startet die Funktion "die()" ("sterbe").
  • die() oder exit() beendet das Skript sofort und gibt eine Fehlermeldung aus.

MySQLi unterstützt Prepared Statements, Stored Procedures und erlaubt mehrere gleichzeitige Abfragen. Außerdem kann es auch prozedural verwendet werden:

$con = mysqli_connect("localhost", "root", "", "KuReAr");

Dies ist möglich, weil PHP beide Programmierstile zulässt, den prozeduralen und den objektorientierten und weil dies mit der Klasse MySQLi auch umgesetzt wurde.

2.2 KundenAbfragenMySQLi_1.php

2.2.1 Merkmale des Programms

  • Start durch Browser-Befehl
  • Einfache Abfrage
  • Direkter Zugriff auf die Datenbank.
  • Verwendete Klasse für Verbindungsaufbau: mysqli
  • HTML-Ausgabe des Ergebnisses

Das erste Programm führt eine einfache, unveränderliche SQL-Abfrage aus. Da die SQL-Anweisung keine Benutzereingaben oder anderen veränderlichen Werte enthält, ist hierfür kein Prepared Statement erforderlich. Im nachfolgenden Beispiel wird die Abfrage um einen Suchwert ergänzt und deshalb als Prepared Statement ausgeführt.

2.2.2 Das Programm

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Relation Kunden</title>

  <link rel="stylesheet" type="text/css" href="mySQL_1.css">

</head>

 

<body>

  <p class="text-red">

    Relation Kunden - mit Spaltenüberschriften:

  </p>

  <table border="1">

    <caption class="text-bold">Relation Kunden</caption>

    <thead>

      <tr>

      <th>Kundennummer</th>

      <th>Kundenname</th>

      <th>Kundenvorname</th>

      </tr>

    </thead>

    <tbody>

     

<?php

/*

Verbindung zur Datenbank aufbauen.

mysqli steht für MySQL Improved. Mit new mysqli()wird ein Objekt

der Klasse mysqli erzeugt. Die Variable $con verweist auf dieses

Objekt und ermöglicht den Zugriff auf seine Eigenschaften und

Methoden.

*/

$con = new mysqli("localhost","root","","KuReAr");

 

/*

Prüfen, ob beim Aufbau der Datenbankverbindung ein Fehler

aufgetreten ist. Falls ein Fehler auftritt, beendet die(), nach der

Ausgabe der Meldung, sofort die weitere Ausführung des Programms.

*/

    if ($con->connect_error) {

      die("Datenbankverbindung fehlgeschlagen: "

      . $con->connect_error);

    }

// Zeichensatz für die Datenbankverbindung festlegen.

// utf8mb4 unterstützt Umlaute und andere Unicode-Zeichen.

$con->set_charset("utf8mb4");

// SQL-Anweisung festlegen. Mit AS erhalten die ausgewählten Spalten

// neue Bezeichnungen. Diese Bezeichnungen werden später als

// Schlüssel des assoziativen Arrays verwendet.

$sql = "SELECT KuNr AS Kundennummer, Name AS Kundenname,

    Vorname AS Kundenvorname FROM kunden ORDER BY KuNr";

/*

SQL-Anweisung ausführen.

execute_query() ist eine Methode der Klasse mysqli. Bei einer

SELECT-Anweisung liefert sie ein Objekt der Klasse mysqli_result

zurück. Die Variable $res verweist auf dieses Ergebnisobjekt.

*/

$res = $con->execute_query($sql);

// num_rows ist eine Eigenschaft des Ergebnisobjekts. Sie enthält

// die Anzahl der durch die Abfrage gefundenen Datensätze.

if ($res->num_rows === 0) {

  echo "

    <tr>

    <td colspan=\"3\">

      Keine Ergebnisse

    </td>

    </tr>

    ";

}

/*

fetch_assoc() liest jeweils einen Datensatz aus dem Ergebnisobjekt

und liefert ihn als assoziatives Array.

Die Spaltenbezeichnungen, beziehungsweise die mit AS vergebenen

Namen bilden die Schlüssel des Arrays.

Die Schleife wird ausgeführt, solange ein weiterer Datensatz

vorhanden ist.

*/

while ($dsatz = $res->fetch_assoc()) {

  echo "<tr>";

  echo "<td>" . htmlspecialchars((string)$dsatz["Kundennummer"],

    ENT_QUOTES,"UTF-8") . "</td>";

  echo "<td>" . htmlspecialchars($dsatz["Kundenname"],

    ENT_QUOTES,"UTF-8") . "</td>";

  echo "<td>" . htmlspecialchars($dsatz["Kundenvorname"],

    ENT_QUOTES, "UTF-8") . "</td>";

  echo "</tr>";

}

// Ergebnisobjekt schließen und die dafür verwendeten Ressourcen

// freigeben.

$res->close();

// Datenbankverbindung schließen und die damit

// verbundenen Ressourcen freigeben.

$con->close();

?>

  </tbody>

  </table>

</body>

</html>

Die Konsolenausgabe

2.2.3 Die Stylesheets

Datei mySQL_1.css

body {margin: 10px;color: black;font-family: Arial, sans-serif;}

h1 {margin: 5px 0 10px;color: blue;font-family: Calibri, Arial, sans-serif;font-size: 16pt;font-weight: bold;text-align: left;}

p {margin: 10px 0;}

table {border-collapse: collapse;font-family: inherit;}

thead {font-size: 10pt;font-weight: bold;}

td,th {padding: 3px;border: 1px solid black;text-align: left;vertical-align: middle;}

.text-red {color: red;font-size: 14pt;}

.text-bold {font-size: 14pt;font-weight: bold;}

2.2.4 Anmerkungen

Folgende Klassen kommen hier zum Einsatz

  • mysqli: Erlaubt die Einrichtung einer Verbindung zu einer MySQL-Datenbank. Jedes erzeugte Objekt, z.B. $con, verfügt über Attribute und Methoden zur Arbeit mit der Datenbank. Eine Methode von mysqli ist execute_query(). Diese führt eine SQL-Abfrage aus und liefert ein Ergebnisobjekt der Klasse mysqli_result.
  • mysqli_result: Ein Objekt der Klasse mysqli_result enthält somit das Ergebnis der jeweiligen SQL-Abfrage. Zu den Attributen der Klasse gehört num_rows. Eine Methode ist fetch_assoc(). Sie liest jeweils eine Zeile aus dem Ergebnis und gibt sie als assoziatives Array zurück.

Verschmelzung

Das Programm macht deutlich, wie prozedurale und objektorientierte Programmierelemente in PHP miteinander verbunden werden können. Mit der Anweisung

$con = new mysqli("localhost", "root", "", "KuReAr");

wird ein Objekt der Klasse mysqli erzeugt. Die PHP-Variable $con verweist anschließend auf dieses Objekt. Über $con kann auf die Eigenschaften und Methoden des Objekts zugegriffen werden.

Beispielsweise:

$con->connect_error // Eigenschaft

$con->query($sql) // Methode

Zur Programmierung

  • fetch_assoc() gibt die Datensätze nicht selbst aus. Die Methode liest jeweils einen Datensatz und liefert ihn als assoziatives Array zurück. Die Ausgabe erfolgt anschließend beispielsweise mit echo.
  • Die aus der Datenbank gelesenen Werte werden vor ihrer Einfügung in die HTML-Ausgabe mit htmlspecialchars() behandelt (vgl. Anhang). Dadurch werden Sonderzeichen umgewandelt und nicht als Bestandteile des HTML-Codes interpretiert.
  • Die Methode execute_query() steht seit PHP 8.2 zur Verfügung. Enthält die SQL-Anweisung keine Platzhalter, kann bei älteren PHP-Versionen stattdessen $con->query($sql) verwendet werden. Enthält sie Platzhalter, muss die Abfrage mit prepare(), bind_param() und execute() als Prepared Statement ausgeführt werden.

2.3 KundenAbfragenMySQLi_2.php

2.3.1 Merkmale des Programms

  • Start durch Browser-Befehl
  • Abfrage mit Hilfe eines Prepared Statements

Das zweite Beispiel zeigt wiederum die grundsätzliche Struktur eines PHP-Programms, das mit einem Browser gestartet wird und eine Datenbank mit der Klasse mysqli abfragt. Die Abfrage erfolgt nun aber als Prepared Statement und nicht mit einer einfachen SQL-Abfrage, was aus Sicherheitsgründen sehr zu empfehlen ist.

Prepared Statement

2.3.2 Das Programm

<?php

/*

Kundennummer, nach der gesucht werden soll.

In diesem Beispiel wird die Kundennummer unmittelbar

im Programm festgelegt. In einer Webanwendung würde

sie normalerweise aus einem Eingabeformular stammen.

*/

$id = 1002;

/*

Das HTML-Element <pre> sorgt dafür, dass Leerzeichen und

Zeilenumbrüche der Eingabe bei der Ausgabe erhalten bleiben.

Dadurch lässt sich das Ergebnis von print_r() übersichtlich

darstellen.

*/

echo "<pre>";

echo "--- Verbindungsaufbau mit MySQLi ---\n";

echo "--- KuReAr, Relation Kunden ---\n\n";

 

/*

Verbindung zur Datenbank aufbauen.

Mit new mysqli() wird ein Objekt der Klasse mysqli

erzeugt:

- "localhost" bezeichnet den Datenbankserver.

- "root" ist der MySQL-Benutzer.

- "" ist das hier leere Passwort.

- "KuReAr" ist die Bezeichnung der Datenbank.

Die Variable $con verweist auf das erzeugte

mysqli-Verbindungsobjekt.

*/

$con = new mysqli("localhost","root","","KuReAr");

 

/* Prüfen, ob beim Verbindungsaufbau ein Fehler aufgetreten ist.

connect_error enthält im Fehlerfall die entsprechende

Fehlermeldung. die() gibt die Meldung aus und beendet

anschließend die Ausführung des Programms.

*/

if ($con->connect_error) {die(

    "MySQLi-Verbindungsfehler: ". $con->connect_error. "\n");

}

 

// Zeichensatz für die Datenbankverbindung festlegen. utf8mb4

// unterstützt alle üblichen Unicode-Zeichen.

$con->set_charset("utf8mb4");

 

/*

Prepared Statement vorbereiten.

Das Fragezeichen ist ein Platzhalter für die Kundennummer.

Der konkrete Wert wird erst anschließend mit bind_param() an

diesen Platzhalter gebunden. prepare() liefert ein Objekt der

Klasse mysqli_stmt.

Die Variable $stmt verweist auf dieses Statement-Objekt.

*/

$stmt = $con->prepare(

    "SELECT * FROM Kunden WHERE KuNr = ?"

);

// Prüfen, ob das Prepared Statement erfolgreich

// vorbereitet werden konnte.

if (!$stmt) {

    die("Vorbereiten der SQL-Anweisung fehlgeschlagen: "

      . $con->error . "\n"

    );

}

/*

Im positiven Fall geht es hier weiter:

Den Wert der Variablen $id an den Platzhalter binden.

Das Typkennzeichen "i" bedeutet, dass der übergebene

Wert als Integer behandelt wird.

*/

$stmt->bind_param("i",$id);

/*

Prepared Statement ausführen. Erst jetzt wird die SQL-Anweisung

mit der gebundenen Kundennummer an den Datenbankserver übermittelt.

Zum Kontrollfluss an dieser Stelle vgl. den Exkurs unten.

*/

if (!$stmt->execute()) {

  die("Ausführen der SQL-Anweisung fehlgeschlagen: "

    . $stmt->error . "\n"

  );

}

 

/*

Ergebnis der SELECT-Abfrage abrufen. get_result() liefert ein

Objekt der Klasse mysqli_result. Dieses Ergebnisobjekt kann

anschließend Datensatz für Datensatz gelesen werden.

*/

$result = $stmt->get_result();

// Prüfen, ob ein Datensatz mit der angegebenen

// Kundennummer gefunden wurde.

if ($result->num_rows === 0) {

    echo "Kein Kunde mit der Kundennummer " . $id . " gefunden.\n";}

 

/*

Ergebnis Datensatz für Datensatz durchlaufen.

fetch_assoc() liefert den jeweils nächsten Datensatz als

assoziatives Array. Die Attributnamen der Relation bilden die

Schlüssel und die Attributwerte die zugehörigen Werte des Arrays.

Da KuNr normalerweise der Primärschlüssel ist, kann diese Abfrage

höchstens einen einzigen Datensatz liefern.

*/

while ($row = $result->fetch_assoc()) {

  print_r($row);

  echo "\n";

}

// Ergebnisobjekt schließen und die dafür verwendeten

// Ressourcen freigeben.

$result->close();

// Statement-Objekt schließen und die dafür verwendeten

// Ressourcen freigeben.

$stmt->close();

// Datenbankverbindung schließen.

$con->close();

// HTML-Element für die vorformatierte Ausgabe schließen.

echo "</pre>";

?>

2.3.3 Die Konsolenausgabe

Ergebnis eines Programmlaufs, falls die Kundennummer nicht vorhanden ist

2.3.4 Exkurs

Wo ist der Kontrollfluss in folgendem Programmfragment (von oben):

if (!$stmt->execute()) {
die("Ausführen der SQL-Anweisung fehlgeschlagen: "

      . $stmt->error . "\n");

}

Die Anweisung

$stmt->execute()

wird tatsächlich ausgeführt, während PHP die Bedingung der if-Anweisung auswertet. Das Ergebnis dieses Methodenaufrufs wird dann sofort geprüft. Damit werden Ausführung und Fehlerprüfung in einer if-Anweisung miteinander verbunden.

Der Ablauf im Einzelnen:

Zuerst führt PHP diese Methode aus:

$stmt->execute()

Die Methode liefert normalerweise einen booleschen Wert:

  • true - die SQL-Anweisung wurde erfolgreich ausgeführt
  • false - bei der Ausführung ist ein Fehler aufgetreten

Vor dem Methodenaufruf steht der logische NICHT-Operator: ! Dieser kehrt den zurückgegebenen Wahrheitswert um.

Erfolgreiche Ausführung

Liefert execute() den Wert true, ergibt sich !true. Das Ergebnis ist also false. Damit ist die if-Bedingung nicht erfüllt. Der Fehlerblock wird übersprungen:

die(...);

Das Programm wird nach der if-Anweisung fortgesetzt.

Fehlerhafte Ausführung

Liefert execute() den Wert false, ergibt sich !false. Das Ergebnis ist true. Damit ist die if-Bedingung erfüllt. Die Fehlermeldung wird ausgegeben und das Programm beendet:

die(

  "Ausführen der SQL-Anweisung fehlgeschlagen: " . $stmt->error

  . "\n"

);

Gleichbedeutende ausführlichere Schreibweise

Man könnte Ausführung und Prüfung auch auf zwei Anweisungen verteilen:

$erfolgreich = $stmt->execute();

if (!$erfolgreich) {

  die(

    "Ausführen der SQL-Anweisung fehlgeschlagen: "

      . $stmt->error . "\n"

  );

}

2.3.5 Anmerkungen

Zusammenfassung der Klassen und Methoden

Die Klasse mysqli repräsentiert eine Verbindung zu einer MySQL-Datenbank. Sie ist also eine objektorientierte MySQLi-Schnittstelle mit dem Konstruktor

mysqli

new mysqli(host, user, password, database).

Sie hat das Attribut (im Programmierumfeld wird oft von Eigenschaft gesprochen)

$con->connect_error

die eine Fehlermeldung enthält, falls die Verbindung fehlgeschlagen ist und u.a. folgende Methoden

$con->prepare(string $sql), die ein Prepared Statement erstellt.

$con->close(), mit der die Verbindung zur Datenbank geschlossen wird

Insgesamt gilt:

mysqli_stmt

  • Die Objekte der Klasse mysqli_stmt repräsentieren jeweils ein Prepared Statement.
  • Die Methode $stmt->bind_param(string $types, mixed &$var, mixed &...$vars) bindet die PHP-Variablen an die Platzhalter der SQL-Anweisung. Das Zeichen & kennzeichnet die Übergabe als Referenz. Deshalb müssen Variablen und keine unmittelbar angegebenen Werte übergeben werden.
  • Die Methode $stmt->execute() führt das vorbereitete Statement aus. Dies geschieht auf dem Server.
  • Die Methode $stmt->get_result() liefert ein Ergebnisobjekt (mysqli_result).
  • Die Methode $stmt->close() schließt das Statement.

Beispiel für mehrere Platzhalter:

$stmt = $con->prepare(

  "select * from kunden where KuNr = ? AND Name = ?"

);

$stmt->bind_param("is", $id, $name);

Erster Parameter integer, zweiter Parameter string.

Die Klasse mysqli_result erfasst die Ergebnisse von SQL-Abfragen. Ihre Methode $result->fetch_assoc() liest eine Ergebniszeile als assoziatives Array. Die Schleife läuft, bis false zurückgegeben wird, also keine weitere Zeile mehr vorliegt.

mysqli_result

2.3.6 Zum Programm

  • bind_param() setzt den Wert nicht durch gewöhnliche Textersetzung in die SQL-Anweisung ein. SQL-Anweisung und Parameter werden getrennt an die Datenbank übermittelt. Es entsteht daher intern nicht einfach die Zeichenkette SELECT * FROM kunden WHERE KuNr = 1002. Sinngemäß wird diese Abfrage ausgeführt, technisch bleibt der Parameter aber von der SQL-Anweisung getrennt.
  • Prepared Statements (vgl. auch den Anhang) schützen vor SQL-Injection, weil ein gebundener Wert als Datenwert und nicht als Bestandteil des SQL-Codes behandelt wird. Sie sollten grundsätzlich verwendet werden, wenn Werte von außen in eine SQL-Anweisung gelangen. Dies gilt nicht nur außerhalb eines lokalen XAMPP-Systems.
  • Da nach einer Kundennummer gesucht wird, die normalerweise eindeutig ist, liefert die Abfrage höchstens einen einzigen Datensatz. Die while-Schleife ist trotzdem korrekt und lässt sich später leicht für Abfragen mit mehreren Ergebnissen weiterverwenden.
  • get_result() setzt bei PHP in der Regel den MySQL-Native-Driver mysqlnd voraus. Dieser ist in üblichen XAMPP-Installationen vorhanden.
  • Die Ausgabe mit print_r() eignet sich vor allem zum Lernen und zur Fehlersuche. Für eine fertige Webseite sollten die Werte in einer HTML-Tabelle ausgegeben und dabei mit htmlspecialchars() (vgl. Anhang) behandelt werden.

2.4 KundenAbfragenPDO_1.php

2.4.1 Merkmale des Programms

  • Verwendete Klasse für Verbindungsaufbau: PDO
  • Absicherung durch Prepared Statements
  • Start durch Browser-Befehl
  • HTML-Ausgabe des Ergebnisses

Nun die Verbindungsaufnahme mit Hilfe der Klasse PDO. Wieder erfolgt der Zugriff auf die Relation Kunden der Datenbank KuReAr (Kunden - Rechnungen - Artikel).

2.4.2 Das Programm

<?php

 

/*

Kundennummer, nach der gesucht werden soll. Auch in diesem Beispiel

wird der Suchwert unmittelbar im Programm festgelegt. In einer

Webanwendung würde er üblicherweise aus einem Eingabeformular

stammen.

*/

$id = 1002;

echo "<pre>";

echo "--- Verbindungsaufbau mit PDO ---\n";

echo "--- KuReAr, Relation kunden ---\n\n";

 

try {

/*

Verbindung zur Datenbank aufbauen.

PDO steht für PHP Data Objects. Die Variable $pdo verweist auf das

erzeugte PDO-Objekt. Dieses Objekt stellt die Verbindung zur

Datenbank dar.

Der DSN (Data Source Name) enthält:

- den verwendeten Datenbanktreiber: mysql

- den Datenbankserver: localhost

- die Bezeichnung der Datenbank: KuReAr

- den Zeichensatz: utf8mb4

"root" ist der MySQL-Benutzer. Die darauffolgenden

Anführungszeichenbedeuten, dass kein Passwort verwendet wird.

PDO::ERRMODE_EXCEPTION bewirkt, dass PDO bei einem Datenbankfehler

eine PDOException auslöst.

*/

$pdo = new PDO(

  "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4",

    "root","",[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

 

/*

Prepared Statement vorbereiten.

:id ist ein benannter Platzhalter für die gesuchte Kundennummer.

prepare() liefert ein Objekt der Klasse PDOStatement.

Die Variable $stmt verweist auf dieses Statement-Objekt.

*/

$stmt = $pdo->prepare(

  "SELECT * FROM kunden WHERE KuNr = :id"

  );

 

/*

Prepared Statement ausführen.

Das assoziative Array ordnet dem Platzhalter :id den Wert der

Variablen $id zu. Beim Array-Schlüssel kann der Doppelpunkt

weggelassen werden. SQL-Anweisung und Parameterwert bleiben dabei

voneinander getrennt.

*/

$stmt->execute(["id" => $id]);

/*

Alle gefundenen Datensätze abrufen.

fetchAll() liefert ein Array, das sämtliche Ergebnisdatensätze

enthält.

PDO::FETCH_ASSOC legt fest, dass jeder Datensatz als assoziatives

Array bereitgestellt wird. Die Spaltennamen der Relation bilden die

Schlüssel des Arrays.

*/

$datensaetze = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Prüfen, ob ein Datensatz gefunden wurde.

if (count($datensaetze) === 0) {

  echo "Kein Kunde mit der Kundennummer " . $id . " gefunden.\n";

}

 

/*

Die gefundenen Datensätze nacheinander ausgeben. print_r() ist eine

eingebaute PHP-Funktion. Sie gibt den Inhalt und die Struktur einer

Variablen, insbesondere eines Arrays, in einer für Menschen gut

lesbaren Form aus.

Falls die Anzahl der Datensätze Null ist, ist $datensaetze ein

leeres Array. Die foreach-Schleife wird deshalb kein einziges Mal

durchlaufen. Die Schleife beendet sich, ohne dass print_r() oder

echo "\n" ausgeführt werden.

*/

foreach ($datensaetze as $row) {

  print_r($row);

  echo "\n";

}

 

//von try (ganz oben)

} catch (PDOException $e) {

 

// PDO-Datenbankfehler abfangen. getMessage() liefert die zur

// Exception gehörende Fehlermeldung.

echo "PDO-Fehler: " . $e->getMessage() . "\n";

} //schließt catch

 

// HTML-Element für die vorformatierte Ausgabe schließen.

echo "</pre>";

?>

Die Konsolenausgabe

2.4.3 Anmerkungen

Exkurs: Was genau ist $stmt?

Betrachten wir folgende Anweisung:

$stmt = $pdo->prepare("SELECT * FROM kunden WHERE KuNr = :id");

Die Methode prepare() bereitet die SQL-Anweisung zur späteren Ausführung vor. Die Ausführung selbst erfolgt zu diesem Zeitpunkt noch nicht. Nach erfolgreicher Ausführung von prepare() verweist die Variable $stmt auf ein Objekt der Klasse PDOStatement. Dieses Statement-Objekt repräsentiert die vorbereitete SQL-Anweisung und stellt Methoden bereit, mit denen Platzhalter gebunden, die Anweisung ausgeführt und Abfrageergebnisse abgerufen werden können.

Der benannte Platzhalter :id wird zunächst noch nicht durch einen konkreten Wert ersetzt. Der Wert kann beispielsweise mit bindValue() gebunden werden:

$stmt->bindValue(":id", $id, PDO::PARAM_INT);

Anschließend wird die vorbereitete SQL-Anweisung ausgeführt: $stmt->execute(); Alternativ kann der Wert unmittelbar beim Aufruf von execute() übergeben werden:

$stmt->execute(["id" => $id]);

Wichtige Methoden des PDOStatement-Objekts sind:

  • bindParam() bindet eine Variable als Referenz an einen Platzhalter
  • bindValue() bindet einen Wert an einen Platzhalter
  • execute() führt die vorbereitete SQL-Anweisung aus
  • fetch() ruft den nächsten Ergebnisdatensatz ab
  • fetchAll() ruft alle Ergebnisdatensätze ab

Zum Programm

Das Programm demonstriert, wie ein einzelner Datensatz aus der Datenbank abgefragt und ausgegeben wird. Dabei wird die PDO-Erweiterung verwendet, die eine objektorientierte und sichere Schnittstelle zur Datenbank bereitstellt.

Zu Beginn wird mit der Variablen $id ein Suchkriterium festgelegt. Es handelt sich um eine Ganzzahl, die den Schlüsselwert eines bestimmten Datensatzes repräsentiert. Dieser Wert wird später in die SQL-Abfrage eingesetzt.

Die Ausgabe erfolgt mit der Funktion echo. Durch die Verwendung des HTML-Tags <pre> wird eine formatierte Darstellung erreicht, bei der Zeilenumbrüche und Einrückungen erhalten bleiben. Dies ist besonders hilfreich für Debugging-Zwecke oder zur Veranschaulichung von Datenstrukturen.

Die eigentliche Datenbankoperation ist in einen try/catch-Block eingebettet. Dieses Konstrukt dient der strukturierten Fehlerbehandlung. Im try-Block steht der Code, der ausgeführt werden soll, während der catch-Block aufgerufen wird, falls ein Fehler auftritt. Im Unterschied zu einem einfachen Abbruch mit die() ermöglicht dieses Vorgehen eine kontrollierte Reaktion auf Fehler.

try/catch-Block

Innerhalb des try-Blocks wird zunächst eine Verbindung zur Datenbank aufgebaut, indem ein PDO-Objekt erzeugt wird. Die Verbindungsparameter werden in Form eines sogenannten DSN (Data Source Name) übergeben. Die Eingabe

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION

stellt sicher, dass Fehler als Exceptions gemeldet werden.

Anschließend wird ein Prepared Statement vorbereitet. Die SQL-Anweisung enthält mit :id einen benannten Platzhalter. Dieser wird erst beim Ausführen der Anweisung mit einem konkreten Wert belegt. Das erhöht die Sicherheit, da es vor SQL-Injection schützt, und sorgt zugleich für eine klare Trennung von SQL-Code und Daten.

Mit der Methode execute() wird die vorbereitete Anweisung ausgeführt. Dabei wird der Platzhalter :id mit dem Wert der Variablen $id verknüpft.

Die Ergebnismenge wird anschließend mit fetchAll(PDO::FETCH_ASSOC) abgerufen. Dabei werden alle Treffer als assoziatives Array geliefert, bei dem die Spaltennamen als Schlüssel dienen. Mit einer foreach-Schleife werden die einzelnen Ergebniszeilen durchlaufen und mit print_r() lesbar ausgegeben.

Tritt während der Ausführung ein Fehler auf, wird im catch-Block eine Fehlermeldung ausgegeben. Die Methode getMessage() liefert dabei eine genauere Beschreibung des aufgetretenen Problems.

2.5 KundenAbfragenPDO_2.php

2.5.1 Merkmale des Programms

  • Verwendete Klasse für Verbindungsaufbau: PDO
  • Start über Maskeneingabe (Formular)
  • Absicherung der Abfrage durch ein Prepared Statement
  • HTML-Formular mit Posting für die Eingabe
  • HTML-Ausgabe des Ergebnisses

Um das Zusammenspiel von PHP mit HTML zu demonstrieren, erweitern wir obiges Programm so, dass es über eine Eingabe per Maske gestartet wird.

So soll das Eingabeformular aussehen:

Und so das positive Ergebnis, falls 1001 eingegeben wurde:

2.5.2 Das Programm

<?php

 

/* Mit echo werden - wie oben ja auch schon mehrfach - die

Bestandteile des HTML-Dokuments an den Browser ausgegeben.

Der Browser interpretiert den erzeugten HTML-Code und stellt daraus

die Webseite dar. Er führt den PHP-Code nicht aus. Dieser wird zuvor

auf dem Webserver verarbeitet.

*/

echo "<!doctype html>";

echo "<html lang=\"de\">";

echo "<head>";

echo "<meta charset=\"UTF-8\">";

echo "<title>Kundensuche</title>";

echo "</head>";

echo "<body>";

echo "<h2>Abfrage Kundendaten</h2>";

 

/*

HTML-Formular ausgeben.

method="post" legt fest, dass die eingegebene Kundennummer

mit der HTTP-Methode POST übertragen wird.

Da kein action-Attribut angegeben ist, werden die Daten

an dieselbe Seite gesendet, d.h. an dieselbe URL.

Zu den Schrägstrichen:

Die PHP-Anweisung echo gibt eine Zeichenkette aus, die hier

HTML-Code enthält. Da sowohl die PHP-Zeichenkette als auch die

HTML-Attribute doppelte Anführungszeichen verwenden, müssen die

inneren Anführungszeichen durch einen vorangestellten umgekehrten

Schrägstrich (backslash) \ geschützt werden. Die Zeichenfolge \"

bedeutet, dass das Anführungszeichen zum Inhalt der Zeichenkette

gehört und diese nicht beendet. Der Backslash wird selbst nicht

ausgegeben.

Der normale Schrägstrich / in </form> oder </label> gehört dagegen

zum HTML-Code und kennzeichnet das Ende eines HTML-Elements.

*/

echo "<form method=\"post\">";

echo "<label for=\"id\">Kundennummer:</label> ";

echo "<input type=\"number\" id=\"id\" name=\"id\" min=\"1\"

    required> ";

echo "<input type=\"submit\" value=\"Start\">";

echo "</form>";

 

/*

Prüfen, ob das Formular mit der HTTP-Methode POST abgesendet wurde.

Beim ersten Aufruf der Seite ist die Bedingung nicht erfüllt. Dann

wird lediglich das Formular angezeigt.

*/

if ($_SERVER["REQUEST_METHOD"] === "POST") {

 

/*

Eingabewert aus den POST-Daten übernehmen.

Der Null-Koaleszenz-Operator ?? sorgt dafür, dass bei einem

fehlenden Formularelement ersatzweise die leere Zeichenkette

verwendet wird.

Die explizite Typumwandlung (int) wandelt den Wert anschließend

in eine ganze Zahl um.

*/

$id = (int)($_POST["id"] ?? "");

 

echo "<pre>";

echo "--- Verbindungsaufbau mit PDO ---\n";

echo "--- KuReAr, Relation kunden ---\n\n";

 

/*

Die Typumwandlung allein ist noch keine vollständige Eingabeprüfung.

Deshalb wird zusätzlich geprüft, ob die Kundennummer größer

als 0 ist.

*/

if ($id <= 0) {

  echo "Bitte geben Sie eine gültige Kundennummer ein.\n";

} else {

  try {

 

/*

Verbindung zur Datenbank aufbauen.

Der DSN enthält den Datenbanktreiber, den Datenbankserver,

die Datenbankbezeichnung und den Zeichensatz.

PDO::ERRMODE_EXCEPTION bewirkt, dass PDO bei Datenbankfehlern

eine PDOException auslöst.

*/

$pdo = new PDO("mysql:host=localhost;" . "dbname=KuReAr;"

  . "charset=utf8mb4", "root","",

  [PDO::ATTR_ERRMODE =>PDO::ERRMODE_EXCEPTION]

);

 

// Prepared Statement vorbereiten.

// :id ist ein benannter Platzhalter für die

// gesuchte Kundennummer.

$stmt = $pdo->prepare("SELECT * FROM kunden WHERE KuNr = :id");

 

// Prepared Statement ausführen.

// Der Wert der Variablen $id wird dem Platzhalter :id zugeordnet.

$stmt->execute(["id" => $id]);

 

// Alle gefundenen Datensätze als assoziative Arrays abrufen.

$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Prüfen, ob ein Kunde gefunden wurde.

if (count($rows) === 0) {

  echo "Kein Kunde mit der Kundennummer " . $id . " gefunden.\n";

} else {

// Gefundene Datensätze nacheinander

// ausgeben.

  foreach ($rows as $row) {

    print_r($row);

    echo "\n";

  }

  }

} catch (PDOException $e) {

 

// Datenbankfehler abfangen und ausgeben.

echo "PDO-Fehler: " . $e->getMessage() . "\n";

      }

  }

// HTML-Element für die vorformatierte Ausgabe schließen.

  echo "</pre>";

}

// HTML-Dokument beenden.

echo "</body>";

echo "</html>";

?>

2.5.3 Anmerkungen

  • Der Browser führt die ausgegebenen HTML-Zeilen nicht als "Programmzeilen" aus. PHP wird auf dem Webserver ausgeführt; der Browser empfängt den dabei erzeugten HTML-Code, interpretiert ihn und stellt die Webseite dar.
  • Das label-Element wurde mit dem Eingabefeld verbunden. Dazu gehören for="id" beim Label und id="id" beim Eingabefeld.
  • min="1" verhindert im Browser die Eingabe einer Kundennummer kleiner als 1. Die serverseitige Prüfung in PHP bleibt trotzdem erforderlich.
  • (int) ist eine explizite Typumwandlung, aber allein keine "sichere" oder vollständige Eingabeprüfung. Beispielsweise werden ungültige Zeichenketten zu 0 umgewandelt. Deshalb wird anschließend $id <= 0 geprüft.
  • $_POST["id"] wurde durch $_POST["id"] ?? "" abgesichert. Dadurch entsteht keine Warnung, falls der Wert wider Erwarten fehlt.

3 Eintragen von Daten

In diesem Kapitel wird gezeigt, wie aus PHP-Programmen in Relationale Datenbanken Daten eingetragen werden. Zuerst am Beispiel MySQLi, dann mit PDO.

3.1 KundenEintragenMySQLi

3.1.1 Das Programm

<?php

// Variable für Rückmeldungen an den Benutzer.

// Sie bleibt zunächst leer und wird später mit einer

// Erfolgs- oder Fehlermeldung beschrieben.

$message = "";

// Zugangsdaten zur Datenbank.

$host = "localhost";

$user = "root";

$pass = "";

$db = "KuReAr";

// Prüfen, ob das Formular mit der HTTP-Methode POST abgeschickt

// wurde. Der PHP-Code zum Einfügen wird erst nach dem Absenden des

// Formulars ausgeführt.

if ($_SERVER["REQUEST_METHOD"] === "POST") {

/*

Eingaben aus dem Formular übernehmen. Der Operator ?? liefert

einen Standardwert, falls kein Wert übertragen wurde. trim()

entfernt Leerzeichen am Anfang und Ende. (int) wandelt den

übernommenen Wert in eine ganze Zahl um.

*/

  $kuNr = (int)($_POST["kuNr"] ?? 0);

  $name = trim($_POST["name"] ?? "");

  $vorname = trim($_POST["vorname"] ?? "");

// Eingaben auf Plausibilität prüfen.

// Die Kundennummer muss größer als 0 sein.

// Name und Vorname dürfen nicht leer sein.

if ($kuNr <= 0 || $name === "" || $vorname === "") {

  $message = "Bitte alle Felder korrekt ausfüllen.";

} else {

// Verbindung zur Datenbank aufbauen.

// Die Variable $con verweist auf das erzeugte

// mysqli-Objekt.

$con = new mysqli($host, $user, $pass, $db);

// Prüfen, ob beim Verbindungsaufbau ein Fehler

// aufgetreten ist. die() ("sterben!") gibt die Meldung aus

// und beendet das Programm.

if ($con->connect_error) {

  die("DB-Verbindung fehlgeschlagen: " . $con->connect_error);

}

// Zeichensatz für die Datenbankverbindung festlegen.

// utf8mb4 unterstützt Umlaute und andere

// Unicode-Zeichen.

$con->set_charset("utf8mb4");

// SQL-Anweisung vorbereiten.

// Die Fragezeichen dienen als Platzhalter für

// Kundennummer, Name und Vorname.

$stmt = $con->prepare(

  "INSERT INTO kunden (KuNr, Name, Vorname)

      VALUES (?, ?, ?)"

);

// Prüfen, ob das Prepared Statement erstellt

// werden konnte.

if (!$stmt) {

  $message = "Prepare fehlgeschlagen: " . $con->error;

} else {

/*

Werte an die Platzhalter binden.

"i" bezeichnet einen Integer-Wert.

"s" bezeichnet einen String-Wert.

"iss" steht daher für Integer, String, String.

*/

$stmt->bind_param("iss",$kuNr,$name,$vorname);

// Prepared Statement ausführen.

if ($stmt->execute()) {

  $message = "Datensatz wurde eingefügt.";

} else {

// Fehlermeldung des Statements übernehmen.

    $message = "Fehler beim Einfügen: " . $stmt->error;

}

// Prepared Statement schließen und die dafür

// verwendeten Ressourcen freigeben.

$stmt->close();

}

// Datenbankverbindung schließen und die damit

// verbundenen Ressourcen freigeben.

$con->close();

  }

}

?>

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Kunde eintragen mit MySQLi</title>

    <style>body {font-family: Arial, sans-serif;

    }

    </style>

</head>

 

<body>

<h2>Kunde eintragen (MySQLi)</h2>

<!--

Formular zur Eingabe der Kundendaten.

method="post" legt fest, dass die Formulardaten

mit der HTTP-Methode POST übertragen werden.

-->

<form method="post">

    <label for="kuNr">Kundennummer:</label>

    <input type="number" id="kuNr" name="kuNr" min="1"

      required>

<br><br>

    

    <label for="name">Name:</label>

    <input type="text" id="name" name="name" maxlength="10"

      required>

<br><br>

    <label for="vorname">Vorname:</label>

    <input type="text" id="vorname" name="vorname"

      maxlength="10" required>

<br><br>

    

<!-- Schaltfläche zum Absenden des Formulars. -->

<button type="submit">Speichern</button>

</form>

<!--

Wenn eine Rückmeldung vorliegt, wird sie in Fettschrift ausgegeben.

Zu htmlspecialchars() vgl. den Anhang.

-->

<?php if ($message !== ""): ?>

<p><strong>

  <?php

    echo htmlspecialchars($message,ENT_QUOTES,"UTF-8");

  ?>

  </strong>

</p>

<?php endif; ?>

</body>

</html>

3.1.2 Anmerkungen

  • Die Platzhalter werden nicht durch eine gewöhnliche Textersetzung "gefüllt". bind_param() bindet die Werte getrennt an die SQL-Anweisung.
  • htmlspecialchars() sorgt hier dafür, dass Zeichen wie <, > und Anführungszeichen nicht als HTML interpretiert werden. Vgl. Anhang.
  • Die Attribute for und id ordnen jede Beschriftung eindeutig ihrem Eingabefeld zu.
  • min="1" ergänzt die Prüfung der Kundennummer im Browser. Die serverseitige PHP-Prüfung bleibt trotzdem erforderlich.
  • Existiert die angegebene Kundennummer bereits, wird das Einfügen bei einem Primärschlüssel beziehungsweise einer eindeutigen Einschränkung abgelehnt und die Datenbankfehlermeldung ausgegeben.

3.2 RechnungSpeichernMySQLi

3.2.1 Merkmale des Programms

  • Speichern in mehreren verknüpften Dateien
  • Transaktionen
  • Klasse MySQLi

3.2.2 Aufgabe

Es wird eine Rechnung in die Datenbank KuReAr eingetragen, konkret der Rechnungskopf mit seinen Positionen sowie die Kundennummer. Insgesamt macht das Transaktionen nötig, deren Realisierung damit hier ebenfalls gezeigt wird. Realisiert wird das mit Hilfe des Programmes RechnungSpeichernMySQLi.php, diesmal ohne Eingabe über die Webseite.

Transaktionen

Die neue Rechnung hat die Rechnungsnummer 23012, bezieht sich auf den Kunden mit der Kundennummer 1002 und eingekauft werden die Artikel mit den Artikelnummern 100, 101 und 102. Nach dem erfolgten Eintrag soll folgende Abfrage möglich sein:

SELECT r.ReNr, r.KuNr, r.reDatum, p.PosNr, p.ArtNr, p.Anzahl, k.Name,

  k.Vorname FROM rechkoepfe r, rechpos p, kunden k

  where r.kunr=k.kunr and r.renr=p.ReNr and r.kunr=1002

      and r.renr=23012;


ReNr

KuNr

reDatum

PosNr

ArtNr

Anzahl

Name

Vorname

23012

1002

2023-03-28

1

100

2

Hinterhof

Rudolf

23012

1002

2023-03-28

2

101

1

Hinterhof

Rudolf

23012

1002

2023-03-28

3

102

5

Hinterhof

Rudolf

 

Anmerkung: Um die Rechnung speichern zu können sollten die oben angeführten Einträge in kunden (Kundenummer 1002) und artikel (Artikelnummern 100, 101 und 102) in der Datenbank vorhanden sein. In rechnung darf die Rechnungsnummer 23012 noch nicht vergeben sein.

3.2.3 Das Programm

<?php

// Daten für den Verbindungsaufbau.

$host = "localhost";

$user = "root";

$pass = "";

$db = "KuReAr";

// Daten des Rechnungskopfes.

$reNr = 23012;

$kuNr = 1002;

$reDatum = "2023-03-28";

// Daten der Rechnungspositionen.

// In diesem Beispiel werden die Daten unmittelbar im Programm

// festgelegt. In einer Webanwendung stammen sie üblicherweise

// aus einem Formular.

$positionen = [

  ["posNr" => 1,"artNr" => 100,"anzahl" => 2],

  ["posNr" => 2,"artNr" => 101,"anzahl" => 1],

  ["posNr" => 3,"artNr" => 102,"anzahl" => 5]];

// Die Variable enthält zunächst kein Verbindungsobjekt.

// Nach erfolgreichem Verbindungsaufbau verweist sie

// auf das erzeugte mysqli-Objekt.

$con = null;

try {

// Verbindung zur Datenbank aufbauen.

  $con = new mysqli($host,$user,$pass,$db);

// Prüfen, ob beim Verbindungsaufbau ein Fehler

// aufgetreten ist. In diesem Fall wird eine

// Exception ausgelöst.

    if ($con->connect_error) {throw new Exception(

    "MySQLi-Verbindungsfehler: " . $con->connect_error);

}

// Zeichensatz für die Datenbankverbindung festlegen.

$con->set_charset("utf8mb4");

// Transaktion starten.

// Der Rechnungskopf und alle Rechnungspositionen

// bilden eine gemeinsame Arbeitseinheit. Sie werden

// entweder vollständig oder gar nicht gespeichert.

$con->begin_transaction();

// Prepared Statement zum Einfügen des Rechnungskopfes

// vorbereiten. Die Fragezeichen dienen als Platzhalter.

    $stmtKopf = $con->prepare("INSERT INTO rechkoepfe

      (ReNr, KuNr, ReDatum) VALUES (?, ?, ?)");

// Prüfen, ob das Prepared Statement erstellt

// werden konnte.

    if (!$stmtKopf) {throw new Exception(

      "Prepare fehlgeschlagen (Rechnungskopf): "

      . $con->error);

}

/*

Werte an die Platzhalter binden.

"i" bezeichnet einen Integer-Wert.

"s" bezeichnet einen String-Wert.

"iis" steht also für Integer, Integer, String.

*/

$stmtKopf->bind_param("iis",$reNr,$kuNr,$reDatum);

// Prepared Statement ausführen.

if (!$stmtKopf->execute()) {throw new Exception(

    "Execute fehlgeschlagen (Rechnungskopf): "

      . $stmtKopf->error);

}

// Statement für den Rechnungskopf schließen und die

// dafür verwendeten Ressourcen freigeben.

$stmtKopf->close();

// Prepared Statement zum Einfügen der

// Rechnungspositionen vorbereiten.

$stmtPos = $con->prepare("INSERT INTO rechpos

  (ReNr, PosNr, ArtNr, Anzahl) VALUES (?, ?, ?, ?)");

// Prüfen, ob das Prepared Statement erstellt

// werden konnte.

if (!$stmtPos) {throw new Exception(

    "Prepare fehlgeschlagen (Rechnungspositionen): "

      . $con->error);

}

// Variablen für die Werte einer Rechnungsposition.

// Sie werden in jedem Schleifendurchlauf neu beschrieben.

$posNr = 0;

$artNr = 0;

$anzahl = 0;

/*

Variablen an die Platzhalter binden. Alle vier Werte sind ganze

Zahlen. MySQLi bindet hier die Variablen, nicht nur deren

gegenwärtige Werte. Deshalb können sie innerhalb der Schleife

neu beschrieben und das Statement mehrfach ausgeführt werden.

*/

$stmtPos->bind_param("iiii",$reNr,$posNr,$artNr,$anzahl);

// Die Rechnungspositionen nacheinander bearbeiten.

foreach ($positionen as $p) {

// Werte aus dem jeweiligen Array-Element übernehmen

// und in ganze Zahlen umwandeln.

  $posNr = (int)$p["posNr"];

  $artNr = (int)$p["artNr"];

  $anzahl = (int)$p["anzahl"];

// Anzahl auf Plausibilität prüfen.

  if ($anzahl <= 0) {throw new Exception(

    "Anzahl muss größer als 0 sein " . "(PosNr $posNr).");

  }

// Prepared Statement mit den Werten der aktuellen

// Rechnungsposition ausführen.

  if (!$stmtPos->execute()) {throw new Exception(

    "Execute fehlgeschlagen " . "(PosNr $posNr): "

      . $stmtPos->error);

  }

}

// Statement für die Rechnungspositionen schließen und

// die dafür verwendeten Ressourcen freigeben.

$stmtPos->close();

/*

Transaktion erfolgreich abschließen.

Erst dadurch werden der Rechnungskopf und alle

Rechnungspositionen dauerhaft gespeichert.

*/

$con->commit();

echo "Rechnung erfolgreich gespeichert.";

} catch (Exception $e) {

// Wenn eine Datenbankverbindung besteht, werden die seit Beginn der

// Transaktion vorgenommenen Änderungen rückgängig gemacht.

  if ($con instanceof mysqli) {

    $con->rollback();

  }

    echo "Fehler beim Speichern. " . "Alles wurde zurückgesetzt: "

    . $e->getMessage();

  } finally {

// Datenbankverbindung schließen und die damit

// verbundenen Ressourcen freigeben.

    if ($con instanceof mysqli) {$con->close();

  }

}

?>

Daran denken: Falls Sie das Programm mehrfach erfolgreich ausführen wollen, müssen Sie diese Rechnung (23012) löschen und dabei, wegen der Fremdschlüssel, die Rechnungspositionen zuerst.

3.3 KundenEintragenPDO.php

3.3.1 Merkmale des Programms

  • Verwendete Klasse für Verbindungsaufbau: PDO
  • Absicherung durch Prepared Statements
  • Start durch Browser-Befehl
  • HTML-Ausgabe des Ergebnisses

3.3.2 Das Programm

<?php

 

// Variable für Rückmeldungen an den Benutzer.

// Sie bleibt zunächst leer und wird später mit einer

// Erfolgs- oder Fehlermeldung beschrieben.

$message = "";

/*

Daten für den Verbindungsaufbau. Der DSN (Data Source Name) enthält:

- den verwendeten Datenbanktreiber: mysql

- den Datenbankserver: localhost

- die Bezeichnung der Datenbank: KuReAr

- den Zeichensatz: utf8mb4

*/

$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";

$user = "root";

$pass = "";

// Prüfen, ob das Formular mit der HTTP-Methode POST

// abgeschickt wurde. Der Code zum Einfügen wird erst

// nach dem Absenden des Formulars ausgeführt.

if ($_SERVER["REQUEST_METHOD"] === "POST") {

/*

Eingaben aus dem Formular übernehmen. Der Operator ?? liefert

einen Standardwert, falls kein entsprechender Wert übertragen wurde.

trim() entfernt Leerzeichen am Anfang und Ende.

(int) wandelt den übernommenen Wert in eine ganze Zahl um.

*/

$kuNr = (int)($_POST["kuNr"] ?? 0);

$name = trim($_POST["name"] ?? "");

$vorname = trim($_POST["vorname"] ?? "");

// Eingaben auf Plausibilität prüfen.

// Die Kundennummer muss größer als 0 sein.

// Name und Vorname dürfen nicht leer sein.

  if ($kuNr <= 0 || $name === "" || $vorname === "") {

    $message = "Bitte alle Felder korrekt ausfüllen.";

  } else {

try {

/*

Verbindung zur Datenbank aufbauen.

PDO steht für PHP Data Objects. Die Variable $pdo verweist auf das

erzeugte PDO-Objekt.

PDO::ATTR_ERRMODE legt fest, wie PDO auf Fehler reagieren soll.

PDO::ERRMODE_EXCEPTION bewirkt, dass bei einem Datenbankfehler eine

Exception ausgelöst wird.

*/

    $pdo = new PDO( $dsn, $user, $pass,

      [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]);

// Prepared Statement vorbereiten. :kuNr, :name und :vorname sind

// benannte Platzhalter. Die Eingabewerte werden erst beim

// Ausführen an diese Platzhalter gebunden.

    $stmt = $pdo->prepare( "INSERT INTO kunden (KuNr, Name, Vorname)

      VALUES (:kuNr, :name, :vorname)"

    );

// Prepared Statement ausführen. Das Array ordnet jedem benannten

// Platzhalter den zugehörigen Wert zu. Bei den Array-Schlüsseln

// kann der Doppelpunkt weggelassen werden.

    $stmt->execute(["kuNr" => $kuNr,"name" => $name,

    "vorname" => $vorname]);

// Diese Zeile wird nur erreicht, wenn execute()

// keine Exception ausgelöst hat.

    $message = "Datensatz wurde eingefügt.";

  } catch (PDOException $e) {

 

// PDO-Datenbankfehler abfangen und eine entsprechende Rückmeldung

// erzeugen.

    $message = "Fehler beim Einfügen: " . $e->getMessage();

    }

  }

}

?>

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Kunde eintragen mit PDO</title>

  <style>

    body {font-family: Arial, sans-serif;}

  </style>

</head>

 

<body>

<h2>Kunde eintragen (PDO)</h2>

<!--

Formular zur Eingabe der Kundendaten.

method="post" legt fest, dass die Formulardaten mit der HTTP-Methode

POST übertragen werden. Da kein action-Attribut angegeben ist,

werden die Daten an dieselbe PHP-Datei gesendet (d.h. an die, von

der der Aufruf kommt.

-->

<form method="post">

    <label for="kuNr">Kundennummer:</label>

    <input type="number" id="kuNr" name="kuNr" min="1" required>

    <br><br>

    <label for="name">Name:</label>

    <input type="text" id="name" name="name" maxlength="10" required>

    <br><br>

    <label for="vorname">Vorname:</label>

    <input type="text" id="vorname" name="vorname" maxlength="10"

      required>

    <br><br>

     

<!-- Schaltfläche zum Absenden des Formulars. -->

    <button type="submit">Speichern</button>

  </form>

 

<!--

Wenn eine Rückmeldung vorliegt, wird sie in

Fettschrift ausgegeben.

Zu htmlspecialchars() vgl. den Anhang.

-->

  <?php if ($message !== ""): ?>

  <p><strong>

    <?php echo htmlspecialchars($message,ENT_QUOTES,"UTF-8"); ?>

  </strong>

  </p>

<?php endif; ?>

</body>

</html>

3.3.3 Anmerkungen

  • PDO ist eine einheitliche Schnittstelle für den Zugriff auf unterschiedliche Datenbanksysteme. Hier wird durch mysql: im DSN der MySQL-Treiber ausgewählt.
  • Im Unterschied zur vorherigen MySQLi-Fassung wird der Zeichensatz bereits im DSN mit charset=utf8mb4 festgelegt.
  • PDO verwendet hier benannte Platzhalter wie :kuNr. Dadurch ist unmittelbar erkennbar, welcher Wert an welcher Stelle eingesetzt wird.
  • Die Werte werden getrennt von der SQL-Anweisung an execute() übergeben. Das schützt insbesondere vor SQL-Injection.
  • Weil PDO::ERRMODE_EXCEPTION aktiviert ist, verzweigt das Programm bei einem Datenbankfehler unmittelbar in den catch-Block.
  • Ein ausdrückliches Schließen von Statement und Datenbankverbindung ist bei PDO normalerweise nicht erforderlich. Die zugehörigen Ressourcen werden spätestens am Ende der Skriptausführung freigegeben.
  • required, min und maxlength prüfen Eingaben zunächst im Browser. Die Prüfung in PHP bleibt dennoch erforderlich, weil Browserprüfungen umgangen werden können.
  • Das Programm benötigt keine Transaktion, weil nur ein einzelner Datensatz mit einer einzigen INSERT-Anweisung gespeichert wird. Eine einzelne SQL-Anweisung wird von der Datenbank bereits als unteilbare Operation behandelt.

3.4 RechnungSpeichernPDO.php

3.4.1 Merkmale des Programms:

  • Speichern verknüpfter Relationen ("Rechnung")
  • Verwendete Klasse für Verbindungsaufbau: PDO
  • Transaktionen
  • Absicherung durch Prepared Statements
  • Start durch Browser-Befehl
  • HTML-Ausgabe des Ergebnisses

3.4.2 Aufgabe

Es wird eine Rechnung in die Datenbank KuReAr eingetragen, konkret der Rechnungskopf mit seinen Positionen sowie die Kundennummer. Insgesamt macht das Transaktionen nötig, deren Realisierung damit hier ebenfalls gezeigt wird. Realisiert wird das mit Hilfe des Programmes RechnungSpeichernPDO.php, diesmal ohne Eingabe über die Webseite.

Transaktionen

Die neue Rechnung hat die Rechnungsnummer 23002, bezieht sich auf den Kunden mit der Kundennummer 1010 und eingekauft werden die Artikel mit den Artikelnummern 100, 101 und 102. Nach dem erfolgten Eintrag soll folgende Abfrage möglich sein:

SELECT r.ReNr, r.KuNr, r.reDatum, p.PosNr, p.ArtNr, p.Anzahl, k.Name,

    k.Vorname FROM rechkoepfe r, rechpos p, kunden k

      where r.kunr=k.kunr and r.renr=p.ReNr and r.renr=23002;

 


ReNr

KuNr

reDatum

PosNr

ArtNr

Anzahl

Name

Vorname

23002

1010

2026-03-28

1

100

2

Anger

Albert

23002

1010

2026-03-28

2

101

1

Anger

Albert

23002

1010

2026-03-28

3

102

5

Anger

Albert

3.4.3 Das Programm

<?php

/*

Das Programm speichert einen Rechnungskopf und die zugehörigen

Rechnungspositionen in einer Transaktion. Dadurch wird

sichergestellt, dass die Rechnung entweder vollständig oder gar

nicht in der Datenbank gespeichert wird.

Daten für den Verbindungsaufbau.

DSN bedeutet Data Source Name. Diese Zeichenkette enthält:

- den verwendeten Datenbanktreiber: mysql

- den Datenbankserver: localhost

- die Bezeichnung der Datenbank: KuReAr

- den Zeichensatz: utf8mb4

*/

$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";

$user = "root";

$pass = "";

 

/*

Zentrale Daten des neuen Rechnungskopfes.

In einer Webanwendung würden diese Werte üblicherweise aus einem

Formular stammen.

*/

$reNr = 23011;

$kuNr = 1004;

$reDatum = "2026-03-28";

 

// Daten der Rechnungspositionen.

// Das äußere Array enthält die Rechnungspositionen als Ganzes.

// Jedes innere Array beschreibt eine einzelne Position.

$positionen = [["posNr" => 1,"artNr" => 100,"anzahl" => 2],

    ["posNr" => 2,"artNr" => 101,"anzahl" => 1],

    ["posNr" => 3,"artNr" => 102,"anzahl" => 5]];

try {

 

/*

Verbindung zur Datenbank aufbauen.

PDO steht für PHP Data Objects. Die Variable $pdo verweist auf das

erzeugte PDO-Objekt.

PDO::ERRMODE_EXCEPTION bewirkt, dass PDO bei einem Datenbankfehler

eine Exception auslöst.

*/

  $pdo = new PDO($dsn,$user,$pass,

  [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

 

// Transaktion starten. Alle folgenden Datenbankoperationen gehören

// nun zu einer gemeinsamen Arbeitseinheit. Dauerhaft gespeichert

// werden die Änderungen erst durch commit().

  $pdo->beginTransaction();

 

// Prepared Statement für den Rechnungskopf vorbereiten.

// :reNr, :kuNr und :reDatum sind benannte Platzhalter. Sie werden

// beim Aufruf von execute() mit konkreten Werten verbunden.

  $stmtKopf = $pdo->prepare(

  "INSERT INTO rechkoepfe (ReNr, KuNr, ReDatum)

  VALUES (:reNr, :kuNr, :reDatum)");

 

// Prepared Statement für den Rechnungskopf ausführen.

// Das assoziative Array ordnet jedem benannten Platzhalter den

// zugehörigen Wert zu.

  $stmtKopf->execute(["reNr" => $reNr,"kuNr" => $kuNr,

    "reDatum" => $reDatum]);

 

// Prepared Statement für die Rechnungspositionen vorbereiten.

// Das Statement wird nur einmal vorbereitet und danach

// für jede Rechnungsposition erneut ausgeführt.

$stmtPos = $pdo->prepare("INSERT INTO rechpos

  (ReNr, PosNr, ArtNr, Anzahl) VALUES

    (:reNr, :posNr, :artNr, :anzahl)");

 

// Rechnungspositionen nacheinander bearbeiten.

foreach ($positionen as $p) {

 

// Werte der aktuellen Rechnungsposition übernehmen und in ganze

// Zahlen umwandeln.

    $posNr = (int)$p["posNr"];

    $artNr = (int)$p["artNr"];

    $anzahl = (int)$p["anzahl"];

 

// Einfache Plausibilitätsprüfung.

// Eine Anzahl kleiner oder gleich 0 soll nicht gespeichert werden.

    if ($anzahl <= 0) {throw new Exception(

    "Anzahl muss größer als 0 sein " . "(PosNr $posNr).");}

 

// Prepared Statement für die aktuelle Rechnungsposition ausführen.

  $stmtPos->execute(["reNr" => $reNr,"posNr" => $posNr,

    "artNr" => $artNr,"anzahl" => $anzahl]);

}

 

// Transaktion erfolgreich abschließen. Erst durch commit() werden

// der Rechnungskopf und sämtliche Rechnungspositionen dauerhaft

// gespeichert.

  $pdo->commit();

  echo "Rechnung erfolgreich gespeichert.";

} catch (Exception $e) {

 

// Prüfen, ob das PDO-Objekt vorhanden und noch eine Transaktion

// aktiv ist. Ist dies der Fall, werden mit rollBack() sämtliche

// Änderungen seit beginTransaction() zurückgenommen.

  if (isset($pdo) && $pdo->inTransaction()) {

    $pdo->rollBack();

  }

 

// Fehlermeldung ausgeben.

    echo "Fehler beim Speichern. " . "Alles wurde zurückgesetzt: "

      . $e->getMessage();

}

?>

Daran denken: Ein wiederholter Programmlauf erfordert das Löschen der obigen Rechnung.

3.4.4 Anmerkungen

  • Die Positionswerte werden vor ihrer Prüfung und Speicherung ausdrücklich in int umgewandelt.
  • Das Prepared Statement für die Positionen wird nur einmal vorbereitet. Innerhalb der Schleife wird es mit jeweils anderen Werten mehrfach ausgeführt.
  • commit() speichert alle Änderungen der Transaktion dauerhaft. Bei einem Fehler nimmt rollBack() sowohl den Rechnungskopf als auch bereits eingefügte Rechnungspositionen zurück.
  • inTransaction() verhindert, dass rollBack() aufgerufen wird, obwohl keine aktive Transaktion besteht.
  • Die Kundennummer 1004 muss bereits in kunden vorhanden sein. Ebenso müssen die Artikelnummern 100, 101 und 102 in der Artikelrelation existieren. Andernfalls verhindert ein Fremdschlüssel das Speichern.
  • Auch die Rechnungsnummer 23011 darf noch nicht vorhanden sein, wenn ReNr Primärschlüssel oder eindeutig ist.

3.4.5 Programmbeschreibung

Das Programm realisiert das Eintragen einer vollständigen Rechnung mit PDO und verwendet dabei eine Transaktion. Das ist notwendig, weil eine Rechnung aus zwei zusammengehörigen Teilen besteht: aus dem Rechnungskopf in der Relation rechkoepfe und aus den Rechnungspositionen in der Relation rechpos. Beide Teile müssen vollständig gespeichert werden. Es darf also nicht vorkommen, dass zwar der Rechnungskopf eingetragen wird, einzelne Positionen aber wegen eines Fehlers fehlen.

Zu Beginn werden die Verbindungsdaten zur Datenbank in der Variablen $dsn sowie in $user und $pass festgelegt. Danach folgen die Daten des neuen Rechnungskopfes, also Rechnungsnummer, Kundennummer und Rechnungsdatum. Die zugehörigen Positionen werden in einem mehrdimensionalen Array gespeichert. Jedes innere Array steht dabei für genau eine Rechnungsposition mit Positionsnummer, Artikelnummer und Anzahl.

Anschließend beginnt ein try/catch-Block. Im try-Teil wird zunächst ein PDO-Objekt erzeugt, das die Verbindung zur Datenbank herstellt. Die Eingabe

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION

bewirkt, dass PDO bei Fehlern Exceptions auslöst. Dadurch können Fehler zentral im catch-Teil behandelt werden.

Nach dem Verbindungsaufbau wird mit beginTransaction() eine Transaktion gestartet. Ab diesem Zeitpunkt gehören alle folgenden Änderungen zu einer gemeinsamen Arbeitseinheit. Dauerhaft in der Datenbank gespeichert werden sie erst dann, wenn am Ende commit() ausgeführt wird.

Danach wird ein prepared statement für den Rechnungskopf vorbereitet. In der SQL-Anweisung stehen mit :reNr, :kuNr und :redatum sogenannte named parameter. Das sind Platzhalter, die erst beim Aufruf von execute() mit konkreten Werten belegt werden. Auf diese Weise werden SQL-Anweisung und Daten voneinander getrennt.

Im nächsten Schritt wird ein zweites prepared statement für die Rechnungspositionen vorbereitet. Dieses Statement wird nur einmal erzeugt und anschließend in der Schleife für jede Position erneut verwendet. Die foreach-Schleife durchläuft nacheinander alle Positionen des Arrays $positionen. Vor dem Eintragen wird hier noch ein einfacher Plausibilitätscheck durchgeführt: Die Anzahl muss größer als 0 sein. Ist das nicht der Fall, wird mit throw new Exception(...) eine Exception ausgelöst. Dadurch wird der normale Programmablauf sofort unterbrochen, und die Verarbeitung springt in den catch-Block.

Wenn dagegen alle Positionen korrekt eingetragen werden konnten, wird am Ende mit commit() die gesamte Transaktion dauerhaft gespeichert. Erst in diesem Moment werden also Rechnungskopf und Rechnungspositionen verbindlich in die Datenbank übernommen.

Tritt irgendwo im try-Block ein Fehler auf, so wird der catch-Block ausgeführt. Dort wird mit rollBack() die laufende Transaktion vollständig zurückgenommen. Damit werden alle bis dahin vorgenommenen Änderungen rückgängig gemacht. Genau das ist der entscheidende Vorteil der Transaktion: Die Rechnung wird entweder vollständig gespeichert oder gar nicht. Auf diese Weise bleiben die Daten in der Datenbank konsistent.

4 Löschen von Daten

Hier wird jeweils zuerst das Löschen in einer einzigen Relation (Kunden) und dann das Löschen in mehreren zusammenhängenden (Rechnung) gezeigt. Zuerst mit MySQLi, dann mit PDO.

4.1 KundenLoeschenMySQLi.php

4.1.1 Merkmale des Programms:

  • Verwendete Klasse für Verbindungsaufbau: mysqli
  • Löschen in einer Relation (Kunden)
  • Absicherung durch Prepared Statements
  • Start durch Browser-Befehl
  • HTML-Ausgabe des Ergebnisses

Zum Nachvollziehen: Zuerst einen Kunden anlegen ohne Fremdschlüsselbeziehungen zu Rechnungsköpfen, z.B. mit dem Programm KundenEintragenMySQLi.php. Der kann dann mit diesem Programm gelöscht werden.

4.1.2 Das Programm

<?php

 

// Variable für Rückmeldungen an den Benutzer.

// Sie speichert Erfolgs- oder Fehlermeldungen,

// die später im HTML-Abschnitt ausgegeben werden.

$message = "";

 

// Daten für den Verbindungsaufbau.

$host = "localhost";

$user = "root";

$pass = "";

$db = "KuReAr";

 

// Prüfen, ob das Formular mit der HTTP-Methode POST

// abgeschickt wurde.

if ($_SERVER["REQUEST_METHOD"] === "POST") {

 

// Kundennummer aus dem Formular übernehmen.

// Der Operator ?? liefert 0, wenn der Formulareintrag fehlt.

// (int) wandelt den übernommenen Wert in eine ganze Zahl um.

$kuNr = (int)($_POST["kuNr"] ?? 0);

 

// Eingabe prüfen. Die Kundennummer muss größer als 0 sein.

if ($kuNr <= 0) {

  $message = "Bitte eine gültige Kundennummer eingeben.";

  } else {

 

// Verbindung zur Datenbank aufbauen.

// Die Variable $con verweist auf das erzeugte mysqli-Objekt.

$con = new mysqli($host, $user, $pass, $db);

 

// Prüfen, ob beim Verbindungsaufbau ein Fehler aufgetreten ist.

// die() gibt die Meldung aus und beendet das Programm.

  if ($con->connect_error) {die("DB-Verbindung fehlgeschlagen: "

    . $con->connect_error

  );

}

 

// Zeichensatz für die Datenbankverbindung festlegen,

// damit Sonderzeichen korrekt verarbeitet werden.

$con->set_charset("utf8mb4");

 

// SQL-Anweisung vorbereiten.

// Das Fragezeichen dient als Platzhalter für die Kundennummer.

    $stmt = $con->prepare("DELETE FROM kunden WHERE KuNr = ?");

 

// Prüfen, ob das Prepared Statement erstellt werden konnte.

if (!$stmt) {

  $message = "Prepare fehlgeschlagen: " . $con->error;

} else {

 

// Kundennummer an den Platzhalter binden.

// "i" bezeichnet den Datentyp Integer.

$stmt->bind_param("i", $kuNr);

 

// Prepared Statement ausführen.

if ($stmt->execute()) {

 

// affected_rows enthält die Anzahl der Datensätze, die durch die

// DELETE-Anweisung gelöscht wurden.

  if ($stmt->affected_rows > 0) {

    $message = "Datensatz wurde gelöscht.";

    } else {$message = "Kein Datensatz mit dieser "

      . "Kundennummer gefunden.";

    }

  } else {$message = "Fehler beim Löschen: "

    . $stmt->error;

  }

 

// Prepared Statement schließen und die dafür verwendeten Ressourcen

// freigeben.

  $stmt->close();

}

 

// Datenbankverbindung schließen und die damit verbundenen

// Ressourcen freigeben.

  $con->close();

  }

}

?>

<!doctype html>

<html lang="de">

<head>

<meta charset="utf-8">

<title>Kunde löschen mit MySQLi</title>

  <style>

    body {font-family: Arial, sans-serif;}

  </style>

</head>

<body>

<h2>Kunde löschen (MySQLi)</h2>

 

<!--

Formular zur Eingabe der Kundennummer.

method="post" legt fest, dass die Formulardaten mit der

HTTP-Methode POST übertragen werden. Da kein action-Attribut

angegeben ist, werden sie an die sendende PHP-Datei gesendet.

-->

<form method="post">

<label for="kuNr">Kundennummer:</label>

    <input type="number" id="kuNr" name="kuNr" min="1" required>

<button type="submit">Löschen</button>

</form>

 

<!--Zu htmlspecialchars() vgl. den Anhang.

-->

<?php if ($message !== ""): ?>

  <p><strong>

  <?php echo htmlspecialchars($message,ENT_QUOTES,"UTF-8");?>

  </strong>

  </p>

<?php endif; ?>

</body>

</html>

4.1.3 Programmlauf

Wichtig: Geben Sie einen Kunden an, der nicht durch Schlüssel-/Fremdschlüsselbeziehungen eingebunden ist. Also: Mit obigem Programm KundenEintragen einen "isolierten" Kunden eintragen und diesen dann hier löschen.

So sieht die Eingabemaske aus.

4.2 RechnungLoeschenMySQLi.php

4.2.1 Merkmale des Programms

  • Verwendete Klasse für Verbindungsaufbau: mysqli
  • Löschen in mehreren relational verknüpften Dateien der Datenbank KuReAr
  • Durchführung einer Transaktion
  • Absicherung durch ein Prepared Statement
  • Start durch ein Formular
  • HTML-Ausgabe des Ergebnisses

4.2.2 Aufgabe

Hier wird eine Rechnung in der Datenbank KuReAr gelöscht, d.h., der Rechnungskopf mit seinen Positionen. Insgesamt macht das Transaktionen nötig, deren Realisierung damit hier ebenfalls gezeigt wird. Realisiert wird das mit Hilfe des Programmes RechnungLoeschenMySQLi.php. Das Programm soll über die Konsole gestartet werden.

Transaktionen

Das folgende Beispiel wird unten verwendet, eine Rechnung des (natürlich) fiktiven Kunden Rudolf Hinterhof mit zwei Positionen.

SELECT r.ReNr, r.KuNr, r.Datum, p.PosNr, p.ArtNr, p.Anzahl, k.Name,

  k.Vorname FROM rechkoepfe r, rechpos p, kunden k

where r.kunr=k.kunr and r.renr=p.ReNr and r.renr=23001;


ReNr

KuNr

Datum

PosNr

ArtNr

Anzahl

Name

Vorname

23001

1002

2026-03-27

1

101

2

Hinterhof

Rudolf

23001

1002

2026-03-27

2

554

1

Hinterhof

Rudolf

 

Die Webseite für die Dateneingabe soll wie folgt sein:

4.2.3 Das Programm

<?php

 

// Daten für den Verbindungsaufbau. Oben schon mehrfach beschrieben

$host = "localhost";

$user = "root";

$pass = "";

$db = "KuReAr";

 

// Variable für Rückmeldungen an den Benutzer.

$message = "";

 

// Verbindung zur Datenbank aufbauen.

// Die Variable $con verweist auf das erzeugte mysqli-Objekt.

$con = new mysqli($host, $user, $pass, $db);

 

// Prüfen, ob beim Verbindungsaufbau ein Fehler aufgetreten ist.

// die() gibt die Meldung aus und beendet das Programm.

if ($con->connect_error) {

  die("DB-Verbindung fehlgeschlagen: " . $con->connect_error);

}

 

// Zeichensatz für die Datenbankverbindung festlegen.

$con->set_charset("utf8mb4");

 

// Prüfen, ob das Formular mit der HTTP-Methode POST

// abgeschickt wurde.

if ($_SERVER["REQUEST_METHOD"] === "POST") {

 

// Rechnungsnummer aus dem Formular übernehmen.

// Der Operator ?? liefert 0, wenn der Formulareintrag fehlt.

// (int) wandelt den übernommenen Wert in eine ganze Zahl um.

$reNr = (int)($_POST["reNr"] ?? 0);

 

// Eingabe prüfen. Die Rechnungsnummer muss größer als 0 sein.

if ($reNr <= 0) {

  $message = "Bitte eine gültige Rechnungsnummer eingeben.";

  } else {

 

// try/catch-Block zum Abfangen möglicher Fehler.

try {

// Transaktion starten. $con ist das MySQLi-Verbindungsobjekt.

// Alle folgenden Löschoperationen werden dadurch zu

// einer gemeinsamen Einheit zusammengefasst.

$con->begin_transaction();

 

/*

Prüfen, ob ein Rechnungskopf mit der eingegebenen Rechnungsnummer

existiert.

$stmtCheck verweist auf ein Statement-Objekt der Klasse mysqli_stmt.

Die Methode prepare() erzeugt ein Prepared Statement. Das

Fragezeichen dient als Platzhalter für die Rechnungsnummer.

*/

$stmtCheck = $con->prepare("SELECT 1 FROM rechkoepfe

  WHERE ReNr = ?");

 

// Prüfen, ob das Prepared Statement erstellt wurde.

    if (!$stmtCheck) {throw new Exception(

    "Prepare fehlgeschlagen (Prüfung): " . $con->error);

}

 

// Rechnungsnummer an den Platzhalter binden.

// "i" bezeichnet den Datentyp Integer.

$stmtCheck->bind_param("i", $reNr);

 

// Prepared Statement ausführen.

if (!$stmtCheck->execute()) {

  throw new Exception("Execute fehlgeschlagen (Prüfung): "

    . $stmtCheck->error

  );

}

 

// Ergebnismenge zwischenspeichern, damit anschließend

// die Anzahl der gefundenen Zeilen ermittelt werden kann.

$stmtCheck->store_result();

 

// Prüfen, ob ein Rechnungskopf gefunden wurde.

if ($stmtCheck->num_rows === 0) {$stmtCheck->close();

 

// Es gibt nichts zu löschen. Die Transaktion wird

// ohne dauerhafte Änderungen beendet.

$con->rollback();

$message = "Keine Rechnung mit ReNr = $reNr gefunden.";

  } else {

    $stmtCheck->close();

 

/*

1. Die zur Rechnung gehörenden Positionen löschen.

Die Positionen müssen vor dem Rechnungskopf gelöscht werden, falls

der Fremdschlüssel nicht mit ON DELETE CASCADE definiert wurde.

*/

    $stmtPosDel = $con->prepare("DELETE FROM rechpos

      WHERE ReNr = ?");

    if (!$stmtPosDel) {throw new Exception("Prepare fehlgeschlagen "

      . "(Positionen löschen): " . $con->error);

}

$stmtPosDel->bind_param("i", $reNr);

if (!$stmtPosDel->execute()) {throw new Exception(

  "Execute fehlgeschlagen " . "(Positionen löschen): "

  . $stmtPosDel->error);

}

 

// Anzahl der gelöschten Rechnungspositionen ermitteln.

$deletedPos = $stmtPosDel->affected_rows;

$stmtPosDel->close();

 

// 2. Den Rechnungskopf löschen.

$stmtKopfDel = $con->prepare("DELETE FROM rechkoepfe

  WHERE ReNr = ?");

if (!$stmtKopfDel) {throw new Exception("Prepare fehlgeschlagen "

  . "(Rechnungskopf löschen): " . $con->error);

}

$stmtKopfDel->bind_param("i", $reNr);

if (!$stmtKopfDel->execute()) {throw new Exception(

  "Execute fehlgeschlagen " . "(Rechnungskopf löschen): "

    . $stmtKopfDel->error);

}

 

// Anzahl der gelöschten Rechnungsköpfe ermitteln. Bei einer

// eindeutigen Rechnungsnummer sollte der Wert 1 sein.

$deletedKopf = $stmtKopfDel->affected_rows;

$stmtKopfDel->close();

 

// Transaktion erfolgreich abschließen. Erst dadurch werden die

// Löschungen dauerhaft in der Datenbank gespeichert.

$con->commit();

$message = "Rechnung ReNr = $reNr gelöscht "

  . "(Kopf: $deletedKopf, " . "Positionen: $deletedPos).";

  }

} catch (Exception $e) {

 

// Im Fehlerfall alle seit Beginn der Transaktion vorgenommenen

// Änderungen rückgängig machen.

$con->rollback();

$message = "Fehler beim Löschen. " . "Alles wurde zurückgesetzt: "

    . $e->getMessage();

    }

  }

}

?>

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>KuReAr - Rechnung löschen</title>

    <style>body {font-family: Arial, sans-serif;}</style>

</head>

<body>

<h2>Rechnung löschen (MySQLi)</h2>

 

<!--

Formular zur Eingabe der Rechnungsnummer.

Da kein action-Attribut angegeben ist, werden die Daten

an die sendende PHP-Datei gesendet.

-->

<form method="post">

  <label for="reNr">Rechnungsnummer (ReNr):</label>

  <input type="number" id="reNr" name="reNr" min="1"

    required>

  <button type="submit">Löschen</button>

</form>

<!--

Wenn eine Rückmeldung vorliegt, wird sie in Fettschrift

ausgegeben. ZU htmlspecialchars() vgl. den Anhang.

-->

<?php if ($message !== ""): ?>

  <p><strong>

  <?php echo htmlspecialchars($message,ENT_QUOTES,"UTF-8"); ?>

  </strong>

</p>

<?php endif; ?>

</body>

</html>

<?php

// Datenbankverbindung schließen und die damit verbundenen

// Ressourcen freigeben.

$con->close();

?>

4.2.4 Erläuterung des Programms

Das Programm realisiert das Löschen einer Rechnung mit MySQLi und verwendet dabei eine Transaktion, um die Konsistenz der Datenbank sicherzustellen. Eine Rechnung besteht in der Datenbank KuReAr aus zwei zusammengehörigen Teilen: dem Rechnungskopf in der Relation rechkoepfe und den zugehörigen Rechnungspositionen in der Relation rechpos. Aufgrund dieser Abhängigkeit dürfen beide Teile nicht unabhängig voneinander gelöscht werden.

Zu Beginn werden die Verbindungsdaten zur Datenbank festgelegt. Anschließend wird mit new mysqli(...) eine Datenbankverbindung aufgebaut. Tritt dabei ein Fehler auf, wird das Programm mit die() sofort beendet und eine Fehlermeldung ausgegeben. Danach wird der Zeichensatz auf utf8mb4 gesetzt, um eine korrekte Verarbeitung von Sonderzeichen zu gewährleisten.

Wenn das Formular abgeschickt wird, liest das Programm die eingegebene Rechnungsnummer und prüft, ob es sich um einen gültigen Wert handelt. Nur positive Zahlen werden akzeptiert. Ist die Eingabe ungültig, wird eine entsprechende Meldung ausgegeben.

Bei gültiger Eingabe beginnt ein try/catch-Block. Zunächst wird mit begin_transaction() eine Transaktion gestartet. Alle folgenden Datenbankoperationen gehören damit zu einer gemeinsamen logischen Einheit, die entweder vollständig ausgeführt oder komplett zurückgenommen wird.

Im nächsten Schritt wird überprüft, ob der angegebene Rechnungskopf überhaupt existiert. Dazu wird ein Prepared Statement mit einem Platzhalter (?) verwendet. Mit store_result() kann anschließend festgestellt werden, ob eine passende Zeile gefunden wurde. Existiert keine Rechnung, wird die Transaktion mit rollback() beendet (ohne Änderungen) und eine entsprechende Meldung ausgegeben.

Existiert die Rechnung, erfolgt das Löschen in zwei Schritten. Zunächst werden alle zugehörigen Einträge in rechpos gelöscht. Dieser Schritt ist zwingend erforderlich, da aufgrund der Fremdschlüsselbeziehung ein Rechnungskopf nicht gelöscht werden darf, solange noch abhängige Positionen vorhanden sind. Die Anzahl der gelöschten Datensätze wird über die Eigenschaft affected_rows ermittelt.

Anschließend wird der Rechnungskopf selbst aus der Relation rechkoepfe gelöscht. Auch hier wird die Anzahl der betroffenen Zeilen gespeichert. Sind beide Löschoperationen erfolgreich, wird die Transaktion mit commit() abgeschlossen. Erst zu diesem Zeitpunkt werden die Änderungen dauerhaft in der Datenbank gespeichert.

Tritt hingegen während der Ausführung ein Fehler auf, wird eine Exception ausgelöst. Die Programmausführung springt dann in den catch-Block, in dem mit rollback() alle bisherigen Änderungen vollständig zurückgenommen werden. Dadurch wird sichergestellt, dass keine unvollständigen Löschvorgänge entstehen. Die Rechnung wird also entweder vollständig (inklusive aller Positionen) gelöscht oder überhaupt nicht. Auf diese Weise bleibt die Datenbank stets in einem konsistenten Zustand.

4.2.5 Anmerkung zur Programmierung

  • $message enthält keine Benutzereingaben, sondern Rückmeldungen an den Benutzer.
  • die() bedeutet hier nicht einfach "sterben", also "beenden", sondern: Meldung ausgeben und die Ausführung des Programms beenden.
  • Die bislang ungeprüfte Ausführung von $stmtCheck->execute() wird jetzt ebenfalls kontrolliert.

Die HTML-Anweisung <input type="number" id="reNr" name="reNr" min="1" required> erzeugt in einem Formular ein Eingabefeld für eine Zahl, hier für eine Rechnungsnummer. Die einzelnen Bestandteile bedeuten:

  • <input ...> Erzeugt ein Eingabefeld in einem HTML-Formular.
  • type="number" legt fest, dass in das Feld eine Zahl eingegeben werden soll. Der Browser stellt dafür häufig zusätzlich kleine Pfeile zum Erhöhen und Verringern des Wertes bereit.
  • id="reNr" gibt dem Eingabefeld die eindeutige Kennung reNr. Diese Kennung wird beispielsweise benötigt, um das Feld mit einem <label> zu verbinden oder über CSS bzw. JavaScript anzusprechen.
  • name="reNr" legt den Namen fest, unter dem der eingegebene Wert beim Absenden des Formulars an den Server übertragen wird. Bei einem Formular mit method="post" kann PHP beispielsweise mit $_POST["reNr"] auf den eingegebenen Wert zugreifen.
  • min="1" legt den kleinsten zulässigen Wert fest. Es dürfen also nur Zahlen ab 1 eingegeben werden.
  • required kennzeichnet das Feld als Pflichtfeld. Der Browser lässt das Formular normalerweise nicht absenden, solange keine gültige Eingabe vorhanden ist.

Insgesamt also

Die HTML-Anweisung erzeugt ein Eingabefeld für eine Zahl. Das Feld erhält die Kennung und den Namen reNr. Der eingegebene Wert muss mindestens 1 betragen. Da das Attribut required angegeben ist, handelt es sich um ein Pflichtfeld. Beim Absenden des Formulars wird der eingegebene Wert unter dem Namen reNr an den Server übertragen.

Wichtig dabei ist dabei der Unterschied zwischen id und name: id identifiziert das Element innerhalb der Webseite. name bestimmt dagegen insbesondere, unter welchem Namen der eingegebene Wert beim Absenden übertragen wird.

4.3 KundenLoeschenPDO.php

Ein Kundeneintrag wird gelöscht.

4.3.1 Merkmale des Programms

  • Verwendete Klasse für den Verbindungsaufbau: PDO
  • Start durch Browser-Befehl
  • HTML-Ausgabe des Ergebnisses

4.3.2 Das Programm

<?php

 

// Variable für Erfolgs- und Fehlermeldungen.

$message = "";

 

// Verbindungsdaten für PDO.

// DSN bedeutet "Data Source Name". Der DSN enthält hier den Host,

// die Bezeichnung der Datenbank und den verwendeten Zeichensatz.

$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";

$user = "root";

$pass = "";

 

// Prüfen, ob das Formular mit der HTTP-Methode POST abgeschickt wurde.

if ($_SERVER["REQUEST_METHOD"] === "POST") {

 

// Eingabewert aus dem Formular übernehmen.

// Der Operator ?? liefert 0, wenn der Formulareintrag nicht vorhanden ist.

// (int) wandelt den übernommenen Wert in eine ganze Zahl um.

$kuNr = (int)($_POST["kuNr"] ?? 0);

// Eingabe prüfen. Die Kundennummer muss größer als 0 sein.

if ($kuNr <= 0) {

  $message = "Bitte eine gültige Kundennummer eingeben.";

  } else {

 

// try/catch-Block zum Abfangen möglicher Datenbankfehler.

try {

 

// Datenbankverbindung mit PDO aufbauen. Der Fehlermodus bewirkt,

// dass PDO bei einem Fehler // eine Exception auslöst.

$pdo = new PDO($dsn,$user,$pass,

  [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

 

// SQL-Anweisung als Prepared Statement vorbereiten.

// :kuNr ist ein benannter Platzhalter.

$stmt = $pdo->prepare("DELETE FROM kunden WHERE KuNr = :kuNr");

 

// Den Wert der PHP-Variablen $kuNr an den Platzhalter

// binden und die vorbereitete SQL-Anweisung ausführen.

$stmt->execute(["kuNr" => $kuNr]);

 

// Prüfen, ob tatsächlich ein Datensatz gelöscht wurde.

// rowCount() liefert hier die Anzahl der gelöschten Datensätze.

if ($stmt->rowCount() > 0) {$message = "Datensatz wurde gelöscht.";

  } else {

 

// Wenn kein Datensatz gelöscht wurde, existierte kein Kunde

// mit der eingegebenen Kundennummer.

  $message = "Kein Datensatz mit dieser Kundennummer gefunden.";

}

} catch (PDOException $e) {

 

// Fehlerbehandlung: Tritt beim Verbindungsaufbau oder bei der

// Ausführung der SQL-Anweisung ein Fehler auf, wird hier eine

// Fehlermeldung erzeugt.

    $message = "Fehler: " . $e->getMessage();

    }

  }

}

?>

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Kunde löschen mit PDO</title>

  <style>body {font-family: Arial, sans-serif;}</style>

</head>

<body>

<h2>Kunde löschen (PDO)</h2>

 

<!--

Formular zur Eingabe der Kundennummer.

method="post" legt fest, dass die Formulardaten mit der

HTTP-Methode POST übertragen werden. Da kein action-Attribut

angegeben ist, werden sie an sendende PHP-Datei gesendet.

-->

<form method="post">

  <label for="kuNr">Kundennummer:</label>

  <input type="number" id="kuNr" name="kuNr" min="1" required>

   

  <!-- Schaltfläche zum Absenden des Formulars. -->

  <button type="submit">Löschen</button>

</form>

<!--

Wenn eine Meldung vorliegt, wird sie in Fettschrift ausgegeben.

htmlspecialchars() verhindert, dass enthaltene Sonderzeichen als

Bestandteile des HTML-Codes interpretiert werden. Vgl. auch den

Anhang.

-->

<?php if ($message !== ""): ?>

<p><strong>

  <?php echo htmlspecialchars($message,ENT_QUOTES,"UTF-8"); ?>

  </strong>

</p>

<?php endif; ?>

</body>

</html>

4.3.3 Programmlauf

Auch hier gilt: Zuerst einen isolierten Kunden (d.h. einen ohne Rechnung) eingeben. Dieser kann dann problemlos gelöscht werden.

Die Konsoleneingabe

4.3.4 Erläuterung des Programms

Das Programm zeigt, wie ein Kundendatensatz aus der Datenbank KuReAr mithilfe der PDO-Erweiterung (PHP Data Objects) gelöscht werden kann. Zu Beginn wird eine Variable $message definiert. Sie dient dazu, Statusmeldungen zu speichern, die den Benutzer darüber informieren, ob die Operation erfolgreich war oder ob ein Fehler aufgetreten ist. Anschließend werden die Verbindungsparameter in Form eines DSN (Data Source Name) sowie eines Benutzernamens und Passworts festgelegt. Der DSN enthält alle notwendigen Informationen zum Aufbau der Datenbankverbindung.

PHP Data Objects

Nach dem Absenden des Formulars prüft das Programm, ob die Anfrage mit der Methode POST erfolgt ist. Dadurch wird sichergestellt, dass die Daten aus dem Formular stammen und nicht durch einen direkten Aufruf der URL übergeben wurden. Die eingegebene Kundennummer wird anschließend ausgelesen und in eine Ganzzahl umgewandelt. Falls kein Wert übergeben wird, wird standardmäßig der Wert 0 verwendet.

Im nächsten Schritt erfolgt eine Validierung der Eingabe. Es werden nur positive Werte als gültige Kundennummern akzeptiert. Ist die Eingabe ungültig, wird eine entsprechende Fehlermeldung gespeichert und keine Datenbankoperation durchgeführt.

Ist die Eingabe gültig, wird ein try/catch-Block ausgeführt. Innerhalb des try-Abschnitts wird ein PDO-Objekt erzeugt, um die Verbindung zur Datenbank herzustellen. Die Option

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION

bewirkt, dass auftretende Datenbankfehler als Exceptions gemeldet werden, die zentral behandelt werden können.

Das Löschen des Datensatzes erfolgt über ein Prepared Statement. Die SQL-Anweisung enthält einen benannten Parameter (:customer_id), der als Platzhalter dient. Dieser Platzhalter wird erst bei der Ausführung der Anweisung durch den tatsächlichen Wert ersetzt. Dieses Vorgehen erhöht sowohl die Sicherheit (insbesondere Schutz vor SQL-Injection) als auch die Übersichtlichkeit des Codes.

Nach der Ausführung wird mit der Methode rowCount() ermittelt, wie viele Datensätze betroffen sind. Wurde mindestens ein Datensatz gelöscht, wird eine Erfolgsmeldung ausgegeben. Andernfalls informiert das Programm darüber, dass kein entsprechender Datensatz gefunden wurde.

Tritt während der Ausführung ein Fehler auf, wird eine PDOException ausgelöst und im catch-Block abgefangen. Die entsprechende Fehlermeldung wird anschließend in der Variablen $message gespeichert.

Im abschließenden HTML-Teil wird ein einfaches Formular zur Eingabe der Kundennummer bereitgestellt. Ist die Variable $message nicht leer, wird ihr Inhalt ausgegeben. Die Funktion htmlspecialchars() sorgt dafür, dass Sonderzeichen korrekt kodiert werden und verhindert so mögliche Sicherheitsprobleme wie HTML-Injection. Vgl. auch den Anhang.

4.4 RechnungLoeschenPDO.php

Nun das Löschen einer ganzen Rechnung aus der Datenbank KuReAr.

4.4.1 Das Programm

<?php

 

// Variable für Rückmeldungen an den Benutzer.

$message = "";

 

// Verbindungsdaten für PDO. DSN bedeutet "Data Source Name".

// Der DSN enthält den Host, die Bezeichnung der Datenbank

// und den verwendeten Zeichensatz.

$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";

$user = "root";

$pass = "";

 

// Die Variable wird zunächst mit null beschrieben.

// Später verweist sie auf das erzeugte PDO-Objekt.

$pdo = null;

try {

// Verbindung zur Datenbank aufbauen.

// PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION bewirkt,

// dass PDO bei einem Datenbankfehler eine Exception auslöst.

    $pdo = new PDO($dsn,$user,$pass,

    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

 

// Prüfen, ob das Formular mit der HTTP-Methode POST

// abgeschickt wurde.

if ($_SERVER["REQUEST_METHOD"] === "POST") {

 

// Rechnungsnummer aus dem Formular übernehmen.

// Der Operator ?? liefert 0, wenn der Formulareintrag fehlt.

// (int) wandelt den übernommenen Wert in eine ganze Zahl um.

$reNr = (int)($_POST["reNr"] ?? 0);

 

// Eingabe prüfen. Die Rechnungsnummer muss größer als 0 sein.

if ($reNr <= 0) {

    $message = "Bitte eine gültige Rechnungsnummer eingeben.";

} else {

try {

 

// Transaktion starten. Alle folgenden Löschoperationen gehören

// damit zu // einer gemeinsamen Einheit. Sie werden entweder

// vollständig durchgeführt oder vollständig rückgängig gemacht.

$pdo->beginTransaction();

 

// Prüfen, ob ein Rechnungskopf mit der eingegebenen

// Rechnungsnummer existiert.

// :reNr ist ein Platzhalter für die Rechnungsnummer.

$stmtCheck = $pdo->prepare(

  "SELECT 1 FROM rechkoepfe WHERE ReNr = :reNr");

 

// Prepared Statement ausführen.

// Der Wert der PHP-Variablen $reNr wird dem

// Platzhalter :reNr zugeordnet.

$stmtCheck->execute(["reNr" => $reNr]);

 

// fetchColumn() liefert den Inhalt der ersten Spalte der gefundenen

// Zeile. Wird keine Zeile gefunden, liefert die Methode false.

$rechnungVorhanden = $stmtCheck->fetchColumn();

 

// Prüfen, ob ein Rechnungskopf gefunden wurde.

if ($rechnungVorhanden === false) {

 

// Es gibt nichts zu löschen. Die Transaktion wird

// ohne dauerhafte Änderungen beendet.

$pdo->rollBack();

$message = "Keine Rechnung mit ReNr = $reNr gefunden.";

  } else {

 

/*

1. Die zur Rechnung gehörenden Positionen löschen.

Die Positionen müssen vor dem Rechnungskopf gelöscht werden,

sofern der Fremdschlüssel nicht mit ON DELETE CASCADE

definiert wurde.

*/

$stmtPosDel = $pdo->prepare("DELETE FROM rechpos

  WHERE ReNr = :reNr");

$stmtPosDel->execute(["reNr" => $reNr]);

 

// rowCount() liefert bei dieser DELETE-Anweisung

// die Anzahl der gelöschten Rechnungspositionen.

$deletedPos = $stmtPosDel->rowCount();

 

// 2. Den Rechnungskopf löschen.

$stmtKopfDel = $pdo->prepare("DELETE FROM rechkoepfe

    WHERE ReNr = :reNr");

$stmtKopfDel->execute(["reNr" => $reNr]);

 

// Anzahl der gelöschten Rechnungsköpfe ermitteln.

// Bei einer eindeutigen Rechnungsnummer sollte

// der Wert 1 sein.

$deletedKopf = $stmtKopfDel->rowCount();

 

// Zusätzliche Sicherheitsprüfung:

// Wurde kein Rechnungskopf gelöscht, wird eine

// Exception ausgelöst.

if ($deletedKopf !== 1) {throw new Exception(

    "Der Rechnungskopf konnte nicht gelöscht werden.");

}

 

// Transaktion erfolgreich abschließen. // Erst dadurch werden

// die Löschungen dauerhaft in der Datenbank gespeichert.

$pdo->commit();

$message = "Rechnung ReNr = $reNr gelöscht "

    . "(Kopf: $deletedKopf, " . "Positionen: $deletedPos).";

  }

} catch (Exception $e) {

 

// Prüfen, ob noch eine Transaktion aktiv ist.

// Nur dann darf rollBack() aufgerufen werden.

if ($pdo->inTransaction()) {

 

// Alle seit Beginn der Transaktion vorgenommenen

// Änderungen rückgängig machen.

  $pdo->rollBack();

}

$message = "Fehler beim Löschen. " . "Alles wurde zurückgesetzt: "

      . $e->getMessage();

    }

  }

}

} catch (PDOException $e) {

 

// Fehler beim Aufbau der Datenbankverbindung.

$message = "DB-Verbindung fehlgeschlagen: " . $e->getMessage();

}

?>

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>KuReAr - Rechnung löschen</title>

  <style>body {font-family: Arial, sans-serif;}</style>

</head>

<body>

<h2>Rechnung löschen (PDO)</h2>

 

<!--

Formular zur Eingabe der Rechnungsnummer.

method="post" legt fest, dass die Formulardaten mit der

HTTP-Methode POST übertragen werden. Da kein action-Attribut

angegeben ist, werden sie an dieselbe PHP-Datei gesendet.

-->

<form method="post">

  <label for="reNr">Rechnungsnummer (ReNr):</label>

  <input type="number" id="reNr" name="reNr" min="1" required>

  <button type="submit">Löschen</button>

</form>

 

<!--

Wenn eine Rückmeldung vorliegt, wird sie in Fettschrift

ausgegeben. htmlspecialchars() verhindert, dass enthaltene

Sonderzeichen als HTML-Code interpretiert werden. Vgl. Anhang.

-->

<?php if ($message !== ""): ?>

  <p>

    <strong>

    <?php echo htmlspecialchars($message,ENT_QUOTES,"UTF-8"); ?>

    </strong>

  </p>

<?php endif; ?>

</body>

</html>

4.4.2 Programmlauf

So sieht das Ergebnis nach einem erfolgreichen Programmlauf aus.

4.4.3 Erläuterung des Programms

Das Programm dient dazu, eine Rechnung mit Rechnungskopf und Rechnungspositionen aus der Datenbank zu löschen. Es verwendet dazu PDO und eine Transaktion. Der Einsatz einer Transaktion ist hier unabdiingbar, da eine Rechnung aus zwei miteinander verknüpften Teilen besteht: dem Rechnungskopf in der Relation Rechkoepfe und den zugehörigen Rechnungspositionen in der Relation Rechpos. Beide Teile müssen konsistent behandelt werden, sodass entweder alle zugehörigen Löschungen gelöscht werden oder keine Änderung dauerhaft gespeichert wird.

Zu Beginn werden die Verbindungsdaten für PDO festgelegt. Der DSN (Data Source Name) enthält den Host, die Bezeichnung der Datenbank und den verwendeten Zeichensatz utf8mb4. Anschließend wird mit new PDO(...) eine Verbindung zur Datenbank aufgebaut. Die Option PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION bewirkt, dass PDO bei Datenbankfehlern Exceptions auslöst. Tritt bereits beim Verbindungsaufbau ein Fehler auf, wird dieser in einem catch-Block für PDOException abgefangen und eine entsprechende Fehlermeldung erzeugt.

Nach dem Absenden des Formulars wird die eingegebene Rechnungsnummer eingelesen und überprüft. Nur positive Werte werden als gültig akzeptiert. Ist die Eingabe ungültig, wird eine entsprechende Fehlermeldung ausgegeben.

Bei gültiger Eingabe wird ein weiterer try/catch-Block durchlaufen. Zunächst wird mit beginTransaction() eine Transaktion gestartet. Ab diesem Zeitpunkt gehören die folgenden Datenbankoperationen zu einer gemeinsamen Arbeitseinheit.

Als Nächstes prüft das Programm, ob die angegebene Rechnung existiert. Dazu wird mit prepare() ein Prepared Statement mit dem benannten Platzhalter :reNr vorbereitet. Beim Aufruf von execute() wird diesem Platzhalter die eingegebene Rechnungsnummer zugeordnet. Die Methode fetchColumn() liefert den Wert der ersten Spalte der gefundenen Zeile. Wird keine passende Zeile gefunden, liefert sie false. In diesem Fall wird die Transaktion mit rollBack() beendet und eine entsprechende Meldung ausgegeben. Da bis dahin noch keine Datensätze gelöscht wurden, müssen allerdings keine Änderungen rückgängig gemacht werden.

Existiert die Rechnung, erfolgt das Löschen in zwei Schritten. Zuerst werden alle zugehörigen Datensätze aus Rechpos gelöscht. Dieser Schritt ist erforderlich, da die Rechnungspositionen aufgrund der Fremdschlüsselbeziehung vom Rechnungskopf abhängen. Solange sie noch existieren, kann der Rechnungskopf nicht gelöscht werden, sofern für den Fremdschlüssel nicht ON DELETE CASCADE festgelegt wurde. Die Anzahl der gelöschten Rechnungspositionen wird mit rowCount() ermittelt.

Anschließend wird der Rechnungskopf aus Rechkoepfe gelöscht. Auch hier wird die Anzahl der gelöschten Datensätze mit rowCount() ermittelt. Da die Rechnungsnummer den Rechnungskopf eindeutig kennzeichnet, sollte genau ein Datensatz gelöscht worden sein. Ist dies nicht der Fall, löst das Programm eine Exception aus.

Wurden beide Löschoperationen erfolgreich durchgeführt, wird die Transaktion mit commit() abgeschlossen. Erst dadurch werden die Änderungen dauerhaft in der Datenbank gespeichert. Anschließend wird eine Meldung ausgegeben, die auch die Anzahl der gelöschten Rechnungsköpfe und Rechnungspositionen enthält.

Tritt während der Transaktion ein Fehler auf, wird dieser im catch-Block abgefangen. Zunächst prüft inTransaction(), ob noch eine aktive Transaktion besteht. Ist dies der Fall, werden mit rollBack() alle seit Beginn der Transaktion vorgenommenen Änderungen rückgängig gemacht. Dadurch wird verhindert, dass beispielsweise die Rechnungspositionen gelöscht werden, der zugehörige Rechnungskopf aber erhalten bleibt. Die Rechnung wird somit entweder vollständig gelöscht oder gar nicht. Auf diese Weise bleibt die Datenbank in einem konsistenten Zustand.

In früheren Zeiten sprach man von einer Stammdatenkrise , wenn inkonsistente Datenbestände für Verwirrung in den Datenbeständen und den auswertenden Programmen sorgten. Dies ist, auch wenn der Begriff aus der Mode kam, sicherlich auch heute noch der Fall.

Stamm-datenkrise

Ein ausdrückliches Schließen der Datenbankverbindung ist bei PDO normalerweise nicht erforderlich. Die Verbindung wird spätestens am Ende der Skriptausführung freigegeben, wenn das PDO-Objekt nicht mehr benötigt wird.

5 Ändern von Daten

5.1 KundenAendernMySQLi.php

5.1.1 Merkmale des Programms

  • Start durch HTML-Formulareingabe mit Posting
  • Verwendete Klasse für den Verbindungsaufbau: MySQLi
  • Absicherung durch Prepared Statement
  • HTML-Ausgabe des Ergebnisses

5.1.2 Aufgabe

Es sollen Daten aus der Datenbank geändert werden. So soll die Eingabemaske aussehen:

5.1.3 Das Programm

<?php

 

// Variable für die Rückmeldung an den Benutzer.

$message = "";

 

// Daten für den späteren Verbindungsaufbau.

$host = "localhost";

$user = "root";

$pass = "";

$db = "KuReAr";

 

/*

Zur besseren Übersicht sind die geschweiften Klammern nummeriert.

Zusammengehörende öffnende und schließende Klammern tragen

dieselbe Nummer.

Dies macht deutlich, dass die folgende öffnende geschweifte

Klammer ihr Gegenstück erst am Progammende hat.

Prüfen, ob das Formular mit der HTTP-Methode POST abgeschickt wurde.

*/

if ($_SERVER["REQUEST_METHOD"] === "POST") { // 1

 

/*

Werte aus den Formularfeldern übernehmen.

$_POST ist ein assoziatives Array. Es enthält die mit POST

übertragenen Formulardaten.

"kuNr", "name" und "vorname" sind die Bezeichnungen der

Formularfelder.

Der Operator ?? legt einen Ersatzwert fest, falls ein

Formulareintrag nicht vorhanden ist.

(int) wandelt den übernommenen Wert in eine ganze Zahl um.

trim() entfernt Leerzeichen am Anfang und Ende einer Zeichenkette.

*/

$kuNr = (int)($_POST["kuNr"] ?? 0);

$name = trim($_POST["name"] ?? "");

$vorname = trim($_POST["vorname"] ?? "");

 

// Ungültige oder leere Eingaben abfangen.

if ($kuNr <= 0 || $name === "" || $vorname === "") { // 2

  $message = "Bitte alle Felder korrekt ausfüllen.";

  } // 2

else { // 3

 

// Verbindung zur Datenbank aufbauen.

// Die Variable $con verweist auf das erzeugte mysqli-Objekt.

$con = new mysqli($host, $user, $pass, $db);

 

// Prüfen, ob beim Verbindungsaufbau ein Fehler aufgetreten ist.

if ($con->connect_error) { // 4

    die("DB-Verbindung fehlgeschlagen: " . $con->connect_error);

    } // 4

 

// Zeichenkodierung für die Datenbankverbindung festlegen.

$con->set_charset("utf8mb4");

 

// SQL-Anweisung als Prepared Statement vorbereiten.

// Die Fragezeichen dienen als Platzhalter für die zu bindenden Werte.

$stmt = $con->prepare("UPDATE kunden SET Name = ?, Vorname = ?

  WHERE KuNr = ?");

// Prüfen, ob das Prepared Statement erstellt werden konnte.

if (!$stmt) { // 5

    $message = "Prepare fehlgeschlagen: " . $con->error;

} // 5

else { // 6

 

/*

Werte an die Platzhalter binden.

"ssi" bezeichnet die Datentypen:

s = String für die Bezeichnung

s = String für den Vornamen

i = Integer für die Kundennummer

*/

$stmt->bind_param("ssi", $name, $vorname, $kuNr);

 

// Vorbereitete SQL-Anweisung ausführen.

if ($stmt->execute()) { // 7

 

// Prüfen, ob ein Datensatz tatsächlich geändert wurde.

  if ($stmt->affected_rows > 0) { // 8

    $message = "Datensatz wurde geändert.";

  } // 8

  else {

    $message = "Keine Änderung vorgenommen.";

    }

} // 7

  else {

    $message = "Fehler: " . $stmt->error;

}

 

// Prepared Statement schließen und die dafür verwendeten

// Ressourcen freigeben.

$stmt->close();

} // 6

 

// Datenbankverbindung schließen und die damit verbundenen

// Ressourcen freigeben.

$con->close();

  } // 3

} // 1

?>

<!doctype html>

<html lang="de">

<head>

    <meta charset="utf-8">

    <title>Kunde ändern mit MySQLi</title>

    <style>body {font-family: Arial, sans-serif;}

    label {font-weight: bold;}</style>

</head>

<body>

 

<!-- Eingabemaske zum Ändern eines Kunden. -->

<h2>Kunde ändern (MySQLi)</h2>

 

<!--

Formular für die Übertragung der eingegebenen Daten an den Server.

Da kein action-Attribut angegeben ist, wird das Formular an die

sendende PHP-Datei weitergeleitet. "required" kennzeichnet die

Pflichtfelder.

-->

<form method="post">

  <label for="kuNr">Kundennummer:</label>

  <input type="number" id="kuNr" name="kuNr" required>

  <br><br>

  <label for="name">Name:</label>

  <input type="text" id="name" name="name" maxlength="10" required>

  <br><br>

  <label for="vorname">Vorname:</label>

  <input type="text" id="vorname" name="vorname" maxlength="10"

  required>

  <br><br>

     

<!-- Schaltfläche zum Absenden des Formulars. -->

  <button type="submit">Ändern</button>

</form>

<!--

PHP kann auch innerhalb des HTML-Abschnitts verwendet werden. Vgl.

auch Kapitel 9.

Wenn eine Rückmeldung vorliegt, wird sie in Fettschrift ausgegeben.

-->

<?php

  if ($message !== "") {echo "<p><strong>"

    . htmlspecialchars($message, ENT_QUOTES, "UTF-8") .

    "</strong></p>";

}

?>

</body>

</html>

5.1.4 Erläuterung des Programms

Das Programm dient dazu, einen vorhandenen Kundendatensatz in der Datenbank zu ändern. Es verwendet dazu MySQLi und folgt dem typischen Ablauf einer Webanwendung:

  • Eingabe verarbeiten,
  • Daten prüfen,
  • Datenbankoperation durchführen und
  • Rückmeldung geben.

Zu Beginn werden die Verbindungsdaten zur Datenbank festgelegt sowie die Variable $message initialisiert. Diese Variable dient dazu, dem Benutzer eine Rückmeldung über den Erfolg oder Misserfolg der Operation zu geben.

Nach dem Absenden des Formulars wird geprüft, ob die Anfrage per POST erfolgt ist. Anschließend werden die eingegebenen Werte aus dem Formular über das Array $_POST ausgelesen. Dabei wird die Kundennummer in einen Integerwert umgewandelt, während die Felder für Name und Vorname mit der Funktion trim() bereinigt werden, sodass führende und nachfolgende Leerzeichen entfernt werden.

Im nächsten Schritt erfolgt eine Eingabevalidierung. Es wird überprüft, ob alle Felder sinnvoll gefüllt sind. Ist dies nicht der Fall, wird eine entsprechende Fehlermeldung ausgegeben und die Verarbeitung abgebrochen.

Sind die Eingaben gültig, wird eine Verbindung zur Datenbank aufgebaut. Tritt dabei ein Fehler auf, wird das Programm mit die() beendet. Anschließend wird der Zeichensatz auf utf8mb4 gesetzt, um eine korrekte Verarbeitung von Sonderzeichen sicherzustellen.

Die eigentliche Änderung erfolgt über ein Prepared Statement. In der SQL-Anweisung werden Platzhalter (?) verwendet, die erst später mit konkreten Werten belegt werden. Dies erhöht die Sicherheit (Schutz vor SQL-Injection) und verbessert die Struktur des Programms.

Prepared Statement

Mit der Methode bind_param() werden die eingegebenen Werte an die Platzhalter gebunden. Die Angabe "ssi" beschreibt dabei die Datentypen der Parameter: zwei Zeichenketten (string) und eine Ganzzahl (integer).

Nach der Ausführung des Statements mit execute() wird überprüft, ob tatsächlich Datensätze geändert wurden. Dies geschieht über die Eigenschaft affected_rows. Ist der Wert größer als 0, wurde mindestens ein Datensatz erfolgreich aktualisiert. Andernfalls wurde zwar die Anweisung ausgeführt, aber es ergab sich keine Änderung (z. B. weil die neuen Werte identisch mit den alten sind).

Nach Abschluss der Datenbankoperation wird das Prepared Statement mit close() geschlossen und anschließend auch die Datenbankverbindung beendet. Dadurch werden belegte Ressourcen wieder freigegeben.

Im HTML-Teil wird ein Formular bereitgestellt, über das der Benutzer die zu ändernden Daten eingeben kann. Die Rückmeldung aus der Variablen $message wird nur dann angezeigt, wenn sie nicht leer ist. Dabei sorgt die Funktion htmlspecialchars() dafür, dass Sonderzeichen korrekt dargestellt werden und keine Sicherheitsprobleme durch HTML-Einschleusung entstehen. Vgl. Anhang.

Das Programm verdeutlicht damit den vollständigen Ablauf einer typischen Änderungsoperation (UPDATE) in einer Webanwendung: von der Dateneingabe über die Validierung bis hin zur sicheren und kontrollierten Ausführung der Datenbankanweisung.

5.2 KundenAendernPDO.php

5.2.1 Merkmale des Programms

  • Verwendete Klasse für Verbindungsaufbau: PDO
  • Absicherung durch Prepared Statements
  • Start durch Formulareingabe
  • HTML-Ausgabe des Ergebnisses

5.2.2 Aufgabe

Es sollen Daten aus der Datenbank geändert werden. So soll die Maske zu Beginn aussehen. Falls die Kundennummer in der Datenbank nicht vorhanden ist, erfolgt eine Meldung. Ebenso, falls nicht alle Felder ausgeführt sind.

5.2.3 Das Programm

<?php

 

// Variable für die Rückmeldung an den Benutzer.

$message = "";

 

// Daten für den Verbindungsaufbau.

// DSN bedeutet "Data Source Name".

$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";

$user = "root";

$pass = "";

 

// Prüfen, ob das Formular mit der HTTP-Methode POST abgeschickt wurde.

if ($_SERVER["REQUEST_METHOD"] === "POST") {

 

// Werte aus den Formularfeldern übernehmen.

// Der Operator ?? legt einen Ersatzwert fest, falls ein Feld nicht vorhanden ist.

$kuNr = (int)($_POST["kuNr"] ?? 0);

$name = trim($_POST["name"] ?? "");

$vorname = trim($_POST["vorname"] ?? "");

 

// Ungültige oder leere Eingaben abfangen.

if ($kuNr <= 0 || $name === "" || $vorname === "") {

  $message = "Bitte alle Felder korrekt ausfüllen.";

} else {

 

// try/catch-Block zum Abfangen möglicher Datenbankfehler.

try {$pdo = new PDO($dsn, $user, $pass,

  [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

 

// SQL-Anweisung vorbereiten.

// Benannte Parameter wie :name dienen als Platzhalter.

$stmt = $pdo->prepare( "UPDATE kunden SET Name = :name,

  Vorname = :vorname WHERE KuNr = :kuNr");

 

// Werte an die benannten Parameter binden und

// die vorbereitete SQL-Anweisung ausführen.

$stmt->execute(["name" => $name,"vorname" => $vorname,

  "kuNr" => $kuNr]);

 

// Prüfen, ob durch die SQL-Anweisung ein Datensatz geändert wurde.

if ($stmt->rowCount() > 0) {

  $message = "Datensatz wurde geändert.";

} else {

  $message = "Keine Änderung vorgenommen.";

}

} catch (PDOException $e) {

 

// Fehlermeldung für die Entwicklungs- und Testumgebung.

    $message = "Fehler: " . $e->getMessage();

    }

  }

}

?>

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Kunde ändern mit PDO</title>

  <style>body {font-family: Arial, sans-serif;}</style>

</head>

<body>

 

<!--

Eingabemaske zum Ändern eines Kunden.

Das Attribut required kennzeichnet ein Pflichtfeld.

-->

<h2>Kunde ändern (PDO)</h2>

<form method="post">

    <label for="kuNr">Kundennummer:</label>

    <input type="number" id="kuNr" name="kuNr" required>

    <br><br>

    <label for="name">Name:</label>

    <input type="text" id="name" name="name" maxlength="10"

      required>

    <br><br>

    <label for="vorname">Vorname:</label>

    <input type="text" id="vorname" name="vorname" maxlength="10"

      required>

    <br><br>

 

<!-- Schaltfläche zum Absenden des Formulars. -->

    <button type="submit">Ändern</button>

</form>

 

<!--

Wenn eine Rückmeldung vorliegt, wird sie in Fettschrift ausgegeben.

Zu htmlspecialchars() vgl. den Anhang.

-->

<?php if ($message !== ""): ?>

<p><strong>

  <?php echo htmlspecialchars($message,ENT_QUOTES,"UTF-8"); ?>

</strong></p><?php endif; ?>

</body>

</html>

5.2.4 Anmerkungen

  • ?? ist kein Platzhalter, sondern der Null-Koaleszenzoperator. Er liefert hier einen Ersatzwert, wenn ein Formulareintrag nicht vorhanden ist.
  • $message enthält keine Benutzereingaben, sondern die Rückmeldung über das Ergebnis.
  • rowCount() === 0 bedeutet nicht zwingend, dass kein passender Datensatz existiert. Es kann auch bedeuten, dass die eingegebenen Werte bereits gespeichert waren und deshalb nichts geändert werden musste.

6 Übersicht CRUD-Operationen

Obige Beispiele zeigen, wie die elementaren datenbanktechnischen Aktionen Eintragen (C: create), Lesen (R: read), Aktualisieren (U: update) und Löschen (D: delete) gelöst werden, wenn die Befehlseingabe von einer Webseite kommt. Zusammen werden sie CRUD-Operationen genannt.

Hier nun eine Zusammenfassung am Beispiel der Relation Kunden aus der oben bearbeiteten Datenbank KuReAr (Kunden, Rechnungen, Artikel). Gezeigt werden die Lösungen für mysqli und für PDO.

Die Programmfragmente sind alle PHP-Programme, müssen also zwischen <?php und ?> gestellt werden.

6.1 CRUD-Übersicht mit PDO

Herstellen der Verbindung

$pdo = new PDO(

  "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4","root","",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

CREATE (Datensatz einfügen)

$stmt = $pdo->prepare(

"INSERT INTO kunden (KuNr, name, vorname) VALUES (:kuNr, :name, :vorname)");

$stmt->execute(['kuNr' => 2001,'name' => 'Meier','vorname' => 'Anna']);

READ (Datensatz lesen)

$stmt = $pdo->prepare("SELECT * FROM kunden WHERE KuNr = :kuNr");

$stmt->execute(['kuNr' => 2001]);

$row = $stmt->fetch(PDO::FETCH_ASSOC);

print_r($row);

UPDATE (Datensatz ändern)

$stmt = $pdo->prepare("UPDATE kunden SET name = :name WHERE KuNr = :kuNr");

$stmt->execute(['name' => 'Meyer','kuNr' => 2001]);

DELETE (Datensatz löschen)

$stmt = $pdo->prepare("DELETE FROM kunden WHERE KuNr = :kuNr");

$stmt->execute(['kuNr' => 2001]);

6.2 CRUD-Übersicht mit MySQLi

Herstellen der Verbindung

$con = new mysqli("localhost", "root", "", "KuReAr");

  if ($con->connect_error) {

    die("Verbindung fehlgeschlagen: " . $con->connect_error);

  }

$con->set_charset("utf8mb4");

CREATE

$stmt = $con->prepare("INSERT INTO kunden (KuNr, name, vorname)

  VALUES (?, ?, ?)");

$stmt->bind_param("iss", $kuNr, $name, $vorname);

$kuNr = 2001;

$name = "Meier";

$vorname = "Anna";

$stmt->execute();

$stmt->close();

READ

$stmt = $con->prepare("SELECT * FROM kunden WHERE KuNr = ?");

$stmt->bind_param("i", $kuNr);

$stmt->execute();

$result = $stmt->get_result();

$row = $result->fetch_assoc();

print_r($row);

$stmt->close();

UPDATE

$stmt = $con->prepare("UPDATE kunden SET name = ? WHERE KuNr = ?");

$name = "Meyer";

$stmt->bind_param("si", $name, $kuNr);

$stmt->execute();

$stmt->close();

DELETE

$stmt = $con->prepare("DELETE FROM kunden WHERE KuNr = ?");

$stmt->bind_param("i", $kuNr);

$stmt->execute();

$stmt->close();

$con->close();

7 KuReAr - Rechnungsverwaltung

Hier nun ein etwas umfangreicheres Beispiel zu den Grundzügen einer Rechnungsverwaltung. Es sollen ganze Rechnungen eingegeben werden. Die Programme hier sind mit der Klasse PDO realisiert.

"KuReAr" meint "Kunden - Rechnungen -Artikel".

7.1 Maskensteuerung

Auf der obersten Ebene (leitseite) wird eine einfache Web-Oberfläche für die Datenbank KuReAr erstellt. Sie wird mit http://localhost/kurearstart.php aufgerufen:

Im ersten Schritt wird die Eingabe von Rechnungen realisiert, die übrigen Punkte kommen später. Das Programm für die Leitseite hat die Bezeichnung KuReArStart.php. Die notwendige CSS-Datei ist KuReArStart.css. Diese beiden befinden sich im Verzeichnis htdocs. Alle weiteren Programme kommen in ein Unterverzeichnis /htdocs/KuReAr. Dorthin verweisen entsprechend die Links in den Schaltflächen. Insgesamt gilt also für die Ordnerstruktur:

Ordner-Struktur

  • Für KuReArStart.php und KuReArStart.css: http://localhost/KuReArStart.php
  • Für Rechnungen eingeben: http://localhost/KuReAr/KuReArRechnungen.php
  • Für Artikel bearbeiten: http://localhost/KuReAr/KuReArArtikel.php
  • Für Kundendaten bearbeiten: http://localhost/KuReAr/KuReArKunden.php
  • usw.

7.2 Start

7.2.1 KuReArStart.php

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Rechnungsverwaltung</title>

  <link rel="stylesheet" href="KuReArStart.css">

</head>

<body>

 

<!--

Äußerer Bereich der Startseite.

Die Klasse "box" kann in der CSS-Datei beispielsweise

für Breite, Hintergrundfarbe, Rahmen und Abstände

verwendet werden.

-->

<div class="box">

<h1>Rechnungsverwaltung</h1>

    

<!--

Navigationsbereich mit Verweisen auf die einzelnen

Programme der Rechnungsverwaltung.

Die Klasse "menu" dient der Gestaltung und

Anordnung der Menüeinträge.

-->

<nav class="menu">

<!-- Erfassung neuer Rechnungen -->

    <a class="btn" href="KuReAr/KuReArRechnungen.php">

      Rechnungen eingeben</a>

    

<!-- Bearbeitung der Artikeldaten -->

    <a class="btn" href="KuReAr/KuReArArtikel.php">

    Artikel bearbeiten</a>

    

<!-- Bearbeitung der Kundendaten -->

    <a class="btn" href="KuReAr/KuReArKunden.php">

    Kundendaten bearbeiten</a>

    

<!-- Trennlinie zwischen Verwaltung und Auswertung -->

    <hr class="sep">

    

<!-- Ausgabe einer Liste der Rechnungen -->

    <a class="btn" href="KuReAr/KuReArAusw1.php">

    Liste der Rechnungen</a>

    

<!-- Ausgabe einer Liste der Kunden -->

    <a class="btn" href="KuReAr/KuReArAusw2.php">

    Liste der Kunden</a>

    

<!-- Ausgabe des aktuellen Artikelbestands -->

    <a class="btn" href="KuReAr/KuReArAusw3.php">

    Artikelbestand</a>

</nav>

</div>

</body>

</html>

Hinweis zum Navigationselement nav: nav ist keine Anweisung, sondern ein semantisches HTML-Element. Die Bezeichnung ist von "navigation" abgeleitet. Es kennzeichnet einen Bereich mit wichtigen Navigationsverweisen, beispielsweise ein Hauptmenü. Es verändert für sich genommen die Darstellung normalerweise nicht wesentlich. Seine Hauptaufgabe besteht darin, die Bedeutung des Bereichs zu beschreiben. Das hilft insbesondere Browsern und Suchmaschinen.

7.2.2 KuReArStart.css

Wie immer gilt: Die Programmerläuterungen beziehen sich, wenn nicht anders angegeben, auf den nachfolgenden Programmabschnitt.

/*

Diese Regel gilt für alle HTML-Elemente.

box-sizing: border-box bewirkt, dass die mit width und

height festgelegten Größen bereits die Innenabstände

und Rahmen enthalten.

Der Stern im folgenden Befehl ist der sog. Universalselektor.

Er wählt alle HTML-Elemente der Webseite aus.

*/

* {box-sizing: border-box;}

 

/*

Grundlegende Gestaltung des sichtbaren Seitenbereichs.

margin: 0 entfernt den vom Browser normalerweise

vorgegebenen Außenabstand.

padding: 20px erzeugt einen Innenabstand zwischen dem

Rand des Browserfensters und dem Inhalt der Webseite.

Als Schriftart wird Arial verwendet. Falls Arial nicht

verfügbar ist, wählt der Browser eine andere serifenlose

Schriftart.

*/

body {margin: 0; padding: 20px; font-family: Arial, sans-serif;}

 

/*

Äußerer Bereich der Rechnungsverwaltung.

width: 100% bewirkt, dass die Box grundsätzlich die

verfügbare Breite einnimmt.

max-width: 500px begrenzt ihre Breite jedoch auf

höchstens 500 Pixel. In schmalen Browserfenstern kann

die Box dadurch entsprechend kleiner werden.

margin: 20px auto legt oben und unten einen Außenabstand

von 20 Pixeln fest. Der Wert auto für links und rechts

zentriert die Box horizontal.

padding legt die Innenabstände fest:

- oben: 18 Pixel

- rechts: 18 Pixel

- unten: 22 Pixel

- links: 18 Pixel

border erzeugt einen schwarzen, durchgezogenen Rahmen

mit einer Stärke von 3 Pixeln.

*/

.box {width: 100%; max-width: 500px;margin: 20px auto;

  padding: 18px 18px 22px; border: 3px solid #000;

}

 

/*

Gestaltung der Hauptüberschrift.

margin legt die Außenabstände fest:

- oben: 6 Pixel

- rechts: 0

- unten: 18 Pixel

- links: 0

font-size legt die Schriftgröße auf 28 Pixel fest.

text-align: center zentriert den Text innerhalb des

Überschriftenbereichs.

*/

h1 {margin: 6px 0 18px; font-size: 28px; text-align: center;}

 

/*

Anordnung des Navigationsmenüs mit Flexbox.

display: flex aktiviert das Flexbox-Verfahren.

flex-direction: column ordnet die unmittelbar

untergeordneten Elemente senkrecht untereinander an.

gap legt zwischen den Menüelementen einen Abstand

von 12 Pixeln fest.

*/

.menu {display: flex; flex-direction: column; gap: 12px;}

 

/*

Gestaltung der Verweise als Schaltflächen. "btn" steht für Button.

display: block macht aus dem Verweis ein Blockelement.

Dadurch kann er die verfügbare Breite einnehmen.

padding legt den Innenabstand fest:

- oben und unten: 12 Pixel

- links und rechts: 14 Pixel

border erzeugt einen schwarzen, durchgezogenen Rahmen

mit einer Stärke von 2 Pixeln.

background-color legt einen hellgrauen Hintergrund fest.

color: #000 legt Schwarz als Schriftfarbe fest.

font-size legt die Schriftgröße auf 16 Pixel fest.

text-decoration: none entfernt die für Verweise übliche

Unterstreichung.

*/

.btn {display: block; padding: 12px 14px;border: 2px solid #000;

  background-color: #eee;color: #000;font-size: 16px;

    text-decoration: none;}

 

/*

Gestaltung einer Schaltfläche, wenn sich der Mauszeiger

darüber befindet.

Die Hintergrundfarbe wird etwas dunkler. Dadurch erhält

der Benutzer eine optische Rückmeldung.

*/

.btn:hover {background-color: #ddd;}

 

/*

Trennlinie zwischen den Programmen zur Datenerfassung

und Datenbearbeitung und den Auswertungsprogrammen.

margin legt oben und unten einen Außenabstand von

6 Pixeln fest. Links und rechts gibt es keinen

zusätzlichen Außenabstand.

border: none entfernt die Standarddarstellung des

hr-Elements.

border-top erzeugt stattdessen eine schwarze,

durchgezogene obere Linie mit einer Stärke von

2 Pixeln.

*/

.sep {margin: 6px 0;border: none;border-top: 2px solid #000;}

7.3 Rechnungen eingeben

7.3.1 Aufgabe

Nun das Programm "hinter" dieser Schaltfläche:

Nach Betätigen der Schaltfläche soll folgende Maske erscheinen:

Die Maske soll folgende Funktionalitäten erfassen, bzw. ermöglichen:

  • Eingabe der Anzahl an Rechnungspositionen ("Maske anpassen"). Drücken von "Maske anpassen" soll dann die Anzahl einzugebender Positionen ändern und zu folgender Anzeige führen:

  • Wahl des Kunden, dem die Rechnung gilt. Das Eingabefenster greift auf die Kundendatei zu, zeigt mit Hilfe eines Dropdown-Menüs Name und Kundennummer an und erlaubt die Übernahme der Daten.

  • Eingabe des Rechnungsdatums

  • Wahl des Artikels.

Klickt man auf ein Artikelfeld werden wiederum über ein Dropdown-Menü die Artikel der Datei (Relation) Artikel angezeigt und es kann ausgewählt werden.

In der letzten Spalte wird dann noch die Anzahl der Artikel der Position angegeben. Sind die Rechnungsdaten eingetragen, werden durch Betätigen von

die Daten in die Datenbank eingetragen. Danach soll die neue Rechnung z.B. so angezeigt werden:

 

Betätigen von "Zurück" führt wieder zur Leitseite.

Eingabe einer Rechnung mit http://localhost/KuReAr/KuReArRechnungen.php:

Anzeigen der neuen Rechnung mit KuReAr_6_show.php

Gesamtausgabe aller vier Relationen (Tabellen) mit KuReAr_5.php

Insgesamt werden also zusätzlich zu KuReArStart (in htdocs) folgende Programme für die Bearbeitung von Rechnungen im Verzeichnis /htdocs/KuReAr angelegt:

  • KuReArRechnungen.php für die Eingabe einer neuen Rechnung
  • KuReArRechnungen.css
  • KuReAr_6_show.php für das Anzeigen der Rechnung nach erfolgter Eingabe einer neuen Rechnung
  • KuReAr_6_show.css
  • KuReAr_5.php für testweise Ausgabe aller vier Relationen in einer Tabelle nach erfolgter Eingabe einer neuen Rechnung
  • KuReAr_5.css

Nun die Programme.

7.3.2 KuReArRechnungen.php

<?php

/*

1. VERBINDUNGSDATEN ZUR DATENBANK

Der DSN (Data Source Name) beschreibt, wie PDO die

Verbindung zur MySQL-Datenbank herstellen soll:

- mysql bezeichnet den zu verwendenden PDO-Treiber.

- host=localhost bedeutet, dass der Datenbankserver auf

    demselben Rechner wie das PHP-Programm läuft.

- dbname=KuReAr gibt die Bezeichnung der Datenbank an.

- charset=utf8mb4 legt den Zeichensatz der Verbindung fest.

*/

$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";

/*

Benutzername und Passwort für den Datenbankzugriff.

Bei einer lokalen XAMPP-Testinstallation wird häufig

der Benutzer root ohne Passwort verwendet. In einer

öffentlich erreichbaren Anwendung wäre dies ungeeignet.

*/

$user = "root";

$pass = "";

/*

2. VOREINSTELLUNGEN FÜR DIE EINGABEMASKE

Beim ersten Aufruf des Programms werden standardmäßig

drei Eingabezeilen für Rechnungspositionen angezeigt.

*/

$defaultPosCount = 3;

$posCount = $defaultPosCount;

/*

Variablen für Rückmeldungen an den Benutzer:

- $message enthält den Meldungstext.

- $messageClass enthält die zugehörige CSS-Klasse,

beispielsweise ok oder err.

*/

$message = "";

$messageClass = "";

/*

Die Arrays werden vorsorglich initialisiert.

Falls bereits der Verbindungsaufbau fehlschlägt, können

die Variablen dadurch später trotzdem ohne PHP-Warnung

verwendet werden.

*/

$kunden = [];

$artikel = [];

/*

3. HILFSFUNKTION FÜR DIE SICHERE HTML-AUSGABE

htmlspecialchars() vgl. Anhang.

ENT_QUOTES berücksichtigt sowohl doppelte als auch

einfache Anführungszeichen.

*/

function h(string $s): string

{

  return htmlspecialchars($s, ENT_QUOTES, "UTF-8");

}

try {

/*

4. VERBINDUNG ZUR DATENBANK HERSTELLEN

new PDO() erzeugt ein Objekt der Klasse PDO. Die

Variable $pdo verweist auf dieses Verbindungsobjekt.

PDO::ERRMODE_EXCEPTION legt fest, dass PDO bei einem

Datenbankfehler eine PDOException auslöst. Diese kann

im catch-Abschnitt zentral behandelt werden.

*/

$pdo = new PDO($dsn,$user,$pass,

  [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

/*

5. STAMMDATEN LADEN

Für die Auswahllisten der Eingabemaske werden die

Kunden- und Artikeldaten aus der Datenbank gelesen.

query() kann hier verwendet werden, weil die

SQL-Anweisungen keine Werte aus Benutzereingaben

enthalten.

*/

 

// Kunden nach Name und Vorname sortiert laden.

$kunden = $pdo->query("SELECT KuNr, Name, Vorname FROM Kunden

  ORDER BY Name, Vorname")->fetchAll(PDO::FETCH_ASSOC);

 

// Artikel nach ihrer Beschreibung sortiert laden.

$artikel = $pdo->query(

  "SELECT ArtNr, Beschreibung, Preis FROM Artikel

    ORDER BY Beschreibung")->fetchAll(PDO::FETCH_ASSOC);

/*

6. ANZAHL DER POSITIONSZEILEN FESTLEGEN

Falls das Formular einen Wert für posCount übermittelt

hat, wird dieser übernommen.

Die Anzahl wird auf den Bereich von 1 bis 20 begrenzt.

Die serverseitige Begrenzung ist auch dann wirksam,

wenn jemand die HTML-Vorgaben im Browser umgeht.

*/

if (isset($_POST["posCount"])) {

    $posCount = (int)$_POST["posCount"];

if ($posCount < 1) {$posCount = 1;}

if ($posCount > 20) {$posCount = 20;}

}

/*

7. GEWÜNSCHTE AKTION FESTSTELLEN

Das Formular kann zwei Aktionen auslösen:

- resize: Anzahl der Eingabezeilen ändern

- save: Rechnung in der Datenbank speichern

Der Null-Koaleszenz-Operator ?? liefert eine leere

Zeichenkette, falls action nicht übermittelt wurde.

*/

$action = $_POST["action"] ?? "";

/*

8. FALL 1: RECHNUNG SPEICHERN

*/

if ($action === "save") {

/*

Kopfdaten aus dem Formular übernehmen.

Formulardaten treffen grundsätzlich als

Zeichenketten ein. Die Kundennummer wird daher

ausdrücklich in eine ganze Zahl umgewandelt.

*/

$KuNr = (int)($_POST["KuNr"] ?? 0);

$ReDatum = trim($_POST["ReDatum"] ?? "");

/*

Prüfen, ob die erforderlichen Kopfdaten vorhanden

sind.

*/

if ($KuNr <= 0 || $ReDatum === "") {

  throw new Exception(

    "Kunde und Rechnungsdatum müssen angegeben werden."

    );

}

/*

Das Format des Datums prüfen.

createFromFormat() versucht, aus der Eingabe ein

Datum im Format Jahr-Monat-Tag zu erzeugen.

*/

$datum = DateTime::createFromFormat("Y-m-d",$ReDatum);

if ($datum === false ||

  $datum->format("Y-m-d") !== $ReDatum) {

    throw new Exception("Das Rechnungsdatum ist ungültig.");

}

/*

9. POSITIONEN AUS DEM FORMULAR EINSAMMELN

In $positions werden alle vollständig ausgefüllten

Rechnungspositionen gesammelt.

Vollständig leere Zeilen werden ignoriert. Ist nur

eines der beiden Felder ausgefüllt, wird dagegen

eine Fehlermeldung erzeugt.

*/

$positions = [];

for ($i = 1; $i <= $posCount; $i++) {

  $ArtNr = (int)($_POST["ArtNr_$i"] ?? 0);

$Anzahl = (int)($_POST["Anzahl_$i"] ?? 0);

/*

Eine vollständig leere Positionszeile wird übersprungen.

*/

if ($ArtNr === 0 && $Anzahl === 0) {

    continue;

}

/*

Eine teilweise oder ungültig ausgefüllte

Positionszeile wird nicht stillschweigend ignoriert.

*/

if ($ArtNr <= 0 || $Anzahl <= 0) {throw new Exception(

    "Position $i ist nicht vollständig "

      . "oder nicht gültig ausgefüllt.");

}

/*

Die Positionsnummern werden lückenlos neu vergeben.

Leere Zeilen verursachen dadurchkeine Lücken in der Nummerierung.

*/

$positions[] = ["PosNr" => count($positions) + 1,

  "ArtNr" => $ArtNr, "Anzahl" => $Anzahl];

}

/*

Eine Rechnung muss mindestens eine gültige

Rechnungsposition besitzen.

*/

if (count($positions) === 0) {throw new Exception(

  "Mindestens eine Rechnungsposition "

    . "muss ausgefüllt sein.");

}

/*

10. TRANSAKTION STARTEN

Rechnungskopf und Rechnungspositionen gehören zusammen.

Deshalb werden alle zugehörigen Daten innerhalb einer

Transaktion gespeichert.Entweder werden sämtliche Daten

übernommen oder im Fehlerfall vollständig zurückgenommen.

*/

$pdo->beginTransaction();

/*

11. NEUE RECHNUNGSNUMMER ERMITTELN

Zunächst wird die größte bisher vorhandene

Rechnungsnummer ermittelt und anschließend um

eins erhöht.

Konkret:

Die SQL-Anweisung ermittelt mit MAX(ReNr) die größte vorhandene

Rechnungsnummer. Ist die Relation noch leer, ersetzt IFNULL() den

Wert NULL durch 0. Anschließend wird der Wert um 1 erhöht und mit

der Bezeichnung NeueNr zurückgegeben. fetch(PDO::FETCH_ASSOC) liest

das Ergebnis als assoziatives Array. Der Wert mit dem Schlüssel

"NeueNr" wird danach in eine ganze Zahl umgewandelt und in $NeueReNr

gespeichert.

*/

$stmt = $pdo->query("SELECT IFNULL(MAX(ReNr), 0) + 1 AS NeueNr

  FROM RechKoepfe");

$row = $stmt->fetch(PDO::FETCH_ASSOC);

$NeueReNr = (int)$row["NeueNr"];

/*

12. RECHNUNGSKOPF SPEICHERN

Die Fragezeichen sind Positionsplatzhalter eines

Prepared Statements.

Beim Aufruf von execute() müssen die Werte in

derselben Reihenfolge angegeben werden wie die

zugehörigen Platzhalter.

*/

$stmtHead = $pdo->prepare("INSERT INTO RechKoepfe

    (ReNr, KuNr, ReDatum) VALUES (?, ?, ?)");

$stmtHead->execute([$NeueReNr,$KuNr,$ReDatum]);

/*

13. RECHNUNGSPOSITIONEN SPEICHERN

Das Prepared Statement wird nur einmal vorbereitet.

Anschließend kann es für jede Rechnungsposition

erneut ausgeführt werden.

*/

$stmtPos = $pdo->prepare("INSERT INTO RechPos

  (ReNr, PosNr, ArtNr, Anzahl) VALUES(?,?, ?, ?)");

foreach ($positions as $p)

    {$stmtPos->execute([$NeueReNr,$p["PosNr"],$p["ArtNr"],

      $p["Anzahl"]]);

}

/*

14. TRANSAKTION ABSCHLIESSEN

Da alle Datenbankoperationen erfolgreich waren,

werden die Änderungen mit commit() dauerhaft übernommen.

*/

$pdo->commit();

/*

15. ZUR ANZEIGESEITE WEITERLEITEN

Die Rechnungsnummer wird als URL-Parameter an die

Anzeigeseite übergeben.

urlencode() sorgt dafür, dass der übergebene Wert

für die Verwendung innerhalb einer URL geeignet ist.

exit beendet das aktuelle PHP-Programm unmittelbar

nach dem Versenden des Location-Headers.

*/

header("Location: KuReAr_6_show.php?ReNr="

    . urlencode((string)$NeueReNr));

exit;

}

/*

16. FALL 2: EINGABEMASKE ANPASSEN

In diesem Fall werden keine Daten in der Datenbank

gespeichert. Es wird lediglich die Anzahl der

angezeigten Positionszeilen geändert.

*/

if ($action === "resize") {$message = "Maske angepasst: "

    . $posCount . "Position(en).";

$messageClass = "ok";

    }

} catch (Exception $e) {

 

/*

17. FEHLERBEHANDLUNG

Falls während einer laufenden Transaktion ein Fehler

aufgetreten ist, werden alle bis dahin vorgenommenen

Änderungen zurückgenommen.

Konkret: Zunächst prüft isset($pdo), ob die Variable $pdo

vorhanden ist und nicht null enthält. Anschließend prüft

$pdo->inTransaction(), ob eine Transaktion aktiv ist.

Nur wenn beide Bedingungen erfüllt sind, setzt $pdo->rollBack()

die Transaktion zurück. Durch die Kurzschlussauswertung des

UND-Operators && wird inTransaction() nur aufgerufen, wenn

$pdo tatsächlich vorhanden ist.

*/

if (isset($pdo) && $pdo->inTransaction()) {$pdo->rollBack();}

/*

Der Meldungstext wird zunächst unverändert gespeichert.

Die Umwandlung in eine sichere HTML-Ausgabe erfolgt

erst an der Ausgabestelle mit h().

*/

$message = "Fehler: ". $e->getMessage();

$messageClass = "err";

}

?>

<!doctype html>

<html lang="de">

<head>

    <meta charset="utf-8">

    <title>KuReAr - Rechnung erfassen</title>

    <link rel="stylesheet" href="KuReArRechnungen.css" >

</head>

 

<body>

<div class="box">

<div class="caption">

  KuReAr - Eingabe einer Rechnung mit mehreren Positionen

</div>

<h2>Rechnung erfassen</h2>

  <?php if ($message !== ""): ?>

<!--

Eine vorhandene Rückmeldung wird ausgegeben.

h($messageClass) schützt die Bezeichnung der CSS-Klasse.

h($message) schützt den eigentlichen Meldungstext.

-->

<div class="msg <?= h($messageClass) ?>"><?= h($message) ?>

</div>

<?php endif; ?>

<!--

FORMULAR 1: ANZAHL DER POSITIONEN ÄNDERN

Dieses Formular verändert nur die Anzahl der sichtbaren

Positionszeilen. Es speichert noch keine Rechnung.

-->

<form method="post" class="row">

<label for="posCount">Anzahl Positionen:</label>

<input type="number" id="posCount" name="posCount" min="1"

    max="20" value="<?= (int)$posCount ?>">

<button type="submit" name="action" value="resize">

Maske anpassen

</button>

<!--

Bereits eingegebene Kopfdaten werden als verborgene

Formularfelder mitübertragen. Dadurch gehen diese

Werte beim Anpassen der Maske nicht verloren.

-->

<input type="hidden" name="KuNr"

    value="<?= h($_POST["KuNr"] ?? "") ?>">

<input type="hidden" name="ReDatum"

    value="<?= h($_POST["ReDatum"] ?? "") ?>">

<?php

/*

Auch die bereits eingegebenen Positionsdaten werden

in verborgenen Formularfeldern mitübertragen.

*/

for ($i = 1; $i <= $posCount; $i++) {

  $a = h($_POST["ArtNr_$i"] ?? "");

  $n = h($_POST["Anzahl_$i"] ?? "");

  echo "<input type=\"hidden\" " . "name=\"ArtNr_$i\" "

    . "value=\"$a\">";

  echo "<input type=\"hidden\" "

    . "name=\"Anzahl_$i\" " . "value=\"$n\">";

}

?>

</form>

<!--

FORMULAR 2: RECHNUNG SPEICHERN

Dieses Formular übermittelt die Kopfdaten und alle

Rechnungspositionen zur Speicherung.

-->

<form method="post">

<!--

Die aktuelle Anzahl der Positionszeilen wird

ebenfalls an das PHP-Programm übermittelt.

-->

<input type="hidden" name="posCount" value="<?= (int)$posCount ?>">

<div class="row">

<label for="KuNr">Kunde:</label><br>

<!--

Die Einträge der Auswahlliste werden aus den

zuvor geladenen Kundendaten erzeugt.

-->

<select id="KuNr" name="KuNr" required>

<option value="">

-- bitte wählen --

</option>

 

<?php foreach ($kunden as $k): ?>

 

<?php

$val = (int)$k["KuNr"];

/*

Eine bereits vorgenommene Kundenauswahl

wird beim erneuten Anzeigen des

Formulars beibehalten.

*/

$selectedKuNr = (int)($_POST["KuNr"] ?? 0);

?>

<option value="<?= $val ?>"

<?= $selectedKuNr === $val ? "selected" : "" ?>>

<?= h($k["Name"] . ", " . $k["Vorname"]

    . " (KuNr " . $k["KuNr"] . ")" ) ?>

</option>

<?php endforeach; ?>

</select>

</div>

<div class="row">

<label for="ReDatum">Rechnungsdatum:</label><br>

<!--

Das Eingabefeld type="date" unterstützt die

Eingabe eines Datums. Eine bereits übermittelte

Eingabe bleibt erhalten.

-->

<input type="date" id="ReDatum" name="ReDatum"

required value="<?= h($_POST["ReDatum"] ?? "") ?>">

</div>

<div class="row">

<table class="rel">

  <thead>

    <tr>

      <th>PosNr</th>

      <th>Artikel</th>

      <th>Anzahl</th>

    </tr>

  </thead>

<tbody>

<?php

for ($i = 1;$i <= $posCount;$i++ ): ?>

<?php

/*

Bereits übermittelte Werte werden übernommen, damit sie bei

einer erneuten Anzeige nicht verloren gehen.

*/

$artSel =$_POST["ArtNr_$i"] ?? "";

$anzVal = $_POST["Anzahl_$i"] ?? "";

?>

<tr>

<!--

Laufende Nummer der Eingabezeile. Beim Speichern werden die

tatsächlich verwendeten Positionen lückenlos nummeriert.

-->

<td><?= $i ?></td>

<td>

<!--

Auswahlliste für den Artikel. Der Wert 0 kennzeichnet eine

unbenutzte Positionszeile.

-->

<select name="ArtNr_<?= $i ?>">

<option value="0"> -- leer --</option>

<?php foreach ($artikel as $a): ?>

<?php

$val = (int)$a["ArtNr"];

/*

Eine bereits getroffene Artikelauswahl wird wieder markiert.

*/

$selected = (string)$artSel !== "" && (int)$artSel === $val;

?>

<option value="<?= $val ?>" <?= $selected ? "selected" : "" ?>>

<?= h($a["Beschreibung"] . " (ArtNr " . $a["ArtNr"] . ", "

  . $a["Preis"] . " EUR)") ?>

</option>

<?php endforeach; ?>

</select>

</td>

<td>

<!--

Eingabe der Artikelmenge.

min="0" erlaubt den Wert 0 für eine unbenutzte Positionszeile.

Eine tatsächlich verwendete Position muss serverseitig eine

Anzahl größer als 0 besitzen.

-->

<input type="number" name="Anzahl_<?= $i ?>" min="0"

    value="<?= h((string)$anzVal) ?>">

</td>

</tr>

<?php endfor; ?>

</tbody>

</table>

</div>

<div class="actions">

<!-- Speichert die eingegebene Rechnung. -->

<button type="submit" name="action" value="save">

Rechnung speichern

</button>

<!--

Dieser Schalter sendet das Formular nicht ab,

sondern ruft die Startseite auf.

-->

<button type="button" onclick="location.href='/KuReArStart.php'">

Zurück

</button>

</div>

</form>

</div>

</body>

</html>

7.3.3 KuReArRechnungen ohne Kommentare

Wegen dem Programmumfang und der zahlreichen Kommentare hier noch eine Fassung ohne Kommentare.

<?php

$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";

$user = "root";

$pass = "";

$defaultPosCount = 3;

$posCount = $defaultPosCount;

$message = "";

$messageClass = "";

$kunden = [];

$artikel = [];

function h(string $s): string

{

  return htmlspecialchars($s, ENT_QUOTES, "UTF-8");

}

try {

  $pdo = new PDO($dsn, $user, $pass,

    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

  $kunden = $pdo->query(

    "SELECT KuNr, Name, Vorname FROM Kunden ORDER BY Name, Vorname"

  )->fetchAll(PDO::FETCH_ASSOC);

  $artikel = $pdo->query(

    "SELECT ArtNr, Beschreibung, Preis FROM Artikel ORDER BY Beschreibung"

  )->fetchAll(PDO::FETCH_ASSOC);

  if (isset($_POST["posCount"])) {

    $posCount = (int)$_POST["posCount"];

    if ($posCount < 1) {

      $posCount = 1;

    }

    if ($posCount > 20) {

      $posCount = 20;

    }

  }

  $action = $_POST["action"] ?? "";

  if ($action === "save") {

    $KuNr = (int)($_POST["KuNr"] ?? 0);

    $ReDatum = trim($_POST["ReDatum"] ?? "");

    if ($KuNr <= 0 || $ReDatum === "") {

      throw new Exception(

      "Kunde und Rechnungsdatum müssen angegeben werden."

      );

    }

    $datum = DateTime::createFromFormat("Y-m-d", $ReDatum);

    if ($datum === false || $datum->format("Y-m-d") !== $ReDatum) {

      throw new Exception("Das Rechnungsdatum ist ungültig.");

    }

    $positions = [];

    for ($i = 1; $i <= $posCount; $i++) {

      $ArtNr = (int)($_POST["ArtNr_$i"] ?? 0);

      $Anzahl = (int)($_POST["Anzahl_$i"] ?? 0);

      if ($ArtNr === 0 && $Anzahl === 0) {

      continue;

      }

      if ($ArtNr <= 0 || $Anzahl <= 0) {

      throw new Exception("Position $i ist nicht vollständig "

      . "oder nicht gültig ausgefüllt."

      );

      }

      $positions[] = [

      "PosNr" => count($positions) + 1,

      "ArtNr" => $ArtNr,

      "Anzahl" => $Anzahl];

    }

    if (count($positions) === 0) {

      throw new Exception(

      "Mindestens eine Rechnungsposition muss ausgefüllt sein."

      );

    }

    $pdo->beginTransaction();

    $stmt = $pdo->query(

      "SELECT IFNULL(MAX(ReNr), 0) + 1 AS NeueNr FROM RechKoepfe");

    $row = $stmt->fetch(PDO::FETCH_ASSOC);

    $NeueReNr = (int)$row["NeueNr"];

    $stmtHead = $pdo->prepare("INSERT INTO RechKoepfe "

      . "(ReNr, KuNr, ReDatum) VALUES (?, ?, ?)");

    $stmtHead->execute([$NeueReNr, $KuNr, $ReDatum]);

    $stmtPos = $pdo->prepare("INSERT INTO RechPos "

      . "(ReNr, PosNr, ArtNr, Anzahl) VALUES (?, ?, ?, ?)");

    foreach ($positions as $p) {

      $stmtPos->execute(

      [$NeueReNr, $p["PosNr"], $p["ArtNr"], $p["Anzahl"]]);

    }

    $pdo->commit();

    header("Location: KuReAr_6_show.php?ReNr="

      . urlencode((string)$NeueReNr));

    exit;

  }

  if ($action === "resize") {

    $message = "Maske angepasst: " . $posCount . " Position(en).";

    $messageClass = "ok";

  }

} catch (Exception $e) {

  if (isset($pdo) && $pdo->inTransaction()) {

    $pdo->rollBack();

  }

  $message = "Fehler: " . $e->getMessage();

  $messageClass = "err";

}

?>

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>KuReAr - Rechnung erfassen</title>

  <link rel="stylesheet" href="KuReArRechnungen.css">

</head>

<body>

<div class="box">

  <div class="caption">

    KuReAr - Eingabe einer Rechnung mit mehreren Positionen

  </div>

  <h2>Rechnung erfassen</h2>

<?php if ($message !== ""): ?>

  <div class="msg <?= h($messageClass) ?>">

    <?= h($message) ?>

  </div>

<?php endif; ?>

  <form method="post" class="row">

  <label for="posCount">Anzahl Positionen:</label>

  <input type="number" id="posCount" name="posCount" min="1"

    max="20" value="<?= (int)$posCount ?>">

  <button type="submit" name="action" value="resize">Maske anpassen</button>

  <input type="hidden" name="KuNr"

    value="<?= h($_POST["KuNr"] ?? "") ?>">

  <input type="hidden" name="ReDatum"

    value="<?= h($_POST["ReDatum"] ?? "") ?>">

<?php

for ($i = 1; $i <= $posCount; $i++) {

  $a = h($_POST["ArtNr_$i"] ?? "");

  $n = h($_POST["Anzahl_$i"] ?? "");

  echo "<input type=\"hidden\" name=\"ArtNr_$i\" value=\"$a\">";

  echo "<input type=\"hidden\" name=\"Anzahl_$i\" value=\"$n\">";

}

?>

  </form>

  <form method="post">

  <input type="hidden" name="posCount" value="<?= (int)$posCount ?>">

  <div class="row">

    <label for="KuNr">Kunde:</label><br>

    <select id="KuNr" name="KuNr" required>

    <option value="">-- bitte wählen --</option>

<?php foreach ($kunden as $k):

  $val = (int)$k["KuNr"];

  $selectedKuNr = (int)($_POST["KuNr"] ?? 0);

?>

    <option value="<?= $val ?>"

      <?= $selectedKuNr === $val ? "selected" : "" ?>>

      <?= h($k["Name"] . ", " . $k["Vorname"]

      . " (KuNr " . $k["KuNr"] . ")") ?>

    </option>

<?php endforeach; ?>

    </select>

  </div>

  <div class="row">

    <label for="ReDatum">Rechnungsdatum:</label><br>

    <input type="date" id="ReDatum" name="ReDatum" required

      value="<?= h($_POST["ReDatum"] ?? "") ?>">

  </div>

  <div class="row">

    <table class="rel">

    <thead>

    <tr>

      <th>PosNr</th>

      <th>Artikel</th>

      <th>Anzahl</th>

    </tr>

    </thead>

    <tbody>

<?php for ($i = 1; $i <= $posCount; $i++):

  $artSel = $_POST["ArtNr_$i"] ?? "";

  $anzVal = $_POST["Anzahl_$i"] ?? "";

?>

    <tr>

    <td><?= $i ?></td>

    <td>

    <select name="ArtNr_<?= $i ?>">

    <option value="0">-- leer --</option>

<?php foreach ($artikel as $a):

  $val = (int)$a["ArtNr"];

  $selected = (string)$artSel !== "" && (int)$artSel === $val;

?>

    <option value="<?= $val ?>" <?= $selected ? "selected" : "" ?>>

      <?= h($a["Beschreibung"] . " (ArtNr " . $a["ArtNr"]

      . ", " . $a["Preis"] . " EUR)") ?>

    </option>

<?php endforeach; ?>

    </select>

    </td>

    <td>

    <input type="number" name="Anzahl_<?= $i ?>" min="0"

      value="<?= h((string)$anzVal) ?>">

    </td>

    </tr>

<?php endfor; ?>

    </tbody>

    </table>

  </div>

  <div class="actions">

    <button type="submit" name="action" value="save">

      Rechnung speichern

    </button>

    <button type="button" onclick="location.href='/KuReArStart.php'">

      Zurück

    </button>

  </div>

  </form>

</div>

</body>

</html>

7.3.4 KuReArRechnungen.css

Hier die CSS-Datei für obiges Programm. Es ist recht ausführlich und - für Einsteiger - mit vielen Erläuterungen versehen. Wie immer gilt: Die Erläuterungen beziehen sich auf die nachfolgenden Anweisungen.

/*

Diese Regel gilt für alle HTML-Elemente (Universalselektor).

box-sizing: border-box bewirkt, dass festgelegte Breiten

bereits die Innenabstände und Rahmen enthalten.

*/

* {box-sizing: border-box;}

/*

Grundlegende Gestaltung des sichtbaren Seitenbereichs.

margin: 0 entfernt den vom Browser standardmäßig

vorgegebenen Außenabstand.

padding: 20px erzeugt einen Abstand zwischen dem Rand

des Browserfensters und dem Inhalt der Webseite.

Als Schriftart wird Arial verwendet, ersatzweise eine andere

serifenlose Schriftart.

*/

body {margin: 0; padding: 20px; font-family: Arial, sans-serif;}

/*

Gestaltung der Überschrift zweiter Ordnung.

text-align: center zentriert den Text innerhalb des

Überschriftenbereichs.

margin legt die Außenabstände fest:

- oben: 10 Pixel

- rechts: 0

- unten: 14 Pixel

- links: 0

*/

h2 {margin: 10px 0 14px; text-align: center;}

/*

Äußerer Bereich der Eingabemaske.

width: 100% bewirkt, dass die Box grundsätzlich die

verfügbare Breite einnimmt.

max-width: 920px begrenzt ihre Breite jedoch auf

höchstens 920 Pixel. In einem schmalen Browserfenster kann

die Box dadurch kleiner werden. In einem breiteren Fenster

bleibt sie auf 920 Pixel begrenzt.

margin: 0 auto erzeugt oben und unten keinen zusätzlichen

Außenabstand. Der Wert auto für links und rechts zentriert

die Box horizontal.

*/

.box {width: 100%; max-width: 920px; margin: 0 auto;}

/*

Gestaltung der Beschriftung oberhalb der Eingabemaske.

color: #c00 legt einen kräftigen Rotton als Schriftfarbe

fest.

font-weight: 700 stellt den Text fett dar.

margin legt nur unterhalb der Beschriftung einen

Außenabstand von 8 Pixeln fest.

*/

.caption {margin: 0 0 8px; color: #c00; font-weight: 700;}

/*

Grundlegende Gestaltung der Meldungsbereiche.

margin erzeugt oberhalb und unterhalb der Meldung einen

Außenabstand von 10 Pixeln.

padding erzeugt auf allen Seiten einen Innenabstand von

10 Pixeln.

border erzeugt einen schwarzen, durchgezogenen Rahmen

mit einer Stärke von 2 Pixeln.

background-color legt Weiß als Hintergrundfarbe fest.

*/

.msg {margin: 10px 0;padding: 10px;border: 2px solid #000;

  background-color: #fff;}

/*

Gestaltung einer Erfolgsmeldung.

Ein HTML-Element mit den beiden Klassen msg und ok erhält

eine dunkelgrüne Schriftfarbe.

Beispiel:

<div class="msg ok">Rechnung wurde gespeichert.</div>

*/

.msg.ok {color: #060;}

/*

Gestaltung einer Fehlermeldung.

Ein HTML-Element mit den beiden Klassen msg und err erhält

eine dunkelrote Schriftfarbe.

Beispiel:

<div class="msg err">Fehler beim Speichern.</div>

*/

.msg.err {color: #900;}

/*

Gestaltung aller Beschriftungen von Formularfeldern.

font-weight: 700 stellt die Beschriftungen fett dar.

*/

label {font-weight: 700;}

/*

Abstand zwischen den einzelnen Bereichen der Eingabemaske.

Oberhalb und unterhalb jedes Bereichs wird ein

Außenabstand von 10 Pixeln erzeugt.

*/

.row {margin: 10px 0;}

/*

Gestaltung der Tabelle mit den Rechnungspositionen.

border-collapse: collapse führt die Rahmen benachbarter

Zellen zu einer gemeinsamen Linie zusammen.

width: 100% bewirkt, dass die Tabelle die gesamte

verfügbare Breite einnimmt.

border erzeugt einen schwarzen Außenrahmen mit einer

Stärke von 3 Pixeln.

font-size legt die Schriftgröße auf 15 Pixel fest.

*/

table.rel {width: 100%; border: 3px solid #000;

  border-collapse: collapse; font-size: 15px;}

/*

Gemeinsame Gestaltung der Kopf- und Datenzellen.

border erzeugt um jede Zelle einen schwarzen,

durchgezogenen Rahmen mit einer Stärke von 2 Pixeln.

padding legt den Innenabstand fest:

- oben und unten: 6 Pixel

- links und rechts: 10 Pixel

text-align: left richtet den Inhalt linksbündig aus.

background-color legt Weiß als Hintergrundfarbe fest.

*/

Zur Erinnerung: table.rel ist ein zusammengesetzer Selektor,

der alle HTML-Elemente auswählt, die eine table-Element sind

und gleichzeitg die CSS-Klasse rel besitzen.

table.rel th,

table.rel td {padding: 6px 10px; border: 2px solid #000;

    background-color: #fff;text-align: left;}

/*

Die Beschriftungen der Spaltenüberschriften werden fett

dargestellt.

*/

table.rel th {font-weight: 700;}

/*

Bereich mit den Schaltflächen unterhalb des Formulars.

margin-top erzeugt oberhalb des Bereichs einen

Außenabstand von 12 Pixeln.

display: flex aktiviert das Flexbox-Verfahren. Die

Schaltflächen werden dadurch nebeneinander angeordnet.

flex-wrap: wrap erlaubt einen Zeilenumbruch, falls auf

einem kleinen Bildschirm nicht genügend Platz vorhanden ist.

gap legt zwischen den Schaltflächen einen Abstand von

10 Pixeln fest.

*/

.actions {display: flex; flex-wrap: wrap; gap: 10px;

    margin-top: 12px;}

/*

Gestaltung aller Eingabefelder vom Typ number.

Die Eingabefelder für die Anzahl der Positionen und für

die Artikelmengen erhalten eine Breite von 90 Pixeln.

*/

input[type="number"] {width: 90px;}

/*

Gestaltung der Auswahllisten.

width: 100% bewirkt, dass eine Auswahlliste grundsätzlich

die verfügbare Breite einnimmt.

max-width: 100% verhindert, dass sie über den umgebenden

Bereich hinausragt.

*/

select {width: 100%; max-width: 100%;}

/*

Grundlegende Gestaltung aller Schaltflächen.

padding legt den Innenabstand fest:

- oben und unten: 6 Pixel

- links und rechts: 10 Pixel

border erzeugt einen schwarzen, durchgezogenen Rahmen

mit einer Stärke von 2 Pixeln.

background-color legt einen hellgrauen Hintergrund fest.

cursor: pointer zeigt beim Berühren der Schaltfläche mit

dem Mauszeiger einen Handzeiger an.

*/

button {padding: 6px 10px; border: 2px solid #000;

  background-color: #eee; cursor: pointer;}

/*

Gestaltung einer Schaltfläche, wenn sich der Mauszeiger

darüber befindet.

Die etwas dunklere Hintergrundfarbe gibt dem Benutzer

eine optische Rückmeldung.

*/

button:hover {background-color: #ddd;}

/*

Auf kleinen Bildschirmen kann die Tabelle breiter als

das Browserfenster werden. Dieser Medienausdruck wird

wirksam, sobald das Browserfenster höchstens 700 Pixel

breit ist.

Die äußere Box darf dann horizontal gescrollt werden.

Dadurch bleiben alle Spalten der Tabelle erreichbar.

*/

@media (max-width: 700px) {.box {overflow-x: auto;}

table.rel {min-width: 620px;}

}

7.3.5 Programmbeschreibung

Das Programm dient dazu, eine neue Rechnung mit mehreren Positionen in der Datenbank KuReAr zu erfassen. Dabei werden sowohl der Rechnungskopf mit Kunde und Rechnungsdatum als auch die zugehörigen Rechnungspositionen mit Artikel und Anzahl gespeichert.

Der Zugriff auf die Datenbank erfolgt über PDO (PHP Data Objects). Zu Beginn werden die Verbindungsparameter DSN, Benutzername und Passwort festgelegt. Anschließend wird eine Verbindung zur Datenbank hergestellt. Die Variable $posCount steuert die Anzahl der Eingabezeilen für die Rechnungspositionen. Standardmäßig werden drei Positionszeilen angezeigt. Der Benutzer kann diese Anzahl über ein Formular verändern.

Nach dem Verbindungsaufbau werden zunächst die benötigten Stammdaten geladen:

  • Kunden aus der Relation Kunden
  • Artikel aus der Relation Artikel

Diese Daten werden später in Auswahlfeldern (Dropdown-Menüs) angezeigt. Das Programm verwendet zwei HTML-Formulare:

  • ein Formular zur Anpassung der Anzahl der Positionszeilen
  • ein Formular zur Eingabe und Speicherung der Rechnung

Beim Absenden eines Formulars wird mithilfe der Variablen $action unterschieden, welche Aktion ausgeführt werden soll:

  • "resize": Die Anzahl der Positionszeilen wird angepasst.
  • "save": Die eingegebenen Rechnungsdaten werden geprüft und gespeichert.

Speichern der Rechnung

Beim Speichern werden zunächst die Eingabedaten geprüft. Es muss ein Kunde ausgewählt und ein Rechnungsdatum angegeben worden sein. Anschließend werden die eingegebenen Positionsdaten verarbeitet. Vollständig leere Positionszeilen werden ignoriert. Übernommen werden nur Positionen mit einer gültigen Artikelnummer und einer gültigen Anzahl. Die Positionsnummern (PosNr) werden automatisch fortlaufend vergeben.

Das Programm prüft außerdem, ob mindestens eine gültige Rechnungsposition vorhanden ist. Andernfalls wird eine Fehlermeldung ausgegeben.

Für die neue Rechnung wird eine neue Rechnungsnummer (ReNr) ermittelt. Dazu dient folgende SQL-Abfrage:

SELECT IFNULL(MAX(ReNr), 0) + 1

MAX(ReNr) ermittelt die bisher höchste Rechnungsnummer. Falls noch keine Rechnung vorhanden ist, liefert IFNULL() stattdessen den Wert 0. Durch die Addition von 1 entsteht die neue Rechnungsnummer. Dieses Verfahren eignet sich für das lokale Lernsystem. Bei einer Anwendung mit mehreren gleichzeitig arbeitenden Benutzern wäre dagegen eine automatisch erzeugte Rechnungsnummer, beispielsweise mit AUTO_INCREMENT, richtig.

Transaktion

Das Speichern erfolgt innerhalb einer Transaktion. Zunächst wird der Rechnungskopf in die Relation RechKoepfe eingefügt. Danach werden die zugehörigen Rechnungspositionen in der Relation RechPos gespeichert. Die Transaktion stellt sicher, dass entweder der Rechnungskopf und sämtliche Positionen vollständig gespeichert werden oder im Fehlerfall keine dieser Änderungen dauerhaft übernommen wird. Bei erfolgreicher Ausführung wird die Transaktion mit commit() abgeschlossen. Tritt beim Speichern ein Fehler auf, werden die bereits ausgeführten Änderungen mit rollBack() zurückgenommen.

Weiterleitung und Fehlerbehandlung

Nach erfolgreichem Speichern wird der Benutzer automatisch zur Anzeige der neu angelegten Rechnung weitergeleitet:

header(

  "Location: KuReAr_6_show.php?ReNr=" . $ReNr

);

Die Rechnungsnummer wird dabei als URL-Parameter an KuReAr_6_show.php übergeben. Dieses Programm lädt den Rechnungskopf und die zugehörigen Rechnungspositionen aus der Datenbank und zeigt sie an.

Tritt ein Fehler auf, wird eine entsprechende Fehlermeldung ausgegeben. Falls bereits eine Transaktion begonnen wurde, wird sie mit rollBack() zurückgesetzt.

Benutzeroberfläche

Die Eingabe erfolgt über eine übersichtlich aufgebaute HTML-Seite:

  • Auswahl des Kunden über ein Dropdown-Menü
  • Eingabe des Rechnungsdatums
  • Relation zur Eingabe mehrerer Rechnungspositionen
  • Schaltfläche "Maske anpassen" zur Veränderung der Anzahl der Positionszeilen
  • Schaltfläche "Rechnung speichern" zum Starten des Speichervorgangs
  • Schaltfläche "Zurück" für die Rückkehr zur Startseite
  • Ausgabe von Fehlermeldungen oberhalb des Formulars

Nach erfolgreichem Speichern wird die neue Rechnung auf einer eigenen Seite angezeigt.

Sicherheitsaspekte

Die Datenbankzugriffe erfolgen mit Prepared Statements. Dabei werden die SQL-Anweisungen und die zu verarbeitenden Werte getrennt an das Datenbanksystem übergeben. Dies schützt insbesondere vor SQL-Injection.

Die Funktion htmlspecialchars() sorgt für eine sichere Ausgabe von Daten innerhalb der HTML-Seite. Sonderzeichen in HTML werden in entsprechende HTML-Zeichenreferenzen umgewandelt. Dadurch wird verhindert, dass ausgegebene Daten als unerwünschter HTML- oder JavaScript-Code interpretiert werden. Die Prüfung und Typumwandlung der Eingabewerte stellt zusätzlich sicher, dass nur geeignete Daten verarbeitet werden. Vgl. auch den Anhang.

Zusammenfassung

Das Programm zeigt den typischen Ablauf einer Datenbankanwendung mit PHP und PDO:

  • Laden von Stammdaten
  • dynamische Anpassung eines Formulars
  • Prüfung von Benutzereingaben
  • Verarbeitung mehrerer zusammengehöriger Datensätze
  • Verwendung von Prepared Statements
  • Einsatz einer Transaktion
  • strukturierte Fehlerbehandlung
  • Weiterleitung zur Anzeige der gespeicherten Daten

Nun die weiteren Programme.

7.4 Verbindung zur Datenbank

7.4.1 KuReAr_5.php

<?php

/*

1. VERBINDUNGSDATEN ZUR DATENBANK

Der DSN (Data Source Name) beschreibt die Verbindung:

- mysql bezeichnet den verwendeten PDO-Treiber.

- host=localhost bedeutet, dass der Datenbankserver auf

demselben Rechner läuft.

- dbname=KuReAr bezeichnet die verwendete Datenbank.

- charset=utf8mb4 legt den Zeichensatz der Verbindung fest.

*/

$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";

$user = "root";

$pass = "";

//Prüfen der Eingabe

function h(mixed $value): string

{

    return htmlspecialchars((string)$value,ENT_QUOTES,"UTF-8");

}

//Start des try/catch - Abschnitts

try {

/*

3. VERBINDUNG ZUR DATENBANK HERSTELLEN

new PDO() erzeugt ein Objekt der Klasse PDO. Über dieses

Objekt werden anschließend die SQL-Anweisungen ausgeführt.

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION bewirkt,

dass PDO bei einem Datenbankfehler eine PDOException

auslöst. Diese wird im catch-Abschnitt behandelt.

*/

$pdo = new PDO($dsn,$user,$pass,

[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

/*

4. DATEN AUS DEN VIER RELATIONEN ABFRAGEN

Die SELECT-Anweisung verbindet folgende Relationen:

- Kunden

- RechKoepfe

- RechPos

- Artikel

Dabei werden für jede Rechnungsposition Daten über den

Kunden, den Rechnungskopf, die Rechnungsposition und den

zugehörigen Artikel ausgegeben.

Die Aliasnamen verkürzen die Schreibweise:

- k steht für Kunden

- rk steht für RechKoepfe

- rp steht für RechPos

- a steht für Artikel

k.Name AS KuName gibt der ausgewählten Spalte Name im

Abfrageergebnis den eindeutigen Namen KuName.

DATE_FORMAT() formatiert das Rechnungsdatum in dem in

Deutschland üblichen Format Tag.Monat.Jahr.

Zu den Inner Joins siehe auch den Anhang, sowie grundsätzlich und

ausführlich, die Texte zu Relationalen Datenbanken auf diesen

Webseiten

*/

$sql = "SELECT k.KuNr, k.Name AS KuName, rk.ReNr,

DATE_FORMAT(rk.ReDatum, '%d.%m.%Y') AS RechnDatum,

rp.PosNr, a.ArtNr, a.Beschreibung, a.Preis, rp.Anzahl

FROM RechKoepfe AS rk

INNER JOIN Kunden AS k ON rk.KuNr = k.KuNr

INNER JOIN RechPos AS rp ON rk.ReNr = rp.ReNr

INNER JOIN Artikel AS a ON rp.ArtNr = a.ArtNr

ORDER BY rk.ReNr, rp.PosNr";

 

/*

5. SQL-ANWEISUNG AUSFÜHREN

query() führt die SELECT-Anweisung aus.

Da die SQL-Anweisung keine Benutzereingaben enthält, ist

hier kein Prepared Statement erforderlich.

fetchAll(PDO::FETCH_ASSOC) liest alle Ergebniszeilen und

liefert sie als Array. Jede Zeile ist dabei ein assoziatives

Array, dessen Schlüssel den ausgewählten Spaltennamen entsprechen.

*/

$rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);

} catch (PDOException $e) {

 

/*

6. FEHLERBEHANDLUNG

Falls beim Aufbau der Verbindung oder beim Ausführen der

SQL-Anweisung ein Datenbankfehler auftritt, wird das

Programm mit die() beendet.

h() schützt die Fehlermeldung bei der Ausgabe im Browser.

*/

die("DB-Fehler: " . h($e->getMessage()));

}

?>

 

<!doctype html>

<html lang="de">

<head>

<!--

UTF-8 ermöglicht unter anderem die korrekte Darstellung

von Umlauten und Sonderzeichen.

-->

<meta charset="utf-8">

<title>Gesamte Datenbank KuReAr</title>

<link rel="stylesheet" href="KuReAr_5.css">

</head>

<body>

<div class="box">

 

<!--

Zusätzliche Beschriftung oberhalb der Überschrift.

-->

<div class="caption">Und jetzt alle vier Relationen:</div>

<h2>Die gesamte Datenbank Kunden - Rechnungen - Artikel</h2>

 

<!--

Die Ergebnismenge der SQL-Abfrage wird als HTML-Tabelle

ausgegeben.

Jede Ergebniszeile entspricht einer Rechnungsposition.

Die Angaben zum Kunden und zum Rechnungskopf wiederholen

sich deshalb, wenn eine Rechnung mehrere Positionen

enthält. Dies wurde hier akzeptiert.

Start der Tabellendarstellung:

-->

<table class="rel">

<thead>

 

<!-- Überschriften der Tabellenspalten -->

<tr>

  <th>KuNr</th>

  <th>KuName</th>

  <th>ReNr</th>

  <th>RechnDatum</th>

  <th>PosNr</th>

  <th>ArtNr</th>

  <th>Beschreibung</th>

  <th>Preis</th>

  <th>Anzahl</th>

</tr>

</thead>

<tbody>

 

<!--Prüfen, ob Anzahl der Datensätze gleich Null-->

<?php if (count($rows) === 0): ?>

<!--

Dieser Fall tritt ein, wenn die Abfrage keine

Datensätze geliefert hat.

colspan="9", weil der Text über die ganze Tabellenbreite geht

-->

<tr>

  <td colspan="9">Keine Daten vorhanden.</td>

</tr>

<!--else von obigem if und Schleifenbeginn über die Eergebnisse-->

<?php else: ?>

<?php foreach ($rows as $r): ?>

<!--

Für jede Ergebniszeile wird eine Zeile der

HTML-Tabelle erzeugt.

Alle Werte werden vor der Ausgabe mit h()

geschützt.

-->

<tr>

  <td><?= h($r["KuNr"]) ?></td>

  <td><?= h($r["KuName"]) ?></td>

  <td><?= h($r["ReNr"]) ?></td>

  <td><?= h($r["RechnDatum"]) ?></td>

  <td><?= h($r["PosNr"]) ?></td>

  <td><?= h($r["ArtNr"]) ?></td>

  <td><?= h($r["Beschreibung"]) ?></td>

  <td><?= h($r["Preis"]) ?></td>

  <td><?= h($r["Anzahl"]) ?></td>

</tr>

<?php endforeach; ?>

<?php endif; ?>

</tbody>

</table>

</div>

</body>

</html>

7.4.2 Anmerkungen zum Programm

  • Die mehrfachen Aufrufe von htmlspecialchars() (vgl. Anhang) wurden durch die Hilfsfunktion h() ersetzt.
  • Die Abfrage verwendet einen sog. Inner Join. Damit erscheinen nur Kunden mit Rechnungen und nur Rechnungen mit vorhandenen Positionen und zugeordneten Artikeln. Vgl. Anhang.
  • Dies ist eine Demonstration der Verknüpfung der vier Relationen zu Demonstrationszwecken, weshalb die redundante Darstellung bei den Rechnungspositionen akzeptiert wurde.

7.4.3 KuReAr_5.css

/*

Diese Regel gilt für alle HTML-Elemente (Universalselektor *).

box-sizing: border-box bewirkt, dass festgelegte Breiten

bereits die Innenabstände und Rahmen enthalten.

Dadurch wird insbesondere verhindert, dass die äußere Box

durch ihr padding breiter wird als vorgesehen.

*/

* {box-sizing: border-box;}

/*

1. GESTALTUNG DER GESAMTEN SEITE

Als bevorzugte Schriftart wird Arial verwendet. Falls Arial

nicht verfügbar ist, versucht der Browser Helvetica und

anschließend eine andere serifenlose Schriftart zu verwenden.

background-color legt einen sehr hellen Grauton als Hintergrundfarbe

der gesamten Webseite fest.

margin: 0 und padding: 0 entfernen die vom Browser standardmäßig

vorgesehenen Außen- und Innenabstände.

*/

body {margin: 0; padding: 0; background-color: #f5f5f5;

font-family: Arial, Helvetica, sans-serif;}

 

/*

2. GEMEINSAMER CONTAINER FÜR DIE AUSGABE

width: 100% bewirkt, dass die Box in einem kleinen

Browserfenster die verfügbare Breite einnimmt.

max-width: 940px begrenzt die Gesamtbreite auf 940 Pixel.

Dieser Wert setzt sich gedanklich aus der ursprünglich

festgelegten Breite von 900 Pixeln und den Innenabständen von

jeweils 20 Pixeln zusammen.

margin: 20px auto erzeugt oben und unten einen Außenabstand

von 20 Pixeln. Der Wert auto zentriert die Box horizontal.

padding erzeugt innerhalb der Box auf allen Seiten einen

Abstand von 20 Pixeln.

box-shadow erzeugt einen schwachen Schatten um die Box.

rgba() legt dabei Schwarz mit einer Deckkraft von 10 Prozent

fest.

overflow-x: auto erzeugt bei Bedarf eine horizontale

Bildlaufleiste. Dies ist hier wichtig, weil die Relation neun

Spalten besitzt.

*/

.box {width: 100%; max-width: 940px; margin: 20px auto;

  padding: 20px; overflow-x: auto;background-color: #fff;

  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);

}

 

/*

3. ROTER HINWEISTEXT OBERHALB DER ÜBERSCHRIFT

color legt Rot als Schriftfarbe fest.

font-weight: 700 stellt den Text fett dar.

margin-bottom erzeugt unterhalb des Hinweistextes einen

Außenabstand von 10 Pixeln.

*/

.caption {margin-bottom: 10px; color: red; font-weight: 700;}

/*

4. ÜBERSCHRIFT

text-align: center richtet die Überschrift horizontal zentriert aus.

margin-top: 0 entfernt den standardmäßigen oberen Außenabstand der

Überschrift.

*/

h2 {margin-top: 0; text-align: center;}

 

/*

5. RELATION AUSGEBEN / DARSTELLEN

width: 100% bewirkt, dass die Relation die gesamte Breite

ihres umgebenden Bereichs einnimmt.

border-collapse: collapse führt die Rahmen benachbarter

Zellen zu jeweils einer gemeinsamen Linie zusammen.

margin-top erzeugt oberhalb der Relation einen Außenabstand

von 15 Pixeln.

min-width verhindert, dass die neun Spalten auf kleinen

Bildschirmen zu stark zusammengedrückt werden. Bei Bedarf

kann innerhalb der Box horizontal gescrollt werden.

*/

.rel {width: 100%; min-width: 850px; margin-top: 15px;

border-collapse: collapse;

 

/*

6. TABELLENKOPF

Der Tabellenkopf erhält eine hellgraue Hintergrundfarbe.

Dadurch lässt er sich optisch von den Datenzeilen unterscheiden.

*/

.rel thead {background-color: #e0e0e0;}

 

/*

7. KOPF- UND DATENZELLEN

border erzeugt um jede Zelle einen grauen, durchgezogenen

Rahmen mit einer Stärke von einem Pixel.

padding legt den Innenabstand fest:

- oben und unten: 6 Pixel

- links und rechts: 8 Pixel

text-align: left richtet die Inhalte zunächst grundsätzlich

linksbündig aus.

*/

.rel th, .rel td {padding: 6px 8px; border: 1px solid #999;

text-align: left;}

 

/*

8. RECHTSBÜNDIGE AUSRICHTUNG DER ZAHLEN

:nth-child() wählt eine Zelle anhand ihrer Position innerhalb der

Tabellenzeile aus.

Rechtsbündig ausgegeben werden:

- 1. Spalte: KuNr

- 3. Spalte: ReNr

- 5. Spalte: PosNr

- 6. Spalte: ArtNr

- 8. Spalte: Preis

- 9. Spalte: Anzahl

Die übrigen Spalten enthalten Texte oder ein Datum und

bleiben linksbündig.

*/

.rel td:nth-child(1),

.rel td:nth-child(3),

.rel td:nth-child(5),

.rel td:nth-child(6),

.rel td:nth-child(8),

.rel td:nth-child(9) {text-align: right;}

 

/*

9. HERVORHEBUNG EINER DATENZEILE

:hover wird wirksam, wenn sich der Mauszeiger über einer

Tabellenzeile befindet. Die Zeile erhält dann eine sehr helle blaue

Hintergrundfarbe. Dies erleichtert das Lesen umfangreicher

Ergebnismengen.

Durch tbody wird sichergestellt, dass dieser Effekt nur für

die Datenzeilen und nicht für den Tabellenkopf gilt.

*/

.rel tbody tr:hover {background-color: #f0f8ff;}

 

/*

10. ANPASSUNG AN KLEINE BILDSCHIRME

Sobald das Browserfenster höchstens 980 Pixel breit ist,

erhält die Box links und rechts einen kleinen Außenabstand.

Die Funktion calc() zieht insgesamt 20 Pixel von der

verfügbaren Fensterbreite ab.

*/

@media (max-width: 980px) {

  .box {width: calc(100% - 20px); margin: 10px;}}

7.5 Anzeigen einer Rechnung

7.5.1 KuReAr_6_show.php

<?php

// Anzeige einer Rechnung mit Rechnungskopf und Rechnungspositionen

/*

1. VERBINDUNGSDATEN ZUR DATENBANK

Der DSN (Data Source Name) enthält:

- mysql: verwendeter PDO-Treiber

- host=localhost: Datenbankserver auf demselben Rechner

- dbname=KuReAr: Name der Datenbank

- charset=utf8mb4: Zeichensatz der Verbindung

*/

$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";

$user = "root";

$pass = "";

/*

2. HILFSFUNKTION FÜR DIE HTML-AUSGABE

htmlspecialchars() wandelt Zeichen mit einer besonderen

Bedeutung in HTML in entsprechende HTML-Zeichenreferenzen um.

Dadurch werden die ausgegebenen Daten nicht als HTML-Code

interpretiert. Vgl. auch den Anhang.

Der Datentyp mixed erlaubt die Übergabe von Zeichenketten,

Zahlen und NULL-Werten. Innerhalb der Funktion wird der Wert

in eine Zeichenkette umgewandelt.

*/

function h(mixed $value): string

{

    return htmlspecialchars((string)$value,ENT_QUOTES,"UTF-8");

}

/*

3. RECHNUNGSNUMMER AUS DER URL ÜBERNEHMEN

Das Programm erwartet eine URL in dieser Form:

KuReAr_6_show.php?ReNr=1001

$_GET["ReNr"] enthält dann den Wert 1001.

Der Null-Koaleszenzoperator ?? liefert 0, falls der Parameter

ReNr nicht vorhanden ist. Durch (int) wird der übergebene Wert

in eine ganze Zahl umgewandelt.

*/

$ReNr = (int)($_GET["ReNr"] ?? 0);

 

// Eine Rechnungsnummer muss größer als 0 sein.

// Ist dies nicht der Fall, wird das Programm mit die() beendet.

if ($ReNr <= 0) {

  die("Fehler: ReNr fehlt oder ist ungültig.");

}

try {

/*

4. VERBINDUNG ZUR DATENBANK HERSTELLEN

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION bewirkt,

dass PDO bei einem Datenbankfehler eine PDOException

auslöst.

PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC legt

fest, dass gelesene Datensätze standardmäßig als

assoziative Arrays zurückgegeben werden.

*/

$pdo = new PDO($dsn,$user,$pass,[

    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,

    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]

);

 

/*

5. RECHNUNGSKOPF LADEN

Die Relation RechKoepfe enthält die allgemeinen Angaben

zur Rechnung:

- Rechnungsnummer

- Rechnungsdatum

- Kundennummer

Über die Kundennummer wird die Relation Kunden verbunden.

Dadurch können zusätzlich Name und Vorname des Kunden ausgegeben

werden. DATE_FORMAT() erzeugt aus dem gespeicherten Datum eine

Darstellung in der Form Tag.Monat.Jahr.

*/

$sqlHead = "SELECT rk.ReNr, DATE_FORMAT(rk.ReDatum, '%d.%m.%Y')

  AS ReDatum, rk.KuNr, k.Name, k.Vorname

FROM RechKoepfe AS rk

INNER JOIN Kunden AS k

ON rk.KuNr = k.KuNr

WHERE rk.ReNr = :ReNr";

 

/*

prepare() bereitet die SQL-Anweisung vor.

Der benannte Platzhalter :ReNr wird erst bei execute()

mit der Rechnungsnummer verbunden. Dadurch wird die

Rechnungsnummer nicht unmittelbar in den SQL-Text

eingesetzt.

*/

$stmtHead = $pdo->prepare($sqlHead);

$stmtHead->execute(["ReNr" => $ReNr]);

 

/*

fetch() liest den gefundenen Rechnungskopf.

Wurde keine passende Rechnung gefunden, liefert fetch()

den booleschen Wert false.

*/

$head = $stmtHead->fetch();

 

/*

Das Programm wird beendet, wenn keine Rechnung mit der

angegebenen Rechnungsnummer vorhanden ist.

*/

if ($head === false) {die("Fehler: Rechnung nicht gefunden "

  . "(ReNr " . h($ReNr) . ").");

}

 

/*

6. RECHNUNGSPOSITIONEN LADEN

Die Relation RechPos enthält unter anderem:

- Positionsnummer

- Artikelnummer

- Anzahl

Über die Artikelnummer wird die Relation Artikel

verbunden. Dadurch stehen außerdem die Beschreibung

und der Preis des Artikels zur Verfügung.

Der Positionswert wird innerhalb der SQL-Anweisung berechnet:

Anzahl * Preis

Der Alias Positionswert gibt dem berechneten Ergebnis einen Namen.

Zum Inner-Join: Dieser verbindet die Rechnungspositionen mit den

zugehörigen Artikeldaten. Vgl. auch den Anhang. Die Join-Bedingung rp.ArtNr = a.ArtNr legt

fest, dass Datensätze mit derselben Artikelnummer zusammengeführt

werden. Die Aliase rp und a stehen für die Relationen RechPos und

Artikel. Die WHERE-Bedingung beschränkt das Ergebnis auf die

Positionen der durch :ReNr angegebenen Rechnung.

*/

$sqlPos = "SELECT rp.PosNr, rp.ArtNr, a.Beschreibung, a.Preis,

  rp.Anzahl, (rp.Anzahl * a.Preis) AS Positionswert

    FROM RechPos AS rp INNER JOIN Artikel AS a ON rp.ArtNr = a.ArtNr

      WHERE rp.ReNr = :ReNr

      ORDER BY rp.PosNr";

 

// Auch für diese Abfrage wird ein Prepared Statement verwendet.

$stmtPos = $pdo->prepare($sqlPos);

$stmtPos->execute(["ReNr" => $ReNr]);

 

// fetchAll() liest alle Rechnungspositionen und speichert sie in

// einem Array.

$pos = $stmtPos->fetchAll();

 

/*

7. RECHNUNGSSUMME BERECHNEN

Die Variable $summe erhält den Anfangswert 0. Die foreach-Schleife

durchläuft anschließend sämtliche Rechnungspositionen. Bei jeder

Position wird deren Positionswert zur Rechnungssumme addiert.

Die Typumwandlung (float) stellt sicher, dass mit einem

numerischen Wert gerechnet wird.

*/

$summe = 0.0;

foreach ($pos as $p) {

  $summe += (float)$p["Positionswert"];

  }

} catch (PDOException $e) {

 

/*

8. FEHLERBEHANDLUNG

Tritt beim Verbindungsaufbau oder beim Ausführen einer

SQL-Anweisung ein Datenbankfehler auf, wird das Programm

beendet.

Für das lokale Lernsystem wird hier die technische

Fehlermeldung ausgegeben. Auf einem öffentlichen

Webserver sollte der Benutzer nur eine allgemeine

Fehlermeldung erhalten.

*/

die("DB-Fehler: " . h($e->getMessage()));

}

?>

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Rechnung anzeigen</title>

 

<!--

Die Gestaltung der Webseite wird aus einer CSS-Datei geladen.

-->

<link rel="stylesheet" href="KuReAr_6_show.css">

</head>

<body>

<div class="box">

<!--

Hinweis darauf, dass die Rechnung zuvor erfolgreich

gespeichert wurde.

-->

<div class="caption">Rechnung wurde gespeichert -

Anzeige der neuen Rechnung

</div>

 

<!--

Die Rechnungsnummer wird in die Überschrift eingesetzt.

-->

<h2>Rechnung <?= h($head["ReNr"]) ?></h2>

 

<!--

Ausgabe der allgemeinen Rechnungsdaten. Vgl. zur Syntax Kapitel 9.

-->

<div class="info">

<div><strong>ReNr:</strong><?= h($head["ReNr"]) ?>

  </div>

<div><strong>ReDatum:</strong><?= h($head["ReDatum"]) ?>

  </div>

<div><strong>KuNr:</strong><?= h($head["KuNr"]) ?></div>

<div><strong>Kunde:</strong>

<?= h($head["Name"] . ", " . $head["Vorname"]) ?></div>

<div><strong>Rechnungssumme:</strong>

<?= number_format($summe,2,",",".") ?>Euro</div>

</div>

 

<!--

Ausgabe der einzelnen Rechnungspositionen als HTML-Tabelle.

-->

<table class="rel">

<thead>

  <tr>

    <th>PosNr</th>

    <th>ArtNr</th>

    <th>Beschreibung</th>

    <th>Preis</th>

    <th>Anzahl</th>

    <th>Positionswert</th>

  </tr>

</thead>

<tbody>

 

<!--Prüfen, ob Zahl der Positionen Null -->

<?php if (count($pos) === 0): ?>

 

<!--

Dieser Fall sollte bei einer vollständig gespeicherten Rechnung

normalerweise nicht auftreten.

-->

<tr>

  <td colspan="6">Zu dieser Rechnung sind keine Positionen

  vorhanden.</td>

</tr>

<?php else: ?>

<?php foreach ($pos as $p): ?>

 

<!--

Für jede Rechnungsposition wird eine Zeile der Tabelle erzeugt.

-->

<tr>

  <td><?= h($p["PosNr"]) ?></td>

  <td><?= h($p["ArtNr"]) ?></td>

  <td><?= h($p["Beschreibung"]) ?></td>

  <td><?= number_format((float)$p["Preis"],2,",",".") ?></td>

  <td><?= h($p["Anzahl"]) ?></td>

  <td><?= number_format((float)$p["Positionswert"],2,",",".") ?></td>

</tr>

<?php endforeach; ?>

<?php endif; ?>

</tbody>

</table>

 

<!--

Verweise auf die Erfassung einer weiteren Rechnung und

auf die Gesamtausgabe der vier Relationen.

-->

<div class="actions">

<a class="btn" href="KuReArRechnungen.php">Neue Rechnung

    erfassen</a>

<a class="btn" href="KuReAr_5.php">

    Gesamtausgabe (alle vier Relationen)</a>

</div>

</div>

</body>

</html>

7.5.2 Programmlauf

Nicht vergessen: Das Programm wird über KuRaArStart.php angesteuert: http://localhost/kurearStart.php. Siehe auch oben.

Dann wird der Button Rechnungen eingeben angeklickt.

Das führt zur Eingabemaske:

Nach dem Speichern der Rechnung:

Dann die Gesamtausgabe:

7.5.3 Anmerkungen

Die Funktion h() dient dazu, Werte sicher in einer HTML-Seite auszugeben:

function h(mixed $value): string

{

  return htmlspecialchars((string)$value, ENT_QUOTES, "UTF-8");

}

Die erste Zeile function h(mixed $value): string definiert eine Funktion mit dem Namen h.

  • function leitet die Funktionsdefinition ein.
  • h ist der Name der Funktion. Der kurze Name steht hier sinngemäß für "HTML-sichere Ausgabe".
  • $value ist der Parameter. Beim Aufruf wird der auszugebende Wert an die Funktion übergeben.
  • mixed bedeutet, dass der übergebene Wert unterschiedliche Datentypen haben darf, beispielsweise string, int, float oder null.
  • : string legt fest, dass die Funktion als Ergebnis immer eine Zeichenkette zurückgibt.

Die Typumwandlung (string)$value wandelt den übergebenen Wert zunächst in eine Zeichenkette um. Dadurch können auch Zahlen sicher an htmlspecialchars() übergeben werden.

Die Funktion htmlspecialchars(...) wandelt Zeichen um, die in HTML eine besondere Bedeutung haben. Z.B. das kaufmännische Und, das Größer- und Kleiner-Zeichen sowie Anführungszeichen. Vgl. auch den Anhang.

ENT_QUOTES bewirkt, dass sowohl doppelte als auch einfache Anführungszeichen umgewandelt werden. "UTF-8" gibt den verwendeten Zeichensatz an. Dadurch werden beispielsweise Umlaute wie ä, ö und ü korrekt verarbeitet. return gibt die umgewandelte Zeichenkette an die aufrufende Stelle zurück. Die Funktion kann in einer HTML-Ausgabe kurz und übersichtlich aufgerufen werden:

<p>Kunde: <?= h($kunde["Name"]) ?></p>

Sie ist damit eine Hilfsfunktion für die sichere HTML-Ausgabe. Sie schützt allerdings nicht allgemein vor allen Angriffen: Für SQL-Anweisungen müssen weiterhin Prepared Statements verwendet werden.

7.5.4 KuReAr_6_show.css

/*

Mit dem Universal-Selektor * gilt die folgende Festlegung für alle

HTML-Elemente.

box-sizing: border-box bewirkt, dass festgelegte Breiten

bereits die Innenabstände und Rahmen enthalten.

Dadurch wird verhindert, dass ein Element durch padding

oder border breiter wird als vorgesehen.

*/

* {

box-sizing: border-box;

}

/*

1. GESTALTUNG DER GESAMTEN SEITE mit body

Als bevorzugte Schriftart wird Arial verwendet. Falls Arial

nicht verfügbar ist, verwendet der Browser eine andere serifenlose

Schriftart.

Die hellgraue Hintergrundfarbe hebt den weißen Inhaltsbereich

optisch von der Seite ab.

margin: 0 entfernt den standardmäßigen Außenabstand des Browsers.

*/

body {margin: 0; background-color: #f5f5f5;

  font-family: Arial, sans-serif;

}

 

/*

2. GEMEINSAMER CONTAINER FÜR DIE AUSGABE

- width: 100% ermöglicht die Anpassung an kleine Browserfenster.

- max-width: 920px begrenzt die Breite auf den ursprünglich

vorgesehenen Wert von 920 Pixeln.

- margin: 20px auto erzeugt oben und unten einen Abstand von

20 Pixeln und zentriert die Box horizontal.

- padding erzeugt innerhalb der Box einen Abstand von 20 Pixeln.

- overflow-x: auto blendet bei Bedarf eine horizontale

Bildlaufleiste ein. Dies verhindert, dass die Relation bei

kleinen Bildschirmbreiten über den Inhaltsbereich hinausragt.

*/

.box {width: 100%; max-width: 920px; margin: 20px auto;

  padding: 20px; overflow-x: auto; background-color: #fff;

  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);

}

 

/*

3. HINWEISTEXT

Der Hinweis oberhalb der Überschrift wird dunkelrot und fett

dargestellt.

margin erzeugt:

- oben einen Außenabstand von 10 Pixeln

- links und rechts keinen Außenabstand

- unten einen Außenabstand von 8 Pixeln

*/

.caption {margin: 10px 0 8px; color: #c00; font-weight: 700;}

 

/*

4. ÜBERSCHRIFT

Die Überschrift wird horizontal zentriert.

margin erzeugt oben einen Abstand von 6 Pixeln, links und

rechts keinen Abstand und unten einen Abstand von 12 Pixeln.

*/

h2 {margin: 6px 0 12px; text-align: center;}

 

/*

5. BEREICH MIT DEN ALLGEMEINEN RECHNUNGSDATEN

Der Bereich enthält Rechnungsnummer, Rechnungsdatum,

Kundennummer, Kundenname und Rechnungssumme.

border erzeugt einen schwarzen Rahmen mit einer Stärke von

2 Pixeln.

padding erzeugt innerhalb des Bereichs einen Abstand von 10 Pixeln.

margin erzeugt:

- oben einen Außenabstand von 10 Pixeln

- links und rechts keinen Außenabstand

- unten einen Außenabstand von 14 Pixeln

*/

.info {margin: 10px 0 14px; padding: 10px; border: 2px solid #000;

background-color: #fff;}

 

/*

Zwischen den einzelnen Angaben im Informationsbereich wird

ein kleiner vertikaler Abstand eingefügt.

Diese zusätzliche Regel verbessert die Lesbarkeit, ohne die

HTML-Struktur ändern zu müssen.

*/

.info div + div {margin-top: 4px;}

 

/*

6. RELATION MIT DEN RECHNUNGSPOSITIONEN

- border-collapse: collapse führt die Rahmen benachbarter

Zellen zu gemeinsamen Linien zusammen.

- width: 100% bewirkt, dass die Relation die gesamte verfügbare

Breite einnimmt.

- min-width verhindert, dass die sechs Spalten auf kleinen

Bildschirmen zu stark zusammengedrückt werden.

- border erzeugt einen schwarzen Außenrahmen mit einer Stärke

von 3 Pixeln.

- font-size legt eine Schriftgröße von 15 Pixeln fest.

*/

table.rel {width: 100%; min-width: 700px; border: 3px solid #000;

  border-collapse: collapse;font-size: 15px;}

 

/*

7. KOPF- UND DATENZELLEN

Jede Zelle erhält einen schwarzen Rahmen mit einer Stärke

von 2 Pixeln.

padding legt den Innenabstand fest:

- oben und unten: 6 Pixel

- links und rechts: 10 Pixel

Die Inhalte werden zunächst linksbündig ausgerichtet.

*/

table.rel th,table.rel td {padding: 6px 10px;border: 2px solid #000;

  background-color: #fff; text-align: left;}

 

/*

8. TABELLENKOPF

Die Überschriften werden fett dargestellt und erhalten eine

hellgraue Hintergrundfarbe. Dadurch hebt sich der Tabellenkopf von

den Datenzeilen ab.

*/

table.rel th {background-color: #e0e0e0;font-weight: 700;}

/*

9. RECHTSBÜNDIGE AUSRICHTUNG NUMERISCHER WERTE

Rechtsbündig ausgegeben werden:

- 1. Spalte: PosNr

- 2. Spalte: ArtNr

- 4. Spalte: Preis

- 5. Spalte: Anzahl

- 6. Spalte: Positionswert

Die Beschreibung in der 3. Spalte bleibt linksbündig.

*/

table.rel td:nth-child(1), table.rel td:nth-child(2),

table.rel td:nth-child(4), table.rel td:nth-child(5),

table.rel td:nth-child(6)

{

  text-align: right;

}

 

/*

10. HERVORHEBUNG EINER DATENZEILE

Wenn sich der Mauszeiger über einer Datenzeile befindet,

erhält diese eine sehr helle blaue Hintergrundfarbe.

Da die Hintergrundfarbe ursprünglich den einzelnen Zellen

zugewiesen wurde, muss der Hover-Effekt ebenfalls auf die

Zellen angewendet werden.

*/

table.rel tbody tr:hover td {background-color: #f0f8ff;}

 

/*

11. BEREICH MIT DEN VERWEISEN

display: flex ordnet die beiden Verweise nebeneinander an.

gap erzeugt einen Abstand von 10 Pixeln zwischen ihnen.

flex-wrap: wrap erlaubt einen Zeilenumbruch, wenn der

verfügbare Platz nicht ausreicht.

*/

.actions {display: flex; flex-wrap: wrap; gap: 10px;

  margin-top: 12px;}

/*

12. GESTALTUNG DER VERWEISE ALS SCHALTFLÄCHEN

- display: inline-block ermöglicht die Verwendung von

Innenabständen und lässt den Verweis wie eine Schaltfläche

erscheinen.

- text-decoration: none entfernt die übliche Unterstreichung

eines Verweises.

- color legt Schwarz als Schriftfarbe fest.

*/

a.btn {display: inline-block; padding: 6px 10px;

  border: 2px solid #000; background-color: #eee;

    color: #000; text-decoration: none;}

 

/*

13. HERVORHEBUNG EINER SCHALTFLÄCHE

Wenn sich der Mauszeiger über einer Schaltfläche befindet,

wird deren Hintergrund etwas dunkler.

*/

a.btn:hover {background-color: #ddd;}

 

/*

14. BEDIENUNG MIT DER TASTATUR

- :focus-visible hebt einen Verweis hervor, wenn er über die

Tastatur ausgewählt wurde.

- outline liegt außerhalb des Rahmens und beeinträchtigt daher

nicht die Abmessungen der Schaltfläche.

*/

a.btn:focus-visible {outline: 3px solid #06c; outline-offset: 2px;}

 

/*

15. ANPASSUNG AN KLEINE BILDSCHIRME

Bei einer Fensterbreite von höchstens 960 Pixeln wird links

und rechts ein Außenabstand von jeweils 10 Pixeln berücksichtigt.

calc(100% - 20px) zieht diese beiden Abstände von der verfügbaren

Fensterbreite ab.

*/

@media (max-width: 960px) {

  .box {width: calc(100% - 20px); margin: 10px; }

}

Die Programme für die übrigen Buttons sind in Vorbereitung. Hier im Folgenden sozusagen die Platzhalter.

7.6 Sonstige Programme

7.6.1 KuReArArtikel.php

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Artikel bearbeiten</title>

  <link rel="stylesheet" href="KuReAr_5.css">

</head>

<body>

<div class="box">

  <h2>Artikel bearbeiten</h2>

  <p>

    Das Programm zur Bearbeitung der Artikeldaten

    befindet sich in Vorbereitung.

  </p>

</div>

</body>

</html>

7.6.2 KuReArKunden.php

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Kunden bearbeiten</title>

  <link rel="stylesheet" href="KuReAr_5.css">

</head>

<body>

<div class="box">

  <h2>Kunden bearbeiten</h2>

<p>

    Das Programm zur Bearbeitung der Kundendaten

    befindet sich in Vorbereitung.

  </p>

</div>

</body>

</html>

7.6.3 KuReArAusw1.php

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Liste der Rechnungen</title>

  <link rel="stylesheet" href="KuReAr_5.css">

</head>

<body>

<div class="box">

  <h2>Liste der Rechnungen</h2>

  <p>

    Das Programm zur Ausgabe der Rechnungsliste

    befindet sich in Vorbereitung.

  </p>

</div>

</body>

</html>

7.6.4 KuReArAusw2.php

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Liste der Kunden</title>

  <link rel="stylesheet" href="KuReAr_5.css">

</head>

<body>

<div class="box">

<h2>Liste der Kunden</h2>

  <p>

    Das Programm zur Ausgabe der Kundenliste

    befindet sich in Vorbereitung.

  </p>

</div>

</body>

</html>

7.6.5 KuReArAusw3.php

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Artikelbestand</title>

  <link rel="stylesheet" href="KuReAr_5.css">

</head>

<body>

<div class="box">

<h2>Artikelbestand</h2>

  <p>

    Das Programm zur Ausgabe des Artikelbestands

    befindet sich in Vorbereitung.

  </p>

</div>

</body>

</html>

Die Zuordnung der drei Auswertungsprogramme ist damit:

 


Datei

Vorgesehene Ausgabe

KuReArAusw1.php

Liste der Rechnungen

KuReArAusw2.php

Liste der Kunden

KuReArAusw3.php

Artikelbestand

Die Dateien enthalten noch keinen PHP-Code. Die Dateiendung .php ist dennoch sinnvoll, weil die Platzhalter später um Datenbankabfragen ergänzt werden. Durch <div class="box"> wird außerdem bereits die Gestaltung aus KuReAr_5.css genutzt.

8 Kompakte Übersicht

Hier eine Zusammenfassung zu den oben beschriebenen Möglichkeiten der Verknüpfung von Webseite und Datenbank.

8.1 MySQLi

8.1.1 Typischer Ablauf

  • MySQLi-Verbindung aufbauen
  • SQL-Anweisung vorbereiten
  • Parameter binden (bind_param)
  • Anweisung ausführen
  • Ergebnisse abrufen (mit get_result(), fetch_assoc())
  • Fehler prüfen ($con->connect_error, $stmt->error)

8.1.2 Vorteile

  • Native Unterstützung für MySQL-Datenbanken
  • Unterstützung für Prepared Statements
  • Effiziente Parameterbindung
  • Objektorientierte und prozedurale Verwendung
  • Gute Performance für MySQL-spezifische Anwendungen

8.2 PDO

8.2.1 Typischer Ablauf

  • PDO-Verbindung herstellen
  • SQL-Statement vorbereiten
  • Parameter binden (implizit über execute())
  • Statement ausführen
  • Ergebnisse abrufen
  • Fehler behandeln (try/catch)

8.2.2 Vorteile

  • Datenbankunabhängige Schnittstelle
  • Schutz vor SQL-Injection
  • Klare Trennung von SQL und Daten
  • Integrierte Fehlerbehandlung mit Exceptions
  • Unterstützung von benannten Parametern

8.3 Der direkte Vergleich

Datenbankzugriff in PHP: PDO und MySQLi im Überblick

PDO (PHP Data Objects)

MySQLi (MySQL Improved)

Datenbankunabhängige Schnittstelle
(MySQL, PostgreSQL, SQLite, ...)

Speziell für MySQL entwickelt

Verbindung über DSN (Data Source Name)

Verbindung über Host, Benutzer, Passwort, Datenbank

Unterstützt benannte Parameter (:id)

Verwendet positionsbasierte Platzhalter (?)

Parameterübergabe direkt in execute()

Parameterbindung mit bind_param()

Keine explizite Typangabe nötig

Typangaben erforderlich (i, s, ...)

Fehlerbehandlung über Exceptions

Fehlerprüfung meist manuell

Flexible Ergebnisverarbeitung (fetch, fetchAll)

Typisch: get_result, fetch_assoc

Einheitliche API für mehrere DB-Systeme

MySQL-spezifische Optimierung

 

Typischer Ablauf

PDO

MySQLi

1. Verbindung herstellen (new PDO)

1. Verbindung herstellen (new mysqli)

2. Statement vorbereiten (prepare)

2. Statement vorbereiten (prepare)

3. Parameter übergeben (execute)

3. Parameter binden (bind_param)

4. Statement ausführen

4. Statement ausführen (execute)

5. Ergebnisse abrufen (fetch)

5. Ergebnisse abrufen (fetch_assoc)

6. Fehler behandeln (try/catch)

6. Fehler prüfen (error)

 

9 Zusammenspiel HTML und PHP

9.1 Aufgaben

PHP und HTML übernehmen unterschiedliche Aufgaben:

  • HTML beschreibt den Aufbau und die Inhalte der Webseite.
  • PHP verarbeitet Daten, trifft Entscheidungen, greift auf eine Datenbank zu und erzeugt bei Bedarf HTML-Code.
  • Der Webserver führt PHP aus.
  • Der Browser erhält anschließend nur das erzeugte HTML. Der PHP-Code selbst wird nicht an den Browser übertragen.

9.2 Zusammenspiel

Für das Zusammenspiel gibt es mehrere Möglichkeiten.

9.2.1 HTML außerhalb von PHP schreiben

Das ist die einfachste Form. Der größte Teil der Datei besteht aus normalem HTML. PHP wird nur dort eingeschoben, wo dynamische Inhalte benötigt werden.

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Kundendaten</title>

</head>

<body>

  <h1>Relation Kunden</h1>

  <?php

    echo "Die Daten wurden geladen.";

  ?>

</body>

</html>

Diese Form eignet sich gut, wenn die Seite überwiegend aus HTML besteht.

9.2.2 HTML mit echo durch PHP ausgeben

PHP kann vollständige HTML-Anweisungen als Zeichenketten ausgeben:

<?php

echo "<h1>Relation Kunden</h1>";

echo "<p>Die Daten wurden geladen.</p>";

?>

Der Browser erhält daraus:

<h1>Relation Kunden</h1>

<p>Die Daten wurden geladen.</p>

Diese Form ist für kurze HTML-Ausgaben praktisch. Bei längeren HTML-Abschnitten wird sie jedoch schnell unübersichtlich.

Sollen innerhalb einer PHP-Zeichenkette doppelte Anführungszeichen stehen, müssen sie durch einen umgekehrten Schrägstrich (backslash) geschützt werden:

echo "<p class=\"text-red\">Relation Kunden</p>";

Einfacher könnte hier eine außen einfach begrenzte Zeichenkette:

echo '<p class="text-red">Relation Kunden</p>';

9.2.3 Werte mit der Kurzform <?= ... ?> einsetzen

Mit dieser Schreibweise kann ein PHP-Wert unmittelbar in HTML eingesetzt werden:

<p>Kundennummer: <?= $kuNr ?></p>

Dies ist die Kurzform für:

<p>Kundennummer: <?php echo $kuNr; ?></p>

Bei Texten, die aus einem Formular oder einer Datenbank stammen, sollte htmlspecialchars() verwendet werden:

<p>Kundenname:

  <?= htmlspecialchars($name, ENT_QUOTES, "UTF-8") ?>

</p>

In den Programmen dieses Textes wird dafür auch die Hilfsfunktion h() verwendet:

<?php

function h(string $s): string

{

  return htmlspecialchars($s, ENT_QUOTES, "UTF-8");

}

?>

Dann genügt:

<p>Kundenname: <?= h($name) ?></p>

Diese Schreibweise ist besonders gut geeignet, um einzelne Werte in eine HTML-Struktur einzusetzen.

9.2.4 PHP-Bedingungen mit HTML verbinden

Soll HTML nur unter einer bestimmten Bedingung ausgegeben werden, kann eine alternative PHP-Schreibweise verwendet werden:

<?php if ($message !== ""): ?>

  <p><?= h($message) ?></p>

<?php endif; ?>

Das bedeutet: Wenn $message nicht leer ist, wird das HTML-Element <p> ausgegeben. Die gleiche Verarbeitung könnte vollständig in PHP geschrieben werden:

<?php

if ($message !== "") {

echo "<p>" . h($message) . "</p>";

}

?>

Die erste Variante ist meist übersichtlicher, wenn innerhalb der Bedingung viel HTML steht.

9.2.5 PHP-Schleifen mit HTML verbinden

Auch Schleifen können in eine HTML-Struktur eingebettet werden:

<table>

  <tbody>

    <?php foreach ($kunden as $kunde): ?>

      <tr>

      <td><?= h($kunde["Name"]) ?></td>

      <td><?= h($kunde["Vorname"]) ?></td>

      </tr>

    <?php endforeach; ?>

  </tbody>

</table>

PHP wiederholt für jeden Datensatz den dazwischenstehenden HTML-Code. Die Schreibweise

<?php foreach (...): ?>

bedeutet den Beginn der Schleife. Das zugehörige

<?php endforeach; ?>

beendet sie.

Auch diese Schleife könnte ausschließlich mit PHP und echo geschrieben werden:

<?php

echo "<table>";

echo "<tbody>";

foreach ($kunden as $kunde) {

  echo "<tr>";

  echo "<td>" . h($kunde["Name"]) . "</td>";

  echo "<td>" . h($kunde["Vorname"]) . "</td>";

  echo "</tr>";

}

echo "</tbody>";

echo "</table>";

?>

Bei umfangreichen HTML-Strukturen ist die erste Variante meistens leichter lesbar.

9.2.6 Verkettungsoperator

PHP kann HTML-Code aus festen Textteilen und Variablen zusammensetzen:

<?php

  echo "<p>Kundenname: " . h($name) . "</p>";

?>

Der Punkt ist dabei der PHP-Operator zur Verkettung von Zeichenketten:

"Vorname: " . $vorname

Mehrere Textteile werden zu einer Zeichenkette zusammengesetzt. Bei komplexeren HTML-Anweisungen kann diese Form schwer lesbar werden:

echo "<input type=\"number\" name=\"Anzahl_$i\" value=\""

  . h($anzahl) . "\">";

Dann ist eingebettetes PHP oft übersichtlicher:

<input type="number"

  name="Anzahl_<?= $i ?>" value="<?= h($anzahl) ?>">

9.2.7 Ein Formular übergibt HTML-Daten an PHP

Das Zusammenspiel funktioniert auch in der umgekehrten Richtung: HTML erfasst Daten, PHP verarbeitet sie.

HTML-Formular:

<form method="post">

  <label for="name">Name:</label>

  <input type="text" id="name" name="name">

  <button type="submit">Absenden</button>

</form>

PHP übernimmt den eingegebenen Wert:

<?php

$name = $_POST["name"] ?? "";

?>

Entscheidend ist das Attribut: name="name". Dadurch wird der eingegebene Wert unter dem Schlüssel "name" übertragen. PHP greift darauf folgendermaßen zu:

$_POST["name"]

Der gesamte Ablauf lautet:

  • PHP erzeugt beziehungsweise liefert das HTML-Formular.
  • Der Browser zeigt das Formular an.
  • Der Benutzer trägt Daten ein und sendet das Formular ab.
  • Der Browser übermittelt die Daten an den Webserver.
  • PHP liest und verarbeitet die Daten.
  • PHP erzeugt eine neue HTML-Ausgabe.
  • Der Browser stellt diese Ausgabe dar.

9.2.8 PHP-Logik und HTML-Ausgabe trennen

Bei umfangreicheren Programmen ist es sinnvoll, die Verarbeitung am Anfang der Datei und die HTML-Ausgabe danach unterzubringen:

<?php

$message = "";

if ($_SERVER["REQUEST_METHOD"] === "POST") {

  $name = trim($_POST["name"] ?? "");

  if ($name === "") {

    $message = "Bitte geben Sie einen Namen ein.";

  } else {

    $message = "Eingabe wurde verarbeitet.";

  }

}

?>

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Eingabe</title>

</head>

<body>

  <?php if ($message !== ""): ?>

      <p><?= htmlspecialchars($message, ENT_QUOTES, "UTF-8") ?></p>

  <?php endif; ?>

  <form method="post">

    <input type="text" name="name">

    <button type="submit">Absenden</button>

  </form>

</body>

</html>

Insgesamt also:

  • oben: PHP für Datenbankzugriff, Prüfung und Verarbeitung
  • unten: HTML für die Darstellung
  • im HTML: kurze PHP-Einschübe für Werte, Bedingungen und Schleifen

10 Glossar

ACID-Eigenschaften
Grundlegende Eigenschaften von Transaktionen: Atomicity (Atomarität), Consistency (Konsistenz), Isolation (Isolation) und Durability (Dauerhaftigkeit).

Assoziatives Array
Datenstruktur, bei der Werte über Schlüssel angesprochen werden. In Datenbankprogrammen entsprechen die Schlüssel häufig den Spaltennamen eines Datensatzes. Beispiel: ['customer_id' => 1002, 'last_name' => 'Smith']

Attribut
Eigenschaft einer Entität, die in einer Relation als Spalte gespeichert wird.

Benannter Parameter
Platzhalter in einem Prepared Statement, der durch einen Namen gekennzeichnet ist (z. B. :id).
Der Wert wird erst bei der Ausführung zugewiesen.

bind_param()
Methode von MySQLi-Prepared-Statements, mit der Parameter an Platzhalter einer SQL-Anweisung gebunden werden.

Commit
Bestätigung einer Transaktion, durch die alle darin vorgenommenen Änderungen dauerhaft in der Datenbank gespeichert werden.

connect_error
Eigenschaft des MySQLi-Objekts, die anzeigt, ob ein Verbindungsfehler aufgetreten ist.

Connection Object (MySQLi)
Objekt, das die Verbindung zu einer MySQL-Datenbank repräsentiert und mit new mysqli(...) erzeugt wird.

Connection Parameters
Werden durch Host, Benutzername, Passwort und Datenbankname festgelegt.
Beispiel: new mysqli("localhost", "root", "", "Sales")

DSN (Data Source Name)
Die Variable $dsn enthält eine Verbindungsbeschreibung für PDO, bestehend aus: dem Datenbanktreiber, dem Serverhost, dem Datenbanknamen, dem zu verwendenden Zeichensatz. Sie ist damit die zentrale Konfigurationsangabe für den Aufbau einer Datenbankverbindung. Beispiel: $dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";

Entität
Objekt der realen oder gedanklichen Welt, über das Daten gespeichert werden, z. B. ein Kunde oder ein Artikel.

Exception
Objekt, das einen Fehler oder eine Ausnahmesituation im Programm beschreibt. Exceptions können mit try/catch-Blöcken behandelt werden.

execute()
Methode eines Prepared Statements, die eine vorbereitete SQL-Anweisung ausführt.

fetch_assoc()
Methode von MySQLi-Ergebnismengen, die jeweils einen Datensatz als assoziatives Array mit Spaltennamen als Schlüssel liefert.

fetchAll()
PDO-Methode, die alle Datensätze eines Abfrageergebnisses in einem Array zurückgibt.

Fetch-Modus
Legt fest, in welcher Form Abfrageergebnisse zurückgegeben werden.
Beispiel: PDO::FETCH_ASSOC ergibt Ergebnis als assoziatives Array

Fremdschlüssel (Foreign Key)
Attribut oder Attributkombination einer Relation, die auf den Primärschlüssel einer anderen Relation verweist und damit eine Beziehung zwischen Relationen herstellt.

get_result()
Methode, die die Ergebnismenge eines Prepared Statements zurückliefert (setzt den MySQL Native Driver voraus).

Join (Verbund)
Operation zur Verknüpfung mehrerer Relationen über gemeinsame Attributwerte, meist über Primär- und Fremdschlüssel.

Klasse 
Zentrales Konzept der objektorientierten Theorie,
Bauplan für Objekte. Er definiert die Attribute und Methoden.

Kurzschlussauswertung
In den Programmen kommt öfters der UND-Operator && vor. Für ihn gilt: Er bricht die Auswertung ab, sobald eine Teilbedingung false ergibt. Dieses Verhalten wird als Kurzschlussauswertung (englisch: "short-circuit evaluation") bezeichnet. Einen solchen gibt es auch für das logische ODER. Hier endet die Auswertung, sobald eine Teilbedingung true ergibt.

Methode
Funktion, die zu einer Klasse oder einem Objekt gehört und über dieses aufgerufen wird.

MySQLi Object
Repräsentiert die Datenbankverbindung. Wird mit new mysqli(...) erzeugt.

MySQLi (MySQL improved)
PHP-Erweiterung für den Zugriff auf MySQL-Datenbanken. Sie unterstützt objektorientierte und prozedurale Programmierung sowie Prepared Statements.

Named parameter (benannter Parameter)
Ein Platzhalter in einem Prepared Statement, der mit einem Namen (z. B. :reNr) versehen ist und beim Ausführen der Anweisung mit einem konkreten Wert belegt wird.

Null-Koaleszenzoperator
Der Null-Koaleszenzoperator ?? liefert den Wert seines linken Operanden, sofern dieser vorhanden ist und nicht null enthält. Andernfalls liefert er den Wert des rechten Operanden. Er entspricht damit weitgehend einer verkürzten Prüfung mit isset() und wird häufig verwendet, um für nicht vorhandene Formularwerte einen Ersatzwert festzulegen.

Objekt
Instanz einer Klasse, die Daten (Attribute) und Funktionen (Methoden) kapselt.

Parameterbindung
Zuordnung konkreter Werte zu Platzhaltern in Prepared Statements, meist zur Laufzeit des Programms.

PDO (PHP Data Objects)
PHP-Schnittstelle für den Zugriff auf Datenbanken über ein einheitliches objektorientiertes API.

PDOStatement
Objekt, das eine vorbereitete SQL-Anweisung repräsentiert. Bietet Methoden wie execute() und fetchAll().

Prepared Statement
Vorbereitete SQL-Anweisung mit Platzhaltern, die erst beim Ausführen gebunden werden. Dadurch werden SQL-Injection-Angriffe verhindert und Abfragen können effizienter ausgeführt werden.

Primärschlüssel (Primary Key)
Attribut oder Attributkombination, die jeden Datensatz einer Relation (Tabelle) eindeutig identifiziert.

Relation
Tabelle eines relationalen Datenbanksystems mit bestimmten Eigenschaften, bestehend aus Attributen (Spalten) und Tupeln (Zeilen).

Result Set (Ergebnismenge)
Menge der Datensätze, die eine SQL-Abfrage zurückliefert.

Rollback
Rückgängigmachen einer Transaktion, bei dem alle seit ihrem Beginn vorgenommenen Änderungen verworfen werden.

SQL (Structured Query Language)
Standardisierte Sprache zur Definition, Abfrage und Änderung von Daten in relationalen Datenbanksystemen.

SQL Injection
Sicherheitslücke, bei der schädlicher SQL-Code über Eingaben eingeschleust wird. Kann durch Prepared Statements verhindert werden.

Statement
Objekt, das eine SQL-Anweisung repräsentiert und deren Ausführung sowie Ergebnisverarbeitung ermöglicht.

Transaktion (Transaction)
Zusammengehörige Folge von Datenbankoperationen, die als logische Arbeitseinheit ausgeführt wird. Eine Transaktion wird entweder vollständig durchgeführt (Commit) oder vollständig zurückgenommen (Rollback).

try/catch-Block
Programmkonstrukt zur Behandlung von Exceptions. Der try-Block enthält den zu überwachenden Code, während der catch-Block auftretende Fehler verarbeitet.

Tupel
Datensatz einer Relation, dargestellt als Zeile einer Tabelle.

Verbindungsrelation (Junction Table)
Relation, die eine n:m-Beziehung zwischen zwei Relationen realisiert. Sie enthält typischerweise die Fremdschlüssel der beteiligten Relationen als gemeinsamen Primärschlüssel.

View
Virtuelle Relation, deren Inhalt durch eine SQL-Abfrage definiert ist und die selbst keine eigenen Daten speichert.

 

11 Anhang

Vertiefte Erläuterungen zu ausgewählten Konzepten bzw. Konstrukten oder Begriffen.

11.1 Vom Web zur DB

Von der Webseite zur Datenbank

Es gibt im Wesentlichen zwei Ebenen, auf denen man "von den Masken einer Webseite" (HTML-Formulare, Listen, Detailseiten) auf eine relationale Datenbank wie KuReAr zugreift:

  • Serverseitig (klassisch, am häufigsten)
  • Über eine API (modern, entkoppelt; Frontend ruft Endpunkte auf)

Darunter gibt es mehrere konkrete Varianten.

1) Direkt serverseitig mit PHP (Form -> PHP -> DB)

1.1 PHP + PDO

Das Formular sendet Daten an ein PHP-Skript, das per PDO (Prepared Statements) mit MySQL kommuniziert.

KuReAr-Beispiel (Kunden suchen):

  • Maske: Eingabefeld KuNr
  • PHP: SELECT * FROM kunden WHERE KuNr = :id

Vorteile

  • Sehr sicher (Prepared Statements)
  • Flexibel (MySQL, PostgreSQL, ...)
  • Gute Fehlerbehandlung/Transaktionen

Typische Einsatzfälle in KuReAr

  • Kunden anzeigen/ändern
  • Rechnungskopf speichern
  • Rechnungspositionen speichern (Transaktion!)

1.2 PHP + MySQLi

Ähnlich PDO, aber MySQL-spezifisch. Ebenfalls mit Prepared Statements möglich.

Vorteile

  • Direkt für MySQL/MariaDB
  • Schnell, simpel für kleine Projekte

Nachteil: Weniger portabel als PDO

1.3 (Nicht empfohlen) "direkte" SQL-Strings aus Formulardaten

Beispiel:

SELECT * FROM kunden WHERE KuNr = $id

Das führt schnell zu SQL-Injection, wenn nicht sauber validiert/escaped wird.

2) Zugriff über eine API-Schicht (Frontend <--> API <--> DB)

2.1 PHP-API (REST) + Fetch im Browser

Die Web-Maske (HTML/JS) ruft Endpunkte auf, z. B.:

  • GET /api/kunden?kunr=1003
  • POST /api/rechnungen (Rechnungskopf)
  • POST /api/rechnungen/{id}/positionen

Die API-Skripte greifen dann per PDO/MySQLi auf KuReAr zu.

Vorteile

  • Frontend und DB-Logik sauber getrennt
  • Mehrere Frontends möglich (Web, Mobile)
  • Bessere Struktur für größere Projekte

Typisch für KuReAr

  • Kundenliste als JSON laden und in Tabelle rendern
  • Artikel suchen (Autocomplete)
  • Rechnung speichern über mehrere Calls oder als "ein Payload"

2.2 Node.js/Express-API (oder Python/Java) + MySQL-Driver

Nicht PHP, sondern ein Backend-Dienst stellt die API bereit und nutzt einen MySQL-Treiber/ORM.

Vorteile

  • Gute Architektur für Team/Skalierung
  • Moderne Toolchains

Nachteil: Mehr Setup als reines PHP

3) Mit einem ORM (Object-Relational Mapping) statt "SQL von Hand"

Doctrine (PHP), Sequelize (Node), Hibernate (Java) usw.

Dabei wird mit Klassen und Objekten gearbeitet. Das ORM übernimmt die Zuordnung zwischen Objekten und Datenbankrelationen und erzeugt die erforderlichen SQL-Befehle automatisch.

Vorteile

  • Weniger SQL-Handarbeit
  • Beziehungen (z. B. Kunden <--> Adressen über eine junction table wie kuadr/CustomerAddresses) sind komfortabler zu modellieren

Nachteile

  • Einarbeitung erforderlich; viele Abläufe erfolgen automatisch und sind für Einsteiger zunächst schwer nachvollziehbar.
  • Für kleine Projekte wie KuReAr häufig zu aufwendig (Overkill).

4) Architektur-Muster, wie die Masken "an die DB" kommen

4.1 Klassisch: Multi-Page / Post-Redirect-Get (PRG)

  • Formular POST -> PHP speichert in KuReAr -> Redirect -> Anzeige/Bestätigung
  • Sehr robust, passt gut zu KuReAr-Übungen (Kunde anlegen, Rechnung speichern)

4.2 Single-Page-Anteile: AJAX/Fetch

  • Maske bleibt, lädt Daten dynamisch (z. B. Artikel-Suche, Kundenfilter)
  • Speichern weiterhin über API/Endpoints

5) Was bei einer Datenbank wie KuReAr fast immer gebraucht wird (egal welche Variante)

  • Prepared Statements (PDO/MySQLi)
  • Validierung (z. B. KuNr ist Integer, Datum gültig, Menge > 0)
  • Transaktionen bei zusammengesetzten Speichervorgängen
    KuReAr-Beispiel: Rechnung speichern = rechkoepfe + mehrere rechpos -> alles oder nichts.
  • Saubere Modellierung von n:m
    KuReAr-Beispiel: Kunden <--> Adressen über kuadr (Verbindungsrelation, junction table), ohne doppelte Attribute in der Ergebnisliste (JOIN statt Kopieren).

11.2 HTML Special Characters

PHP-Funktion htmlspecialchars()

Die PHP-Funktion htmlspecialchars() wandelt bestimmte Sonderzeichen in HTML-Entitäten um. Dadurch werden sie nicht als HTML-Code interpretiert, sondern als normaler Text angezeigt. Die Funktion wird hauptsächlich verwendet, um zu verhindern, dass HTML- oder JavaScript-Code über ein Formular eingeschleust wird.

Eine typische Anwendung ist, wenn Benutzereingaben aus einem Formular erfasst und ins Programm bzw. in die Datenbank eingegeben werden. Beispiel:

echo htmlspecialchars($_POST['name']);

Folgende Zeichen werden umgewandelt:

Sinnvoll sind noch folgende Ergänzungen:

htmlspecialchars($text, ENT_QUOTES, 'UTF-8');

Der Parameter ENT_QUOTES wandelt auch einfache Anführungszeichen um und UTF-8 präzisiert die Zeichenkodierung.

11.3 Prepared Statement

Ein Prepared Statement gibt die Möglichkeit, SQL-Befehle abgesichert an eine Datenbank zu schicken. Die Grundidee:

  • Zuerst wird das SQL-Statement mit Platzhaltern vorbereitet.
  • Danach werden die konkreten Werte getrennt übergeben.

So werden SQL-Injection-Angriffe verhindert.

Beispiel

In der Relation (Tabelle) kunden der Datenbank KuReAr wird ein Kunde über seine Kundennummer (KuNr) gesucht. Eine unsichere Variante dafür ist:

$id = $_GET['id'];

$sql = "SELECT * FROM kunden WHERE KuNr = $id";

Damit kann ein Angreifer statt z.B. "1003" folgendes eingeben:

1003 OR 1=1

Das würde im SQL-Befehl so aussehen:

SELECT * FROM kunden WHERE KuNr = 1003 OR 1=1

Diese Anweisung liefert alle Kunden, denn 1=1 gilt immer. Solch ein Vorgehen wird SQL-Injection genannt. Das kann durch die Verwendung von Prepared Statements verhindert werden. Dabei unterscheidet die Anwendung zwischen der SQL-Struktur und den Werten (Attributsausprägungen) und es ist festgelegt, dass die eingegebenen Werte die Struktur (des Befehls) nicht mehr verändern können.

Variante 1: Mit PDO

  • Schritt 1 - SQL vorbereiten (mit Platzhalter)

$stmt = $pdo->prepare(

"SELECT * FROM kunden WHERE KuNr = :id");

:id ist ein Platzhalter.

  • Schritt 2 - Wert einsetzen

$stmt->execute(['id' => $id]);

Erst jetzt wird der Wert gebunden.

  • Schritt 3 - Ergebnis holen

$row = $stmt->fetch(PDO::FETCH_ASSOC);

Die Datenbank bekommt also intern zwei Dinge:

  • Struktur: SELECT * FROM kunden WHERE KuNr = ?
  • Wert (Attributsausprägung): 1003

Der Wert wird nie als Teil des SQL-Codes interpretiert, sondern nur als Datenwert.

Variante 2: Mit MySQLi

Hier gibt es zwei Schritte mehr:

  • 1. Vorbereiten

$stmt = $con->prepare(

  "SELECT * FROM kunden WHERE KuNr = ?"

);

Das Fragezeichen ist der Platzhalter für den später einzutragenden Wert.

  • 2. Parameter binden

$stmt->bind_param("i", $id);

Das "i" bedeutet: integer. Möglich wären auch s (string), d (double), b (blob)

  • 3. Ausführen

$stmt->execute();

Betrachten wir als Beispiel das Speichern einer neuen Rechnung in KuReAr:

$stmt = $pdo->prepare(

  "INSERT INTO rechkoepfe (RechNr, KuNr, Datum)

      VALUES (:rnr, :knr, :datum)");

$stmt->execute([

  'rnr'=> $rechNr,

  'knr'=> $kuNr,

  'datum' => $datum ]);

Es gilt also: Nie Benutzereingaben direkt in den SQL-Befehl einbauen - immer Prepared Statements verwenden.

11.4 Exception auslösen

Zum Begiff Exception (Ausnahme)
Eine Exception ist ein Objekt, das einen außergewöhnlichen Zustand oder Fehler während der Programmausführung beschreibt. Es enthält unter anderem eine Fehlermeldung und Informationen darüber, wo und warum der Fehler aufgetreten ist.

Die Option PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION legt fest, dass PDO bei Datenbankfehlern eine Exception auslöst. Betrachten wir das genauer:

$pdo = new PDO(

    "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4",

    "root",

    "",

    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]

);

Was bedeutet das?

PDO::ATTR_ERRMODE ist eine Konstante der Klasse PDO. Deshalb auch die Großbuchstaben. Diese werden in diesem Kontext für Konstanten verwendet. Sie steht für die Einstellung "Fehlerbehandlungsmodus". Damit wird festgelegt, wie PDO auf Fehler reagiert.

PDO::ERRMODE_EXCEPTION ist ebenfalls eine Klassenkonstante. Sie bedeutet: Bei einem Fehler wird eine Exception (Ausnahme, Fehlermeldung) ausgelöst.

Was passiert dadurch konkret? Wenn z. B. ein SQL-Fehler auftritt:

$pdo->query("SELECT * FROM nicht_existierende_tabelle");

Dann geschieht folgendes:

  • Ohne ERRMODE_EXCEPTION liefert PDO nur einen Fehlercode und das Programm läuft weiter.
  • Mit ERRMODE_EXCEPTION "wirft" PDO automatisch eine PDOException, der Code "springt" in den Catch-Block so dass das Programm kontrolliert auf den Fehler reagiert.

Beispiel:

try {

  $pdo->query("SELECT * FROM nicht_existierende_tabelle");

  } catch (PDOException $e) {

  echo "Fehler: " . $e->getMessage();

}

11.5 Variable $con

$con ist eine Variable, die auf ein Objekt (eine Instanz) der Klasse mysqli verweist. Dieses Objekt entsteht durch den Aufruf von new mysqli(...). Es verwaltet die Verbindung zum MySQL-Server und stellt Methoden sowie Eigenschaften bereit, mit denen PHP-Programme Datenbankoperationen durchführen können. Über die Variable $con kann auf diese Methoden und Eigenschaften zugegriffen werden.

Etwas technischer formuliert: $con verweist auf ein Objekt (eine Instanz) der Klasse mysqli. Dieses Objekt entsteht durch den Aufruf des Konstruktors new mysqli(...) und kapselt die Verbindung zum MySQL-Server einschließlich der dafür erforderlichen Informationen und Funktionen.

Wichtige Eigenschaften

$con->connect_error

Häufig verwendete Methoden

  • $con->prepare()
  • $con->query()
  • $con->commit()
  • $con->begin_transaction()
  • $con->rollback()
  • $con->close()

Beispiel

$con = new mysqli("localhost", "root", "", "KuReAr");

Dabei geschieht Folgendes:

  • new erzeugt ein neues Objekt der Klasse mysqli.
  • Der Konstruktor baut die Verbindung zum angegebenen MySQL-Server auf und authentifiziert den Benutzer.
  • Das Objekt enthält den aktuellen Zustand dieser Verbindung sowie die zugehörigen Methoden und Eigenschaften.
  • Die Variable $con speichert einen Verweis auf dieses Objekt und ermöglicht dadurch den Zugriff auf dessen Methoden und Eigenschaften.

11.6 Array von Arrays

Mehrdimensionales Array, Ein Array von assoziativen Arrays

Die äußere Struktur sieht recht vertraut aus:

$positionen = [ ... ];

Das ist ein normales PHP-Array. Es enthält 3 Elemente. Die Indizes sind automatisch numerisch: 0, 1, 2

Es gibt aber noch die innere Struktur: Jedes Element ist wiederum ein assoziatives Array:

["posnr" => 1, "artnr" => 100, "anzahl" => 2]

Das bedeutet:

  • Schlüssel = String ("posnr", "artnr", "anzahl")
  • Werte = Integer

Der Datentyp ist array<int, array<string, int>>. Das heißt:

  • Äußeres Array: numerisch indiziert
  • Inneres Array: String-Schlüssel -> Integer-Werte

Das ist hier sinnvoll,weil eine Liste von Rechnungspositionen modelliert wird. Jede Position hat Attribute wie:

  • Positionsnummer
  • Artikelnummer
  • Anzahl

Das entspricht konzeptionell einer kleinen "Tabelle im Speicher".

Zugriff im Code

foreach ($positionen as $p) {

  echo $p["artnr"];

}

$p ist jeweils ein assoziatives Array.

11.7 Klasse mysqli

Die Klasse mysqli stellt zahlreiche Eigenschaften (Properties) und Methoden zur Verfügung. Über die Eigenschaften können Informationen über den Zustand der Datenbankverbindung oder über das Ergebnis der zuletzt ausgeführten SQL-Anweisung abgefragt werden.

In der Praxis werden jedoch nur wenige Eigenschaften regelmäßig benötigt. Die wichtigsten werden im Folgenden vorgestellt.

Eigenschaften beim Verbindungsaufbau

Nach dem Aufbau einer Datenbankverbindung kann geprüft werden, ob dabei ein Fehler aufgetreten ist.

connect_errno liefert die Fehlernummer eines Verbindungsfehlers:

if ($con->connect_errno) {

  echo $con->connect_errno;

}

kann folgende Ergebnisse zeitigen:

  • 0: Die Verbindung wurde erfolgreich aufgebaut.
  • Ungleich 0: Es ist ein Fehler beim Verbindungsaufbau aufgetreten.

connect_error liefert den zugehörigen Fehlertext.

echo $con->connect_error;

Diese Eigenschaft wird häufig verwendet, um die Ursache eines Verbindungsfehlers anzuzeigen.

Eigenschaften nach SQL-Anweisungen

Nach der Ausführung einer SQL-Anweisung können weitere Eigenschaften ausgewertet werden.

  • errno liefert die Fehlernummer der zuletzt ausgeführten SQL-Anweisung mit echo $con->errno;
  • error liefert den Fehlertext der zuletzt ausgeführten SQL-Anweisung mit echo $con->error;

Diese beiden Eigenschaften sind beim Testen und Debuggen von Programmen hilfreich.

Eigenschaften für Änderungen an der Datenbank

affected_rows gibt an, wie viele Datensätze durch eine INSERT-, UPDATE- oder DELETE-Anweisung betroffen waren. Abfrage mit echo $con->affected_rows; Damit kann beispielsweise überprüft werden, ob eine Änderung tatsächlich durchgeführt wurde.

insert_id liefert die automatisch vergebene AUTO_INCREMENT-Nummer des zuletzt eingefügten Datensatzes. Abfrage mit echo $con->insert_id; Diese Eigenschaft ist besonders wichtig, wenn zunächst ein Datensatz in einer übergeordneten Relation (z. B. Rechnungsköpfe, InvoiceHeaders) gespeichert und anschließend dazugehörige Datensätze (z. B. Rechnungspositionen, InvoiceItems) eingefügt werden sollen.

Weitere Eigenschaften

Die Klasse mysqli besitzt darüber hinaus zahlreiche weitere Eigenschaften, beispielsweise:

  • server_info - Version des MySQL-Servers
  • host_info - Informationen über den Server bzw. Host
  • protocol_version - verwendete Protokollversion
  • thread_id - Kennung des aktuellen Server-Threads

Diese Eigenschaften werden vor allem für Verwaltungs- und Diagnosezwecke verwendet und spielen in den meisten Anwendungsprogrammen nur eine untergeordnete Rolle.

Zusammenfassung

Für die meisten PHP-Anwendungen reichen bereits wenige Attribute der Klasse mysqli aus:

 


Eigenschaft

Bedeutung

connect_error

Fehlermeldung beim Verbindungsaufbau

connect_errno

Fehlernummer beim Verbindungsaufbau

error

Fehlermeldung der zuletzt ausgeführten SQL-Anweisung

errno

Fehlernummer der zuletzt ausgeführten SQL-Anweisung

affected_rows

Anzahl der geänderten Datensätze

insert_id

AUTO_INCREMENT-Wert des zuletzt eingefügten Datensatzes

 

11.8 Referenzieren

Sehr oft taucht in Texten zu PHP, insbesondere zu PHP-Objekten, das Wort referenzieren auf. Z.B. so:

new PDO() erzeugt ein Objekt der Klasse PDO. Die Variable referenziert dieses Verbindungsobjekt.

Da kann leicht der Eindruck entstehen, dass die Variable das Objekt enthält. Dem ist aber nicht so. Bei PHP-Objekten enthält die Variable einen sogenannten Objektbezeichner, über den auf das Objekt zugegriffen wird. Deshalb wird hier und in ähnlichen Situationen die folgende Formulierung gewählt:

new PDO() erzeugt ein Objekt der Klasse PDO. Die Variable $pdo verweist auf dieses Verbindungsobjekt.

11.9 isset()

Weil in den obigen Beispielen viele Formulare vorkommen und deshalb auch die Funktion isset(), hier eine Erläuterung dazu.

Die PHP-Funktion isset() prüft, ob eine Variable vorhanden ist und nicht den Wert null enthält. Die allgemeine Schreibweise lautet: isset($variable)

Die Funktion liefert einen booleschen Wert:

  • true - die Variable ist vorhanden und enthält nicht null
  • false - die Variable ist nicht vorhanden oder enthält null

Einfaches Beispiel

$name = "Meier";

if (isset($name)) {

  echo "Die Variable ist vorhanden.";

}

Da $name vorhanden ist und den Wert "Meier" enthält, liefert isset($name) den Wert true.

Variable mit dem Wert null

$name = null;

if (isset($name)) {

  echo "Die Variable ist vorhanden.";

}

Obwohl die Variable angelegt wurde, liefert isset($name) den Wert false, weil sie null enthält.

Nicht vorhandene Variable

if (isset($ort)) {

  echo "Die Variable ist vorhanden.";

}

Wenn $ort vorher nicht angelegt wurde, liefert isset($ort) ebenfalls false. Dabei erzeugt isset() keine Warnung wegen einer nicht vorhandenen Variablen.

Verwendung bei Formulardaten

isset() wird häufig verwendet, um zu prüfen, ob ein bestimmtes Formularfeld übermittelt wurde:

if (isset($_POST["posCount"])) {

  $posCount = (int)$_POST["posCount"];

}

Das bedeutet: Wenn im Array $_POST ein Element mit dem Schlüssel "posCount" vorhanden ist und dessen Wert nicht null ist, wird der übermittelte Wert in eine ganze Zahl umgewandelt und der Variablen $posCount zugewiesen.

Ohne die Prüfung könnte der Zugriff

$_POST["posCount"]

eine Warnung erzeugen, wenn das Formularfeld nicht übermittelt wurde.

Verwendung bei einem PDO-Objekt

Im Programm KundenEintragenMySQLi.php steht:

if (isset($pdo) && $pdo->inTransaction()) {

  $pdo->rollBack();

}

Hier prüft isset($pdo), ob die Variable $pdo vorhanden ist und nicht null enthält. Diese Prüfung ist erforderlich, weil bereits der Verbindungsaufbau fehlschlagen könnte:

$pdo = new PDO(...);

Wenn dabei eine Ausnahme ausgelöst wird, wurde $pdo möglicherweise noch kein PDO-Objekt zugewiesen. Deshalb darf

$pdo->inTransaction()

nur aufgerufen werden, wenn $pdo tatsächlich vorhanden ist. Durch die Kurzschlussauswertung des UND-Operators && wird die zweite Bedingung nur geprüft, wenn isset($pdo) den Wert true liefert.

Prüfung mehrerer Variablen

isset() kann mehrere Variablen gleichzeitig prüfen:

if (isset($name, $vorname, $ort)) {

  echo "Alle Variablen sind vorhanden.";

}

Das Ergebnis ist nur dann true, wenn alle drei Variablen vorhanden sind und keine davon null enthält.

Somit gilt:

isset() prüft, ob eine Variable oder ein Element eines Arrays vorhanden ist und nicht den Wert null enthält. Die Funktion liefert in diesem Fall true, andernfalls false. Sie wird häufig eingesetzt, bevor auf möglicherweise nicht vorhandene Formulardaten zugegriffen wird.

11.10 Der Null-Koaleszenzoperator

Der Null-Koaleszenzoperator ?? prüft, ob der Ausdruck auf seiner linken Seite vorhanden ist und nicht den Wert null hat. Die allgemeine Form lautet: $linkerAusdruck ?? $ersatzwert. Das Ergebnis ist:

  • der linke Ausdruck, wenn er vorhanden und nicht null ist
  • andernfalls der rechte Ausdruck als Ersatzwert

Beispiel:

$name = $_POST["name"] ?? "";

Das bedeutet: Wenn im Array $_POST das Element mit dem Schlüssel "name" vorhanden ist und nicht null enthält, wird dessen Wert verwendet. Andernfalls wird die leere Zeichenkette "" verwendet.

Gleichbedeutende Schreibweise mit isset()

Die Anweisung $name = $_POST["name"] ?? ""; entspricht weitgehend:

if (isset($_POST["name"])) {

  $name = $_POST["name"];

} else {

  $name = "";

}

Sie kann auch mit dem Bedingungsoperator geschrieben werden:

$name = isset($_POST["name"]) ? $_POST["name"] : "";

Der Null-Koaleszenzoperator ist somit eine kurze Schreibweise für eine Prüfung mit isset().

Nicht mit einer Prüfung auf "leer" verwechseln

Der Operator ?? prüft nicht, ob ein Wert leer ist. Er prüft nur, ob der linke Ausdruck vorhanden und nicht null ist. Diese Werte werden daher übernommen:

0

""

"0"

false

[]

Verwendung bei Formulardaten

In obigen Programmen findet sich beispielsweise:

$action = $_POST["action"] ?? "";

Beim ersten Aufruf der Seite wurde noch kein Formular abgesendet. Das Element $_POST["action"] ist dann möglicherweise nicht vorhanden. Durch ?? erhält $action trotzdem einen definierten Wert: "" Nach dem Absenden kann $_POST["action"] beispielsweise save enthalten. Dann wird dieser Wert übernommen: $action = "save";

Kurzschlussauswertung

Der Operator ?? arbeitet mit einer Kurzschlussauswertung. Der rechte Ausdruck wird nur ausgewertet, wenn der linke Ausdruck nicht vorhanden ist oder null ergibt.

$wert = $vorhanden ?? ersatzwertErmitteln();

Wenn $vorhanden einen Wert ungleich null enthält, wird die Funktion ersatzwertErmitteln() nicht aufgerufen.

11.11 Joins

Die obigen INNER JOIN-Anweisungen sorgen manchmal für Verwirrung. Deshalb hier eine kurze Erinnerung an die Joins der relationalen Theorie.

Im obigen Beispiel stellen die Inner Joins die Beziehungen zwischen diesen Relationen her:

FROM RechKoepfe AS rk

  INNER JOIN Kunden AS k ON rk.KuNr = k.KuNr

  INNER JOIN RechPos AS rp ON rk.ReNr = rp.ReNr

  INNER JOIN Artikel AS a ON rp.ArtNr = a.ArtNr

Rechnungsköpfe mit Kunden verbinden

INNER JOIN Kunden AS k ON rk.KuNr = k.KuNr

In RechKoepfe steht die Kundennummer. Der Kundenname steht dagegen in Kunden. Deshalb werden beide Relationen über KuNr verbunden. Dadurch können diese Angaben gemeinsam ausgegeben werden:

k.KuNr

k.Name AS KuName

rk.ReNr

Rechnungsköpfe mit Rechnungspositionen verbinden

INNER JOIN RechPos AS rp ON rk.ReNr = rp.ReNr

Ein Rechnungskopf enthält die allgemeinen Rechnungsdaten. Die einzelnen Positionen stehen in RechPos. Beide Relationen werden über die Rechnungsnummer verbunden. Dadurch stehen zu jeder Rechnung die zugehörigen Positionen zur Verfügung:

rk.ReNr

rp.PosNr

rp.Anzahl

Rechnungspositionen mit Artikeln verbinden

INNER JOIN Artikel AS a ON rp.ArtNr = a.ArtNr

In RechPos steht die Artikelnummer. Die Beschreibung und der Preis stehen dagegen in Artikel. Deshalb werden beide Relationen über ArtNr verbunden. Dadurch können diese Werte ausgegeben werden:

a.ArtNr

a.Beschreibung

a.Preis

Muss es ausdrücklich INNER JOIN heißen?

Das Wort INNER kann weggelassen werden. Diese beiden Schreibweisen bedeuten dasselbe:

INNER JOIN Kunden AS k ON rk.KuNr = k.KuNr

JOIN Kunden AS k ON rk.KuNr = k.KuNr

JOIN allein wird standardmäßig als INNER JOIN behandelt: D.h. wird JOIN ohne weiteren Zusatz verwendet, ist damit standardmäßig ein INNER JOIN gemeint. Beide Schreibweisen sind daher gleichwertig. Die ausführliche Bezeichnung INNER JOIN macht jedoch deutlicher, dass nur Datensätze mit übereinstimmenden Verknüpfungswerten in das Ergebnis aufgenommen werden.

Warum gerade ein INNER JOIN?

Ein INNER JOIN liefert nur Datensätze, für die auf beiden Seiten der Verknüpfung passende Werte vorhanden sind. Eine Rechnungsposition wird also nur ausgegeben, wenn

  • zur Rechnung ein Kunde vorhanden ist,
  • zur Rechnung mindestens eine Rechnungsposition vorhanden ist und
  • zur Rechnungsposition ein Artikel vorhanden ist.

Das ist bei korrekt eingerichteten Fremdschlüsselbeziehungen normalerweise erwünscht.

Wann wäre ein LEFT JOIN sinnvoller?

Wenn auch Rechnungen ohne Rechnungspositionen ausgegeben werden sollen, müsste die Verbindung zu RechPos mit LEFT JOIN erfolgen:

LEFT JOIN RechPos AS rp ON rk.ReNr = rp.ReNr

Soll außerdem eine Rechnungsposition auch dann erscheinen, wenn der zugehörige Artikel fehlt, müsste auch die nächste Verbindung ein LEFT JOIN sein:

LEFT JOIN Artikel AS a ON rp.ArtNr = a.ArtNr

Eine mögliche Fassung wäre:

SELECT k.KuNr, k.Name AS KuName, rk.ReNr,

  DATE_FORMAT(rk.ReDatum, '%d.%m.%Y') AS RechnDatum,

  rp.PosNr, a.ArtNr, a.Beschreibung, a.Preis, rp.Anzahl

FROM RechKoepfe AS rk

INNER JOIN Kunden AS k ON rk.KuNr = k.KuNr

LEFT JOIN RechPos AS rp ON rk.ReNr = rp.ReNr

LEFT JOIN Artikel AS a ON rp.ArtNr = a.ArtNr

ORDER BY rk.ReNr, rp.PosNr

Für eine Liste vollständiger Rechnungen ist die ursprüngliche Fassung mit drei INNER JOIN-Verknüpfungen aber sachlich richtig.

Es gibt auch noch den Right Join, der entsprechend funktioniert.