14.11.26 01:02 Uhr

[ab 2026/11] Aufrufe: 187, Hits: 505

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

Copyright 2024, 2025, 2026 Josef L. Staud

Autor: Josef L. Staud

Stand: November 2026

Umfang des gedruckten Textes: ca. 150 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

In diesem Text 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

 

Die ganze Datenbank in einer Relation

select rk.renr, rk.kunr, rk.ReDatum, k.name, k.vorname, rp.posnr, rp.artnr, a.beschreibung, a.preis

  from rechkoepfe rk, kunden k, rechpos rp, artikel a

    where rk.kunr = k.kunr

    AND rk.ReNr=rp.ReNr

    AND rp.ArtNr=a.ArtNr

order by k.KuNr, rp.ReNr;


renr

kunr

ReDatum

name

vorname

posnr

artnr

beschreibung

preis

23012

1002

2023-03-28

Hinterhof

Rudolf

3

102

Entwickler

3200.00

23012

1002

2023-03-28

Hinterhof

Rudolf

1

100

Gamer-PC

2500.00

23012

1002

2023-03-28

Hinterhof

Rudolf

2

101

Büro-PC

1200.00

23010

1009

2026-05-20

Vater

Paul

1

101

Büro-PC

1200.00

23051

1009

2026-09-07

Vater

Paul

3

102

Entwickler

3200.00

23051

1009

2026-09-07

Vater

Paul

2

100

Gamer-PC

2500.00

23051

1009

2026-09-07

Vater

Paul

1

554

SmartPh

330.00

23002

1010

2026-03-28

Anger

Albert

3

102

Entwickler

3200.00

23002

1010

2026-03-28

Anger

Albert

1

100

Gamer-PC

2500.00

23002

1010

2026-03-28

Anger

Albert

2

101

Büro-PC

1200.00

23052

9999

2026-09-07

Presley

Elvis

1

102

Entwickler

3200.00

23053

9999

2026-09-11

Presley

Elvis

1

101

Büro-PC

1200.00

 

Alternativ kann die folgende INNER JOIN - Variante dieses Befehls benutzt werden:

SELECT rk.ReNr, rk.KuNr, rk.ReDatum, k.Name, k.Vorname,

    rp.PosNr, rp.ArtNr, a.Beschreibung, a.Preis

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 k.KuNr, rp.ReNr;

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.

1.3 Kommentare

Einige Programme sind sehr ausführlich mit Kommentaren versehen, um Einsteigern oder Wiederholern die Arbeit zu erleichtern. Damit bei der Wiederholung einer spezifischen Programmstelle nicht in anderen Programmen nach einer entsprechenden Stelle gesucht werden muss, sind die Kommentare oftmals wiederholt. Denn das ist ein Vorteil der Veröffentlichung auf Webseiten: Platz ist genug da.

Trotz der vielen Kommentare und der typografischen Ausgestaltung gilt, dass die jeweiligen Programme so wie angegeben lauffähig sind. Benötigt wird nur der lokale Server mit XAMPP und MySQL.

 

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
  • Festgelegter Suchbegriff im Programm
  • Direkter Zugriff auf die Datenbank
  • Genutzte Klassen: mysqli, mysqli_stmt und mysqli_result
  • Formatierte Ausgabe im Browser

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 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 PHP-Variablen an die Platzhalter eines MySQLi-Prepared-Statements. Das Zeichen & vor dem Variablennamen kennzeichnet die Übergabe als Referenz. bind_param() bindet daher Variablen und nicht nur deren gegenwärtige Werte. Werden die Variablen vor einem späteren Aufruf von execute() geändert, verwendet das Statement ihre dann aktuellen Werte. Deshalb müssen Variablen und dürfen keine unmittelbar angegebenen Werte oder Ausdrücke ü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-Abschnitts.

*/

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 Daten in Relationale Datenbanken eingetragen werden. Zuerst am Beispiel MySQLi, dann mit PDO.

3.1 KundenEintragenMySQLi.php

3.1.1 Merkmale des Programms

  • Die Kundendaten werden über ein HTML-Formular eingegeben.
  • Das Formular wird mit der HTTP-Methode POST an dasselbe PHP-Programm übermittelt.
  • Das PHP-Programm wird nur dann aktiv, wenn das Formular abgeschickt wurde.
  • Kundennummer, Name und Vorname werden aus dem POST-Array übernommen.
  • Die Eingabewerte werden auf grundlegende Plausibilität geprüft.
  • Der Zugriff auf die Datenbank erfolgt mit der Klasse mysqli.
  • Für das Einfügen des Datensatzes wird ein Prepared Statement verwendet.
  • Die eingegebenen Werte werden mit bind_param() an die Platzhalter der SQL-Anweisung gebunden.
  • Nach dem Einfügen wird eine Erfolgs- oder Fehlermeldung ausgegeben.
  • Die Meldung wird vor der HTML-Ausgabe mit htmlspecialchars() behandelt.
  • Das Statement und die Datenbankverbindung werden nach ihrer Verwendung geschlossen.

3.1.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 = "";

 

// 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()" 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.3 Programmlauf

Nach dem Programmstart erscheint eine leere Eingabemaske, die befüllt werden kann.

Nach dem Betätigen der Schaltfläche "Speichern" wird das Programm erneut aufgerufen. Wenn die Eingaben gültig sind und der Datensatz erfolgreich gespeichert wurde, erscheint wieder eine leere Eingabemaske. Unterhalb der Maske wird die Erfolgsmeldung "Datensatz wurde eingefügt." angezeigt.

Ein Blick auf die Relation kunden mit phpMyAdmin zeigt den neu eingefügten Datensatz:

3.1.4 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.

Das Beispiel verdeutlicht außerdem das Zusammenspiel von HTML, PHP und SQL:

  • HTML stellt das Eingabeformular bereit.
  • PHP übernimmt und prüft die Formulardaten und steuert den Programmablauf.
  • SQL fügt den neuen Datensatz in die Relation kunden ein.
  • MySQLi stellt die Verbindung zwischen dem PHP-Programm und der MySQL- beziehungsweise MariaDB-Datenbank her.

3.2 RechnungSpeichernMySQLi.php

3.2.1 Merkmale des Programms

  • Speichern zusammengehöriger Daten in mehreren miteinander verknüpften Relationen
  • Speichern eines Rechnungskopfes und mehrerer Rechnungspositionen
  • Verwendung einer Transaktion
  • Sicherung des Alles-oder-nichts-Prinzips
  • Datenbankzugriff mit der Klasse mysqli
  • Verwendung von Prepared Statements
  • Bindung von Werten mit bind_param()
  • Mehrfache Ausführung eines vorbereiteten Statements
  • Speicherung der Positionsdaten in einem mehrdimensionalen Array
  • Verarbeitung der Rechnungspositionen mit einer foreach-Schleife
  • Beachtung von Primärschlüsseln und Fremdschlüsseln
  • Fehlerbehandlung mit try, catch und finally
  • Zurücksetzen der Transaktion mit rollback() im Fehlerfall
  • Freigabe der verwendeten Ressourcen mit close()
  • Festlegung der Rechnungsdaten unmittelbar im PHP-Programm, also ohne HTML-Formular

3.2.2 Aufgabe

Das Programm trägt eine neue Rechnung in die Datenbank KuReAr ein. Gespeichert werden der Rechnungskopf in der Relation RechKoepfe und die zugehörigen Rechnungspositionen in der Relation RechPos.

Transaktionen

Der Rechnungskopf enthält die Rechnungsnummer, die Kundennummer und das Rechnungsdatum. Die Kundennummer verweist als Fremdschlüssel auf einen bereits vorhandenen Datensatz in der Relation Kunden. Ein neuer Kundendatensatz wird durch dieses Programm nicht angelegt.

Eine Rechnung bildet mit ihren Rechnungspositionen eine fachliche Einheit. Deshalb müssen der Rechnungskopf und sämtliche Rechnungspositionen entweder vollständig oder gar nicht gespeichert werden. Dazu wird eine Transaktion verwendet.

Tritt während des Speicherns einer Position ein Fehler auf, darf auch der zuvor eingefügte Rechnungskopf nicht erhalten bleiben. In diesem Fall werden alle seit Beginn der Transaktion vorgenommenen Änderungen mit rollback() zurückgenommen. Nur wenn sämtliche Datenbankoperationen erfolgreich waren, werden die Änderungen mit commit() dauerhaft gespeichert.

Die Rechnungsdaten werden in diesem Beispiel unmittelbar im PHP-Programm festgelegt. Eine Eingabe über ein HTML-Formular findet nicht statt.

Die neue Rechnung hat die Rechnungsnummer 23012 und gehört zum Kunden mit der Kundennummer 1002. Sie enthält drei Positionen mit den Artikelnummern 100, 101 und 102.

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();

  }

}

?>

3.2.4 Programmlauf

Nach einem erfolgreichen Programmlauf erscheint folgende Meldung:

Rechnung erfolgreich gespeichert.

Eine anschließende Kontrollabfrage zeigt den neu eingefügten Rechnungskopf und seine drei Rechnungspositionen.

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

  • Eingabe eines neuen Kundendatensatzes über ein HTML-Formular
  • Übermittlung der Formulardaten mit der HTTP-Methode POST
  • Verarbeitung des Formulars durch dasselbe PHP-Programm
  • Übernahme und Prüfung der eingegebenen Kundendaten
  • Datenbankzugriff über PDO
  • Auswahl des MySQL-Treibers durch den DSN
  • Verwendung eines Prepared Statements
  • Verwendung benannter Platzhalter
  • getrennte Übergabe der SQL-Anweisung und der einzusetzenden Werte
  • Schutz vor SQL-Injection
  • Fehlerbehandlung mit try und catch
  • Auslösen von Ausnahmen durch PDO::ERRMODE_EXCEPTION
  • sichere HTML-Ausgabe der Rückmeldung mit htmlspecialchars()
  • browserseitige und serverseitige Prüfung der Eingaben
  • keine Transaktion erforderlich, da nur ein einzelner Datensatz mit einer einzigen SQL-Anweisung gespeichert wird

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 Adresse gesendet, unter der das Formular aufgerufen wurde. Dadurch verarbeitet dieses PHP-Programm die Formulardaten selbst.

-->

 

<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 Programmlauf

Zum Nachvollziehen des Programmlaufs wird zunächst mit KundenEintragenMySQLi.php ein neuer Kunde angelegt. Seine Kundennummer darf in keinem Rechnungskopf vorkommen. Z.B. so:

Nach dem Aufruf von KundenLoeschenMySQLi.php erscheint eine Eingabemaske, in die die Kundennummer des zu löschenden Kunden eingetragen wird. Z.B. so:

Nach dem Betätigen der Schaltfläche "Löschen" sind drei Ergebnisse möglich:

  • Ist der Kundendatensatz vorhanden und wird seine Kundennummer von keinem Rechnungskopf verwendet, wird der Datensatz gelöscht. Es erscheint die Meldung:

Datensatz wurde gelöscht.

  • Ist kein Kunde mit der angegebenen Kundennummer vorhanden, erscheint die Meldung:

Kein Datensatz mit dieser Kundennummer gefunden.

  • Wird die Kundennummer noch in der Relation RechKoepfe als Fremdschlüsselwert verwendet, verhindert das Datenbanksystem das Löschen. Das Programm gibt eine entsprechende Fehlermeldung aus.

Ein anschließender Blick auf die Relation kunden mit phpMyAdmin zeigt, ob der Datensatz gelöscht wurde.

3.3.4 Anmerkungen

  • PDO ist eine weitgehend einheitliche Schnittstelle für den Zugriff auf unterschiedliche Datenbanksysteme. Durch mysql: im DSN wird in diesem Programm der MySQL-Treiber ausgewählt. Für ein anderes Datenbanksystem müssten der DSN und gegebenenfalls auch die verwendeten SQL-Anweisungen angepasst werden.
  • Im Unterschied zur vorherigen MySQLi-Fassung wird der Zeichensatz bereits im DSN mit charset=utf8mb4 festgelegt.
  • new PDO() erzeugt ein Objekt der Klasse PDO. Die Variable $pdo verweist auf dieses Verbindungsobjekt.
  • PDO verwendet hier benannte Platzhalter wie :kuNr, :name und :vorname. Dadurch ist unmittelbar erkennbar, welcher Wert zu welchem Platzhalter gehört.
  • Die Platzhalter werden nicht durch eine gewöhnliche Textersetzung ersetzt. Die Werte werden getrennt von der SQL-Anweisung an execute() übergeben. Dies schützt insbesondere vor SQL-Injection.
  • Bei den Schlüsseln des an execute() übergebenen Arrays kann der Doppelpunkt weggelassen werden. Der Schlüssel "kuNr" gehört somit zum Platzhalter :kuNr.
  • Weil PDO::ERRMODE_EXCEPTION aktiviert ist, löst PDO bei einem Datenbankfehler eine Ausnahme der Klasse PDOException aus. Der normale Ablauf des try-Blocks wird dadurch unterbrochen und im catch-Block fortgesetzt.
  • Ein ausdrückliches Schließen des Statement-Objekts und der Datenbankverbindung ist bei PDO normalerweise nicht erforderlich. Die zugehörigen Ressourcen werden spätestens am Ende der Ausführung des PHP-Programms freigegeben. Bei Bedarf können die Variablen vorher auf null gesetzt werden.
  • Die HTML-Attribute required, min und maxlength prüfen die Eingaben zunächst im Browser. Browserseitige Prüfungen können jedoch umgangen werden. Deshalb bleibt eine serverseitige Prüfung mit PHP erforderlich.
  • Die PHP-Prüfung kontrolliert hier, ob die Kundennummer größer als 0 ist und ob Name und Vorname nicht leer sind. Die durch maxlength="10" vorgegebene Höchstlänge wird im vorliegenden PHP-Code allerdings nicht ausdrücklich geprüft.
  • Das Programm benötigt keine ausdrücklich mit beginTransaction() gestartete Transaktion, weil nur ein einzelner Datensatz mit einer einzigen INSERT-Anweisung gespeichert wird. Eine einzelne SQL-Anweisung wird von der Datenbank vollständig ausgeführt oder im Fehlerfall nicht wirksam abgeschlossen.

Das Programm zeigt erneut das Zusammenspiel verschiedener Sprachen:

  • HTML stellt das Eingabeformular bereit.
  • PHP übernimmt und prüft die Eingaben und steuert den Programmablauf.
  • SQL fügt den neuen Datensatz in die Relation kunden ein.
  • PDO stellt die Verbindung zwischen PHP und dem Datenbanksystem her.

3.4 RechnungSpeichernPDO.php

3.4.1 Merkmale des Programms:

  • Speichern zusammengehöriger Datensätze in zwei miteinander verknüpften Relationen
  • Speichern eines Rechnungskopfes und mehrerer Rechnungspositionen
  • Datenbankzugriff über PDO
  • Verwendung einer Transaktion
  • Umsetzung des Alles-oder-nichts-Prinzips
  • Verwendung von Prepared Statements
  • Verwendung benannter Platzhalter
  • mehrfache Ausführung eines vorbereiteten Statements
  • Speicherung der Rechnungspositionen in einem mehrdimensionalen Array
  • Verarbeitung der Rechnungspositionen mit einer foreach-Schleife
  • Plausibilitätsprüfung der Artikelanzahl
  • Fehlerbehandlung mit try, catch und throw
  • Zurücksetzen der Transaktion mit rollBack() im Fehlerfall
  • Prüfung einer laufenden Transaktion mit inTransaction()
  • Aufruf des Programms über den Browser
  • unmittelbare Festlegung der Rechnungsdaten im PHP-Programm, also ohne HTML-Formular
  • Ausgabe einer Erfolgs- oder Fehlermeldung im Browser

3.4.2 Aufgabe

Das Programm trägt eine vollständige Rechnung in die Datenbank KuReAr ein. Gespeichert werden der Rechnungskopf in der Relation RechKoepfe und die zugehörigen Rechnungspositionen in der Relation RechPos.

Transaktionen

Der Rechnungskopf enthält die Rechnungsnummer, die Kundennummer und das Rechnungsdatum. Die Kundennummer verweist auf einen bereits vorhandenen Datensatz in der Relation Kunden. Der Kundendatensatz selbst wird durch dieses Programm nicht neu gespeichert.

Eine Rechnung bildet mit ihren Rechnungspositionen eine fachliche Einheit. Der Rechnungskopf und sämtliche Rechnungspositionen müssen daher entweder vollständig oder gar nicht gespeichert werden. Dies wird durch eine Transaktion sichergestellt. Tritt beim Speichern einer Rechnungsposition ein Fehler auf, werden auch der bereits gespeicherte Rechnungskopf und alle zuvor gespeicherten Positionen zurückgenommen. Nur wenn sämtliche Anweisungen erfolgreich ausgeführt wurden, schließt commit() die Transaktion erfolgreich ab.

Die Rechnungsdaten werden in diesem Beispiel unmittelbar im PHP-Programm festgelegt. Eine Eingabe über ein HTML-Formular findet nicht statt. Die neue Rechnung hat die Rechnungsnummer 23002. Sie gehört zum Kunden mit der Kundennummer 1010 und enthält 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 = 23002;

$kuNr = 1010;

$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

//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();

}

?>

3.4.4 Programmlauf

Nach einem erfolgreichen Programmlauf erscheint im Browser folgende Meldung:

Rechnung erfolgreich gespeichert.

Die Kontrollabfrage zeigt anschließend den neu eingefügten Rechnungskopf und seine drei Rechnungspositionen. Soll das Programm mit denselben Daten erneut erfolgreich ausgeführt werden, muss die Rechnung mit der Rechnungsnummer 23002 zuvor gelöscht werden. Wegen der Fremdschlüsselbeziehung sind zuerst die Rechnungspositionen und danach der Rechnungskopf zu löschen:

DELETE FROM RechPos

WHERE ReNr = 23002;

DELETE FROM RechKoepfe

WHERE ReNr = 23002;

3.4.5 Anmerkungen

  • Die Positionsnummer, die Artikelnummer und die Anzahl werden vor der Prüfung und Speicherung ausdrücklich mit (int) in ganze Zahlen umgewandelt.
  • Das Prepared Statement für die Rechnungspositionen wird nur einmal vorbereitet. Innerhalb der Schleife wird es anschließend mit jeweils anderen Werten mehrfach ausgeführt. Dadurch muss das Datenbanksystem die SQL-Anweisung nicht für jede Position erneut vorbereiten.
  • Die benannten Platzhalter :reNr, :posNr, :artNr und :anzahl werden beim Aufruf von execute() mit den entsprechenden Werten verbunden. Die SQL-Anweisung und die einzusetzenden Werte werden dadurch getrennt verarbeitet.
  • commit() schließt die Transaktion erfolgreich ab und übernimmt alle zu ihr gehörenden Änderungen dauerhaft. Bei einem Fehler nimmt rollBack() sowohl den Rechnungskopf als auch bereits eingefügte Rechnungspositionen zurück.
  • inTransaction() prüft, ob gegenwärtig eine Transaktion aktiv ist. Dadurch wird verhindert, dass rollBack() aufgerufen wird, obwohl keine Transaktion begonnen wurde oder die Transaktion bereits abgeschlossen ist.
  • Durch die Kurzschlussauswertung des UND-Operators && wird $pdo->inTransaction() nur aufgerufen, wenn die Variable $pdo vorhanden ist und nicht null enthält.
  • Der Kunde mit der Kundennummer 1010 muss bereits in der Relation Kunden vorhanden sein. Ebenso müssen die Artikelnummern 100, 101 und 102 in der Relation Artikel existieren. Andernfalls verhindern die Fremdschlüssel das Speichern der Rechnung.
  • Die Rechnungsnummer 23002 darf in der Relation RechKoepfe noch nicht vorhanden sein, wenn ReNr Primärschlüssel ist oder einer anderen eindeutigen Einschränkung unterliegt.
  • Eine ausdrücklich mit beginTransaction() gestartete Transaktion ist hier erforderlich, weil mehrere SQL-Anweisungen zu einer gemeinsamen fachlichen Einheit gehören.

3.4.6 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:

  • Löschen eines Kundendatensatzes aus der Relation kunden
  • Eingabe der Kundennummer über ein HTML-Formular
  • Übermittlung der Kundennummer mit der HTTP-Methode POST
  • Datenbankzugriff mit der Klasse mysqli
  • Verwendung eines Prepared Statements
  • Bindung der Kundennummer mit bind_param()
  • Schutz vor SQL-Injection
  • Ausführung einer DELETE-Anweisung
  • Prüfung des Ergebnisses mit affected_rows
  • Unterscheidung zwischen "Datensatz gelöscht" und "Datensatz nicht gefunden"
  • Beachtung vorhandener Fremdschlüsselbeziehungen
  • Ausgabe einer Erfolgs- oder Fehlermeldung im Browser
  • sichere HTML-Ausgabe der Meldung mit htmlspecialchars()
  • Schließen des Statement-Objekts und der Datenbankverbindung

Zum Nachvollziehen: Zuerst einen Kunden anlegen ohne Fremdschlüsselbeziehungen zu Rechnungsköpfen, z.B. mit dem Programm KundenEintragenMySQLi.php. Dieser "isolierte" Kundendatensatz kann anschließend mit dem Programm KundenLoeschenMySQLi.php wieder gelöscht werden.

4.1.2 Das Programm

<?php

 

// Variable für Rückmeldungen an den Benutzer. Sie speichert die 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 nach der Ausführung der Delete-Anweisung die Anzahl der tatsächlich gelöschten Datensätze.

Wert 1: Ein Datensatz wurde gelöscht; Wert 0: Es wurde kein Datensatz mit dieser Kundennummer gefunden. Hier sind nur 0 und 1 möglich, da KuNr Primärschlüssel ist.

*/

  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 die Daten an dieselbe Adresse gesendet, unter der das Formular aufgerufen wurde. Dadurch verarbeitet dieses PHP-Programm die Formulardaten selbst.

-->

<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

Zum Nachvollziehen des Programmlaufs wird zunächst mit KundenEintragenMySQLi.php ein neuer Kunde angelegt. Seine Kundennummer darf in keinem Rechnungskopf vorkommen.

Nach dem Aufruf von KundenLoeschenMySQLi.php erscheint eine Eingabemaske, in die die Kundennummer des zu löschenden Kunden eingetragen wird.

Nach dem Betätigen der Schaltfläche "Löschen" sind drei Ergebnisse möglich:

  • Ist der Kundendatensatz vorhanden und wird seine Kundennummer von keinem Rechnungskopf verwendet, wird der Datensatz gelöscht. Es erscheint die Meldung:

Datensatz wurde gelöscht.

  • Ist kein Kunde mit der angegebenen Kundennummer vorhanden, erscheint die Meldung:

Kein Datensatz mit dieser Kundennummer gefunden.

  • Wird die Kundennummer noch in der Relation RechKoepfe als Fremdschlüsselwert verwendet, verhindert das Datenbanksystem das Löschen. Das Programm gibt eine entsprechende Fehlermeldung aus.

Ein anschließender Blick auf die Relation kunden mit phpMyAdmin zeigt, ob der Datensatz gelöscht wurde.

Wichtig: Geben Sie die Kundennummer eines Kunden an, zu dem keine Rechnungsköpfe vorhanden sind. Seine Kundennummer darf also in keinem Datensatz der Relation RechKoepfe vorkommen.

4.2 RechnungLoeschenMySQLi.php

4.2.1 Merkmale des Programms

  • Löschen einer vollständigen Rechnung
  • Löschen zusammengehöriger Datensätze aus den Relationen RechPos und RechKoepfe
  • Beachtung einer Primärschlüssel-/Fremdschlüsselbeziehung
  • Datenbankzugriff mit der Klasse mysqli
  • Eingabe der Rechnungsnummer über ein HTML-Formular
  • Übermittlung der Rechnungsnummer mit der HTTP-Methode POST
  • Verwendung einer Transaktion
  • Umsetzung des Alles-oder-nichts-Prinzips
  • Verwendung mehrerer Prepared Statements
  • Bindung der Rechnungsnummer mit bind_param()
  • Prüfung, ob die angegebene Rechnung vorhanden ist
  • Ermittlung der Anzahl gelöschter Datensätze mit affected_rows
  • Fehlerbehandlung mit try, catch und throw
  • Zurücksetzen der Änderungen mit rollback() im Fehlerfall
  • Abschluss der Transaktion mit commit()
  • sichere HTML-Ausgabe der Rückmeldung mit htmlspecialchars()

4.2.2 Aufgabe

Das Programm löscht eine vollständige Rechnung aus der Datenbank KuReAr. Dazu werden zunächst die Rechnungspositionen aus der Relation RechPos und anschließend der Rechnungskopf aus der Relation RechKoepfe gelöscht.

Transaktionen

Die Rechnungspositionen müssen zuerst gelöscht werden, weil ihre Rechnungsnummer als Fremdschlüssel auf den Rechnungskopf verweist. Solange Rechnungspositionen vorhanden sind, darf der zugehörige Rechnungskopf nicht gelöscht werden, sofern die Fremdschlüsselbeziehung nicht mit ON DELETE CASCADE definiert wurde.

Zum Löschen einer Rechnung sind somit mehrere zusammengehörige SQL-Anweisungen erforderlich. Deshalb werden die Löschvorgänge innerhalb einer Transaktion ausgeführt. Entweder werden der Rechnungskopf und alle seine Positionen gelöscht oder keine der Änderungen wird dauerhaft übernommen.

Die Rechnungsnummer wird über ein HTML-Formular eingegeben. Das Programm wird daher über den Browser und nicht über die Konsole aufgerufen.

Im folgenden Beispiel wird die Rechnung 23001 des fiktiven Kunden Rudolf Hinterhof gelöscht. Die Rechnung besitzt zwei Rechnungspositionen.

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=23001;


ReNr

KuNr

ReDatum

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

 

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 {

   

// Festhalten, ob gegenwärtig eine Transaktion aktiv ist.

$transaktionAktiv = false;

try {

 

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

// damit zu einer gemeinsamen Arbeitseinheit.

  $con->begin_transaction();

  $transaktionAktiv = true;

   

/*

Prüfen, ob ein Rechnungskopf mit der eingegebenen Rechnungsnummer existiert.

$stmtCheck verweist auf ein Statement-Objekt der Klasse mysqli_stmt. Das Fragezeichen ist ein Platzhalter für die Rechnungsnummer.

*/

  $stmtCheck = $con->prepare(

    "SELECT 1 FROM rechkoepfe WHERE ReNr = ?");

  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.

  $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 Änderungen beendet.

  $con->rollback();

  $transaktionAktiv = false;

  $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, sofern 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.

$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.

$deletedKopf = $stmtKopfDel->affected_rows;

$stmtKopfDel->close();

 

// Es muss genau ein Rechnungskopf gelöscht worden sein.

if ($deletedKopf !== 1) {

  throw new Exception("Der Rechnungskopf konnte nicht "

    . "eindeutig gelöscht werden.");

}

 

/*

Transaktion erfolgreich abschließen. Erst mit commit() werden alle Löschungen dauerhaft übernommen.

*/

  $con->commit();

  $transaktionAktiv = false;

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

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

  }

} catch (Exception $e) {

 

/*

Die Änderungen werden nur zurückgenommen, wenn tatsächlich eine Transaktion aktiv ist.

*/

if ($transaktionAktiv) {$con->rollback();

  $transaktionAktiv = false;

  }

$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 Programmlauf

Eingabemaske:

Nach erfolgreichem Programmlauf und Löschen der Rechnung mit der Rechnungsnummer 23050:

4.2.5 Erläuterung des Programms

Das Programm löscht eine vollständige Rechnung mit MySQLi. Es verwendet 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. Wegen der Fremdschlüsselbeziehung dürfen diese Datensätze nicht in beliebiger Reihenfolge gelöscht werden.

Zunächst werden die Verbindungsdaten festgelegt. Anschließend erzeugt new mysqli() ein Objekt der Klasse mysqli und stellt damit die Verbindung zur Datenbank her. Die Variable $con verweist auf dieses Verbindungsobjekt.

Das Programm prüft, ob beim Verbindungsaufbau ein Fehler aufgetreten ist. In diesem Fall gibt die() eine Fehlermeldung aus und beendet die Ausführung des Programms. Nach dem erfolgreichen Verbindungsaufbau legt set_charset("utf8mb4") den Zeichensatz für die Datenbankverbindung fest.

Wenn das Formular mit der HTTP-Methode POST abgeschickt wird, übernimmt das Programm die eingegebene Rechnungsnummer aus dem POST-Array. Der Wert wird mit (int) in eine ganze Zahl umgewandelt. Anschließend erfolgt eine einfache Plausibilitätsprüfung. Nur Rechnungsnummern größer als 0 werden akzeptiert.

Bei einer gültigen Eingabe wird zunächst die Variable $transaktionAktiv mit false belegt. Sie hält fest, ob gegenwärtig eine Transaktion läuft.

Im try-Block startet begin_transaction() eine neue Transaktion. Anschließend wird $transaktionAktiv auf true gesetzt. Alle folgenden Datenbankoperationen gehören damit zu einer gemeinsamen Arbeitseinheit. Danach prüft das Programm, ob ein Rechnungskopf mit der angegebenen Rechnungsnummer vorhanden ist. Dazu wird ein Prepared Statement verwendet:

SELECT 1 FROM rechkoepfe WHERE ReNr = ?

Das Fragezeichen ist ein Platzhalter für die Rechnungsnummer. bind_param() bindet die Variable $reNr an diesen Platzhalter. Das Typkennzeichen "i" legt fest, dass der Wert als Integer behandelt wird.

Nach der Ausführung speichert store_result() das Ergebnis im Statement-Objekt zwischen. Dadurch kann anschließend mit num_rows festgestellt werden, ob ein passender Rechnungskopf gefunden wurde. Ist keine Rechnung mit der angegebenen Rechnungsnummer vorhanden, wird das Statement geschlossen. Anschließend beendet rollback() die Transaktion ohne dauerhafte Änderungen. Danach wird $transaktionAktiv wieder auf false gesetzt und eine entsprechende Meldung gespeichert.

Ist die Rechnung vorhanden, erfolgt das Löschen in zwei Schritten. Zuerst werden alle zugehörigen Datensätze aus der Relation RechPos gelöscht. Diese Reihenfolge ist erforderlich, weil die Rechnungspositionen über einen Fremdschlüssel vom Rechnungskopf abhängen. Solange Rechnungspositionen vorhanden sind, darf der Rechnungskopf nicht gelöscht werden, sofern der Fremdschlüssel nicht mit ON DELETE CASCADE definiert wurde.

affected_rows liefert die Anzahl der gelöschten Rechnungspositionen. Diese Anzahl wird in der Variablen $deletedPos gespeichert.

Danach wird der Rechnungskopf aus der Relation RechKoepfe gelöscht. Die Anzahl der gelöschten Rechnungsköpfe wird in $deletedKopf gespeichert. Da ReNr den Rechnungskopf eindeutig kennzeichnet, muss genau ein Rechnungskopf gelöscht worden sein. Ist dies nicht der Fall, löst das Programm mit throw new Exception(...) eine Exception aus.

Wurden beide Löschoperationen erfolgreich ausgeführt, schließt commit() die Transaktion ab. Erst dadurch werden die Löschungen dauerhaft übernommen. Anschließend wird $transaktionAktiv wieder auf false gesetzt.

Tritt während der Transaktion ein Fehler auf, wird der normale Programmablauf unterbrochen und im catch-Block fortgesetzt. Dort prüft das Programm zunächst die Variable $transaktionAktiv. Nur wenn tatsächlich eine Transaktion läuft, wird rollback() aufgerufen. rollback() nimmt alle seit dem Beginn der Transaktion vorgenommenen Änderungen zurück. Dadurch wird verhindert, dass beispielsweise die Rechnungspositionen gelöscht werden, der zugehörige Rechnungskopf aber erhalten bleibt.

Die Rechnung wird somit entweder vollständig oder gar nicht gelöscht. Auf diese Weise bleibt die Datenbank in einem konsistenten Zustand.

4.2.6 Anmerkung zur Programmierung

  • $message enthält Rückmeldungen des Programms. Einige dieser Meldungen enthalten auch die vom Benutzer eingegebene Rechnungsnummer $reNr. Da die Rechnungsnummer zuvor mit (int) in eine ganze Zahl umgewandelt wurde und die vollständige Meldung vor der HTML-Ausgabe mit htmlspecialchars() behandelt wird, kann sie nicht als HTML-Code interpretiert werden.
  • die() ist ein PHP-Sprachkonstrukt und ein Alias für exit(). Wird die() ein Text übergeben, gibt PHP diesen Text aus und beendet anschließend sofort die Ausführung des Programms.
  • Auch die Ausführung von $stmtCheck->execute() wird kontrolliert. Schlägt die Ausführung fehl, löst das Programm eine Exception aus. Der normale Ablauf des try-Blocks wird dadurch beendet und im catch-Block fortgesetzt.
  • Das Programm verwendet insgesamt drei Prepared Statements: eines für die Existenzprüfung, eines zum Löschen der Rechnungspositionen und eines zum Löschen des Rechnungskopfes.
  • Das Statement für die Existenzprüfung verwendet SELECT 1, weil nur festgestellt werden soll, ob ein Rechnungskopf vorhanden ist. Die einzelnen Attribute des Rechnungskopfes müssen dafür nicht gelesen werden.
  • Die Variable $transaktionAktiv hält fest, ob gegenwärtig eine Transaktion läuft. Dadurch wird verhindert, dass rollback() aufgerufen wird, obwohl keine aktive Transaktion vorhanden ist.
  • affected_rows enthält nach einer DELETE-Anweisung die Anzahl der tatsächlich gelöschten Datensätze.
  • Die zusätzliche Prüfung von $deletedKopf stellt sicher, dass genau ein Rechnungskopf gelöscht wurde.

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:

Eingabefeld einrichten

  • <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, unter welchem Namen der eingegebene Wert beim Absenden übertragen wird.

Die browserseitige Prüfung durch type="number", min="1" und required kann umgangen werden. Deshalb prüft das PHP-Programm die übermittelte Rechnungsnummer zusätzlich. Die verwendete Umwandlung mit (int) und die anschließende Prüfung auf einen Wert größer als 0 stellen allerdings nur eine einfache Plausibilitätsprüfung dar.

Noch ein Hinweis

Die hier gegebene Erklärung zu die() passt zur hier gewählten einführenden Programmrealisierung. Unter PHP 8.2 können MySQLi-Verbindungsfehler jedoch abhängig vom eingestellten Fehlermodus bereits als mysqli_sql_exception ausgelöst werden. Soll auch dieser Fall zuverlässig behandelt werden, müsste der Verbindungsaufbau ebenfalls in einen try-catch-Block aufgenommen werden.

4.3 KundenLoeschenPDO.php

Ein Kundendatensatz wird aus der Relation kunden gelöscht.

4.3.1 Merkmale des Programms

  • Löschen eines Datensatzes aus der Relation kunden
  • Eingabe der Kundennummer über ein HTML-Formular
  • Übermittlung der Formulardaten mit der HTTP-Methode POST
  • Datenbankzugriff mit der Klasse PDO
  • Auswahl des MySQL-Treibers über den DSN
  • Verwendung eines Prepared Statements
  • Verwendung des benannten Platzhalters :kuNr
  • getrennte Übergabe der SQL-Anweisung und des einzusetzenden Wertes
  • Schutz vor SQL-Injection
  • Fehlerbehandlung mit try und catch
  • Auslösen von Ausnahmen mit PDO::ERRMODE_EXCEPTION
  • Prüfung des Löschergebnisses mit rowCount()
  • Unterscheidung zwischen "Datensatz gelöscht" und "Datensatz nicht gefunden"
  • Beachtung vorhandener Fremdschlüsselbeziehungen
  • sichere HTML-Ausgabe der Rückmeldung mit htmlspecialchars()
  • keine ausdrücklich gestartete Transaktion erforderlich, da nur eine einzige DELETE-Anweisung ausgeführt wird

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 Datenbankfehler 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 die Daten an dieselbe Adresse gesendet, unter der das Formular aufgerufen wurde. Dadurch verarbeitet dieses PHP-Programm die Formulardaten selbst.

-->

<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

Zum Nachvollziehen des Programmlaufs wird zunächst ein neuer Kunde angelegt, zu dem noch keine Rechnung vorhanden ist. Dazu kann beispielsweise das Programm KundenEintragenPDO.php verwendet werden.

Nach dem Aufruf von KundenLoeschenPDO.php erscheint im Browser eine Eingabemaske. In diese wird die Kundennummer des zu löschenden Kunden eingetragen.

Nach dem Betätigen der Schaltfläche "Löschen" sind drei Ergebnisse möglich:

  • Ist der Kunde vorhanden und wird seine Kundennummer in keinem Rechnungskopf verwendet, wird der Datensatz gelöscht. Es erscheint die Meldung:

  • Ist kein Kunde mit der angegebenen Kundennummer vorhanden, erscheint:

Kein Datensatz mit dieser Kundennummer gefunden.

  • Wird die Kundennummer noch in der Relation RechKoepfe als Fremdschlüsselwert verwendet, verhindert das Datenbanksystem das Löschen. Die dabei ausgelöste PDOException wird vom catch-Block abgefangen und als Fehlermeldung ausgegeben.

Ein anschließender Blick auf die Relation kunden mit phpMyAdmin zeigt, ob der Datensatz gelöscht wurde.

4.3.4 Erläuterung des Programms

Das Programm zeigt, wie ein Kundendatensatz mithilfe von PDO aus der Relation kunden der Datenbank KuReAr gelöscht werden kann. Zu Beginn wird die Variable $message angelegt. Sie speichert die Rückmeldung, die später im HTML-Teil ausgegeben wird. Anschließend werden der DSN, der Benutzername und das Passwort festgelegt.

PHP Data Objects

DSN bedeutet "Data Source Name". Der DSN enthält hier den Datenbanktreiber mysql, den Datenbankserver localhost, die Bezeichnung der Datenbank KuReAr und den Zeichensatz utf8mb4.

Nach dem Absenden des Formulars prüft das Programm, ob die aktuelle Anfrage mit der HTTP-Methode POST realisiert ist:

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

Dadurch wird erreicht, dass der Löschteil beim ersten Aufruf der Webseite noch nicht ausgeführt wird. Erst nach dem Absenden des Formulars wird die übermittelte Kundennummer verarbeitet. Eine POST-Anfrage beweist allerdings nicht, dass die Daten tatsächlich mit diesem Formular erzeugt wurden. POST-Anfragen können auch auf andere Weise an den Webserver gesendet werden. Deshalb müssen die Daten zusätzlich im PHP-Programm geprüft werden.

Die Kundennummer wird aus dem POST-Array übernommen: $kuNr = (int)($_POST["kuNr"] ?? 0); Der Null-Koaleszenz-Operator ?? liefert den Wert 0, wenn kein Formulareintrag mit dem Namen kuNr vorhanden ist. (int) wandelt den übernommenen Wert in eine ganze Zahl um.

Anschließend prüft das Programm, ob die Kundennummer größer als 0 ist. Ist dies nicht der Fall, wird eine Fehlermeldung gespeichert und keine Datenbankoperation ausgeführt.

Bei einer gültigen Eingabe beginnt der try-Block. Innerhalb dieses Blocks erzeugt new PDO() ein Objekt der Klasse PDO und stellt damit die Verbindung zur Datenbank her. Die Option

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION

bewirkt, dass PDO bei einem Datenbankfehler eine Ausnahme der Klasse PDOException auslöst. Diese Ausnahme kann zentral im catch-Block behandelt werden.

Das Löschen erfolgt mit einem Prepared Statement:

$stmt = $pdo->prepare(

    "DELETE FROM kunden WHERE KuNr = :kuNr"

);

:kuNr ist ein benannter Platzhalter. Beim Aufruf von execute() wird der Wert der PHP-Variablen $kuNr diesem Platzhalter zugeordnet:

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

Dabei wird der Wert nicht durch eine gewöhnliche Textersetzung in die SQL-Anweisung eingefügt. SQL-Anweisung und Wert werden getrennt verarbeitet. Dadurch schützt das Prepared Statement insbesondere vor SQL-Injection.

Nach der Ausführung liefert rowCount() die Anzahl der durch die DELETE-Anweisung gelöschten Datensätze: if ($stmt->rowCount() > 0). Da KuNr Primärschlüssel ist, kann höchstens ein Kundendatensatz gelöscht werden:

  • rowCount() liefert 1: Der Kunde wurde gelöscht.
  • rowCount() liefert 0: Ein Kunde mit dieser Kundennummer wurde nicht gefunden.

Tritt beim Verbindungsaufbau oder beim Ausführen der SQL-Anweisung ein Datenbankfehler auf, wird eine PDOException ausgelöst. Der normale Programmablauf wird unterbrochen und im catch-Block fortgesetzt. Die Fehlermeldung wird in $message gespeichert.

Im HTML-Teil stellt das Programm ein Formular zur Eingabe der Kundennummer bereit. Ist $message nicht leer, wird die darin gespeicherte Rückmeldung ausgegeben.

htmlspecialchars() sorgt dafür, dass Zeichen mit besonderer Bedeutung in HTML nicht als Bestandteile des HTML-Codes interpretiert werden. Sie werden stattdessen als Text dargestellt. Vgl. dazu auch den Anhang.

Keine Transaktion erforderlich

Das Programm benötigt keine ausdrücklich mit beginTransaction() gestartete Transaktion, weil nur eine einzige DELETE-Anweisung ausgeführt wird. Die Datenbank führt diese Anweisung vollständig aus oder lässt sie im Fehlerfall ohne wirksames Löschergebnis.

Einfache statt vollständige Eingabeprüfung

Die Anweisung $kuNr = (int)($_POST["kuNr"] ?? 0); führt nur eine einfache Plausibilitätsprüfung durch. Ein manipuliert übermittelter Wert wie "1002abc" würde in die Zahl 1002 umgewandelt. Eine strengere Prüfung wäre:

$kuNr = filter_input(INPUT_POST, "kuNr", FILTER_VALIDATE_INT,

  ["options" => ["min_range" => 1]]);

if ($kuNr === false || $kuNr === null) {

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

}

4.4 RechnungLoeschenPDO.php

Das Programm löscht eine vollständige Rechnung aus der Datenbank KuReAr.

4.4.1 Merkmale des Programms

  • Löschen zusammengehöriger Datensätze aus den Relationen RechPos und RechKoepfe
  • Beachtung einer Primärschlüssel-/Fremdschlüsselbeziehung
  • Eingabe der Rechnungsnummer über ein HTML-Formular
  • Übermittlung der Rechnungsnummer mit der HTTP-Methode POST
  • Datenbankzugriff mit PDO
  • Verwendung einer Transaktion
  • Umsetzung des Alles-oder-nichts-Prinzips
  • Verwendung mehrerer Prepared Statements
  • Verwendung des benannten Platzhalters :reNr
  • Prüfung, ob die angegebene Rechnung vorhanden ist
  • Ermittlung der gelöschten Datensätze mit rowCount()
  • Fehlerbehandlung mit try, catch und throw
  • Prüfung einer aktiven Transaktion mit inTransaction()
  • Zurücksetzen der Transaktion mit rollBack()
  • erfolgreicher Abschluss mit commit()
  • sichere HTML-Ausgabe der Rückmeldung mit htmlspecialchars()

4.4.2 Aufgabe

Das Programm löscht eine vollständige Rechnung aus der Datenbank KuReAr. Eine Rechnung besteht aus dem Rechnungskopf in der Relation RechKoepfe und den zugehörigen Rechnungspositionen in der Relation RechPos. Wegen der Fremdschlüsselbeziehung müssen zuerst die Rechnungspositionen und danach der Rechnungskopf gelöscht werden, sofern der Fremdschlüssel nicht mit ON DELETE CASCADE definiert wurde. Beide Löschoperationen werden innerhalb einer Transaktion ausgeführt. Dadurch ist sichergestellt, dass entweder der Rechnungskopf und sämtliche Rechnungspositionen gelöscht werden oder keine der Änderungen dauerhaft übernommen wird. Die Rechnungsnummer wird über ein HTML-Formular eingegeben.

4.4.3 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

// 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();

}

/*

Zu den beiden try-catch-Blöcken

Der äußere try-catch-Block behandelt insbesondere Fehler beim Aufbau der PDO-Verbindung.

Der innere try-catch-Block behandelt Fehler während der Transaktion.

inTransaction() verhindert, dass rollBack() ohne aktive Transaktion

aufgerufen wird.

Deshalb dieser innere catch-Block:

} catch (Exception $e) {

  if ($pdo->inTransaction()) {$pdo->rollBack();}

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

    . $e->getMessage();

}

Auch eine während der Datenbankoperationen ausgelöste PDOException wird hier erfasst, weil PDOException von Exception abgeleitet ist.

*/

 

?>

<!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.4 Programmlauf

So sieht das Ergebnis nach einem erfolgreichen Programmlauf aus.

Nach dem Aufruf des Programms erscheint im Browser eine Eingabemaske für die Rechnungsnummer. Wird die Schaltfläche "Löschen" betätigt, prüft das Programm zunächst, ob ein Rechnungskopf mit dieser Rechnungsnummer vorhanden ist. Ist die Rechnung vorhanden, werden zuerst die zugehörigen Rechnungspositionen und danach der Rechnungskopf gelöscht. Nach dem erfolgreichen Abschluss erscheint beispielsweise:

Rechnung ReNr = 23050 gelöscht (Kopf: 1, Positionen: 2).

Die Angabe Kopf: 1 bedeutet, dass ein Rechnungskopf gelöscht wurde. Positionen: 2 zeigt, dass zwei zugehörige Rechnungspositionen gelöscht wurden. Wird dieselbe Rechnungsnummer anschließend erneut eingegeben, erscheint:

Keine Rechnung mit ReNr = 23050 gefunden.

Eine Kontrolle mit phpMyAdmin zeigt, dass danach weder der Rechnungskopf noch die zugehörigen Rechnungspositionen vorhanden sind.

4.4.5 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.

Ohne eine Transaktion könnte es vorkommen, dass die Rechnungspositionen gelöscht werden, der Rechnungskopf wegen eines nachfolgenden Fehlers aber erhalten bleibt. Ebenso problematisch wäre der umgekehrte Fall. Es entstünde ein inkonsistenter Datenbestand. Die Transaktion verhindert einen solchen Zustand. Sie stellt sicher, dass alle zusammengehörigen Löschoperationen erfolgreich abgeschlossen oder vollständig zurückgenommen werden.

Datenkonsistenz

4.4.6 Anmerkungen zur Programmierung

  • Das Prepared Statement für die Existenzprüfung verwendet SELECT 1, weil nur festgestellt werden soll, ob ein Rechnungskopf vorhanden ist. Die eigentlichen Rechnungsdaten müssen dafür nicht geladen werden.
  • fetchColumn() liefert den Wert der ersten Spalte der nächsten Ergebniszeile. Da SELECT 1 verwendet wird, ist dieser Wert bei einem Treffer 1. Wird keine Zeile gefunden, liefert die Methode false.
  • Die Rechnungspositionen müssen vor dem Rechnungskopf gelöscht werden, sofern die Fremdschlüsselbeziehung nicht mit ON DELETE CASCADE eingerichtet wurde.
  • rowCount() liefert nach einer DELETE-Anweisung die Anzahl der gelöschten Datensätze.
  • Die zusätzliche Prüfung $deletedKopf !== 1 stellt sicher, dass genau ein Rechnungskopf gelöscht wurde.
  • inTransaction() prüft, ob gegenwärtig eine Transaktion aktiv ist. rollBack() wird nur in diesem Fall aufgerufen.
  • Durch die Transaktion wird die Rechnung entweder vollständig oder gar nicht gelöscht.
  • Ein ausdrückliches Schließen des Statement-Objekts und der PDO-Verbindung ist normalerweise nicht erforderlich. Die Ressourcen werden spätestens am Ende der Programmausführung freigegeben.
  • Die Umwandlung mit (int) ist nur eine einfache Plausibilitätsprüfung. Manipulierte Eingaben können damit nicht streng validiert werden.

5 Ändern von Daten

5.1 KundenAendernMySQLi.php

5.1.1 Merkmale des Programms

  • Aufruf des Programms über den Browser
  • Eingabe der Daten über ein HTML-Formular
  • Ausgabe einer Rückmeldung im Browser
  • Ändern eines vorhandenen Datensatzes in der Relation kunden
  • Eingabe der Kundennummer und der neuen Kundendaten über ein HTML-Formular
  • Übermittlung der Formulardaten mit der HTTP-Methode POST
  • Datenbankzugriff mit der Klasse mysqli
  • Verwendung einer UPDATE-Anweisung
  • Verwendung eines Prepared Statements
  • Verwendung von Positionsplatzhaltern
  • Bindung der Werte mit bind_param()
  • Schutz vor SQL-Injection
  • Prüfung des Ergebnisses mit affected_rows
  • Schließen des Statement-Objekts und der Datenbankverbindung
  • sichere HTML-Ausgabe der Rückmeldung mit htmlspecialchars()
  • keine Transaktion erforderlich, weil nur eine einzige UPDATE-Anweisung ausgeführt wird

5.1.2 Aufgabe

Das Programm ändert den Namen und den Vornamen eines bereits vorhandenen Kunden. Dazu werden die Kundennummer sowie der neue Name und der neue Vorname in ein HTML-Formular eingegeben. Die Kundennummer legt fest, welcher Datensatz geändert werden soll. Das Programm lädt die bisherigen Kundendaten nicht aus der Datenbank. Der Benutzer muss daher die Kundennummer kennen und Name sowie Vorname vollständig neu eingeben. Nach dem Absenden des Formulars prüft das PHP-Programm die eingegebenen Werte und führt anschließend eine UPDATE-Anweisung aus.

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 Ende des PHP-Abschnitts 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 den Namen

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 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 Adresse gesendet, unter der das Formular aufgerufen wurde. Dadurch verarbeitet dieses PHP-Programm die Formulardaten selbst.

required kennzeichnet die Felder als Pflichtfelder.

-->

<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">Ä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

  • Ändern eines vorhandenen Datensatzes in der Relation kunden
  • Eingabe der Kundennummer und der neuen Kundendaten über ein HTML-Formular
  • Übermittlung der Formulardaten mit der HTTP-Methode POST
  • Datenbankzugriff mit der Klasse PDO
  • Auswahl des MySQL-Treibers durch den DSN
  • Verwendung von Prepared Statements
  • Verwendung benannter Platzhalter
  • getrennte Übergabe von SQL-Anweisungen und einzusetzenden Werten
  • Schutz vor SQL-Injection
  • Prüfung, ob die Kundennummer vorhanden ist
  • Unterscheidung zwischen "Kunde nicht gefunden", "Werte unverändert" und "Datensatz geändert"
  • Fehlerbehandlung mit try und catch
  • Auslösen von Ausnahmen mit PDO::ERRMODE_EXCEPTION
  • sichere HTML-Ausgabe der Rückmeldung mit htmlspecialchars()

5.2.2 Aufgabe

Das Programm ändert den Namen und den Vornamen eines bereits vorhandenen Kunden in der Relation kunden. Über ein HTML-Formular werden die Kundennummer, der neue Name und der neue Vorname eingegeben. Die Kundennummer bestimmt, welcher Kundendatensatz geändert werden soll.

Nach dem Absenden des Formulars prüft das Programm zunächst, ob alle erforderlichen Eingaben vorhanden sind. Anschließend wird festgestellt, ob ein Kunde mit der angegebenen Kundennummer existiert. Dabei werden drei Fälle unterschieden:

  • Die Kundennummer ist nicht vorhanden.
  • Der Kunde ist vorhanden, aber die eingegebenen Werte entsprechen bereits den gespeicherten Werten.
  • Der Kunde ist vorhanden und seine Daten werden geändert.

Das Programm lädt die bisherigen Kundendaten nicht automatisch in die Eingabemaske. Der Benutzer muss daher die Kundennummer kennen und Name sowie Vorname vollständig neu eingeben.

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 {

 

// Verbindung zur Datenbank aufbauen.

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

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

   

/*

Prüfen, ob ein Kunde mit der angegebenen Kundennummer vorhanden ist. Gleichzeitig werden die bisher gespeicherten Werte für Name und Vorname abgerufen.

*/

$stmtCheck = $pdo->prepare(

  "SELECT Name, Vorname FROM kunden WHERE KuNr = :kuNr");

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

$kunde = $stmtCheck->fetch(PDO::FETCH_ASSOC);

 

// Kein Kunde mit dieser Kundennummer vorhanden.

if ($kunde === false) {

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

} elseif (

  $kunde["Name"] === $name &&

  $kunde["Vorname"] === $vorname

) {

 

// Der Kunde ist vorhanden. Die eingegebenen Werte stimmen aber

// bereits mit den gespeicherten Werten überein.

$message = "Keine Änderung erforderlich.";

  } else {

   

// Kundendaten ändern.

$stmtUpdate = $pdo->prepare(

  "UPDATE kunden SET Name = :name, Vorname = :vorname

    WHERE KuNr = :kuNr");

$stmtUpdate->execute([

  "name" => $name, "vorname" => $vorname, kuNr" => $kuNr]);

if ($stmtUpdate->rowCount() === 1) {

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

} else {

  $message = "Der Datensatz konnte nicht geändert werden.";

    }

  }

} catch (PDOException $e) {

 

// Technische Fehlermeldung für die Entwicklungsumgebung.

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

}

 

// 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" 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">Ä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 Programmlauf

Nach dem Aufruf des Programms erscheint eine leere Eingabemaske. In diese werden die Kundennummer, der neue Name und der neue Vorname eingetragen. Nach dem Betätigen der Schaltfläche "Ändern" prüft das Programm zunächst, ob alle Eingaben gültig sind.

Ist kein Kunde mit der eingegebenen Kundennummer vorhanden, erscheint:

Kein Kunde mit dieser Kundennummer gefunden.

Stimmen Name und Vorname bereits mit den gespeicherten Werten überein, erscheint:

Keine Änderung erforderlich.

Wurde der Kundendatensatz erfolgreich geändert, erscheint:

Datensatz wurde geändert.

Ein anschließender Blick auf die Relation kunden mit phpMyAdmin zeigt die geänderten Kundendaten.

5.2.5 Anmerkungen

  • ?? ist kein Platzhalter, sondern der Null-Koaleszenzoperator. Er liefert hier einen Ersatzwert, wenn der entsprechende Formulareintrag nicht vorhanden ist. Beispielsweise liefert $_POST["kuNr"] ?? 0 den Wert 0, wenn kein Formulareintrag mit dem Namen kuNr übertragen wurde.
  • $message speichert Rückmeldungen des Programms. Im Fehlerfall kann die Variable auch eine technische Datenbankfehlermeldung enthalten. Vor der HTML-Ausgabe wird ihr Inhalt mit htmlspecialchars() behandelt.
  • Benannte Platzhalter werden nicht durch eine gewöhnliche Textersetzung ersetzt. Die SQL-Anweisung und die einzusetzenden Werte werden getrennt verarbeitet.
  • Die Namen der benannten Platzhalter in der SQL-Anweisung müssen zu den Schlüsseln des an execute() übergebenen Arrays passen. Beispielsweise gehört der Platzhalter :name zum Array-Eintrag "name" => $name.
  • Der Doppelpunkt gehört zum Platzhalter in der SQL-Anweisung. Bei den Schlüsseln des Arrays für execute() kann er weggelassen werden: :name entspricht: "name" => $name.
  • fetch(PDO::FETCH_ASSOC) liefert den gefundenen Kundendatensatz als assoziatives Array. Wird kein Datensatz gefunden, liefert fetch() den Wert false.
  • rowCount() liefert nach der UPDATE-Anweisung die Anzahl der geänderten Datensätze.
  • Durch die vorherige Existenzprüfung kann das Programm unterscheiden, ob der Kunde nicht vorhanden ist oder ob seine Daten bereits den eingegebenen Werten entsprechen.
  • maxlength="10" begrenzt die Länge von Name und Vorname zunächst nur im Browser. Das PHP-Programm prüft diese Höchstlänge nicht ausdrücklich.
  • required und min="1" führen zu browserseitigen Prüfungen. Diese können umgangen werden und ersetzen deshalb nicht die Prüfung im PHP-Programm.

6 Übersicht CRUD-Operationen

Die bisherigen Beispiele zeigen die vier grundlegenden Operationen zur Verarbeitung von Daten:

  • Create: einen neuen Datensatz anlegen
  • Read: einen vorhandenen Datensatz lesen
  • Update: einen vorhandenen Datensatz ändern
  • Delete: einen vorhandenen Datensatz löschen

Aus den Anfangsbuchstaben dieser englischen Bezeichnungen entsteht die Abkürzung CRUD. Diese bezeichnet somit Datenoperationen. Die Bezeichnungen stimmen allerdings nicht in jedem Fall mit den Namen der verwendeten SQL-Anweisungen überein:

 


CRUD-Operation

Bedeutung

SQL-Anweisung

Create

Datensatz anlegen

INSERT

Read

Datensatz lesen

SELECT

Update

Datensatz ändern

UPDATE

Delete

Datensatz löschen

DELETE

 

Insbesondere darf die CRUD-Operation "Create" nicht mit der SQL-Anweisung CREATE verwechselt werden. CREATE legt beispielsweise eine neue Datenbank oder Relation an. Zum Einfügen eines Datensatzes wird dagegen INSERT verwendet.

Die folgende Übersicht zeigt die vier Operationen am Beispiel der Relation kunden aus der Datenbank KuReAr. Zunächst wird PDO, anschließend MySQLi verwendet.

Die Beispiele sind PHP-Programmfragmente. Sie müssen innerhalb eines PHP-Abschnitts stehen:

<?php

// PHP-Anweisungen

?>

Die verwendeten Attributsausprägungen werden zur Vereinfachung unmittelbar im PHP-Code festgelegt. In einer Webanwendung würden sie üblicherweise aus einem HTML-Formular stammen. Die Fragmente bauen aufeinander auf:

1. Der Kunde 2001 wird angelegt.

2. Der Kunde 2001 wird gelesen.

3. Sein Name wird von "Meier" in "Meyer" geändert.

4. Der Kunde 2001 wird gelöscht.

Vor dem ersten Programmlauf darf die Kundennummer 2001 daher noch nicht vorhanden sein.

6.1 CRUD-Übersicht mit PDO

Herstellen der Verbindung

new PDO() erzeugt ein PDO-Verbindungsobjekt. Die Variable $pdo verweist auf dieses Objekt.

$pdo = new PDO(

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

CREATE (Datensatz einfügen)

Die CRUD-Operation "Create" wird mit der SQL-Anweisung INSERT umgesetzt. :kuNr, :name und :vorname sind benannte Platzhalter. Die zugehörigen Werte werden beim Aufruf von execute() übergeben.

$stmt = $pdo->prepare(

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

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

READ (Datensatz lesen)

Die CRUD-Operation "Read" wird mit SELECT umgesetzt. fetch(PDO::FETCH_ASSOC) liefert den gefundenen Datensatz als assoziatives Array. Die Attributnamen bilden die Schlüssel des Arrays. print_r($row, true) liefert eine für Menschen gut lesbare Darstellung des Arrays als Zeichenkette. htmlspecialchars() sorgt dafür, dass diese Zeichenkette sicher als HTML ausgegeben wird.

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

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

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

echo "<pre>";

echo htmlspecialchars(print_r($row, true), ENT_QUOTES, "UTF-8");

echo "</pre>";

UPDATE (Datensatz ändern)

Die CRUD-Operation "Update" wird mit der gleichnamigen SQL-Anweisung UPDATE umgesetzt. Die WHERE-Bedingung ist besonders wichtig. Ohne sie würde der Name in sämtlichen Kundendatensätzen geändert.

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

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

DELETE (Datensatz löschen)

Die CRUD-Operation "Delete" wird mit der SQL-Anweisung DELETE umgesetzt. Auch bei DELETE ist die WHERE-Bedingung entscheidend. Ohne sie würden sämtliche Datensätze der Relation kunden gelöscht.

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

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

6.2 CRUD-Übersicht mit MySQLi

Herstellen der Verbindung

new mysqli() erzeugt ein Verbindungsobjekt der Klasse mysqli. Die Variable $con verweist auf dieses Objekt.

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

  if ($con->connect_error) {

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

  }

$con->set_charset("utf8mb4");

CREATE (Datensatz einfügen)

Die Zeichenfolge "iss" legt die Datentypen der drei gebundenen Variablen fest:

  • i: Integer für die Kundennummer
  • s: String für den Namen
  • s: String für den Vornamen

$kuNr = 2001;

$name = "Meier";

$vorname = "Anna";

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

  VALUES (?, ?, ?)");

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

$stmt->execute();

$stmt->close();

READ (Datensatz lesen)

get_result() liefert ein Ergebnisobjekt der Klasse mysqli_result. fetch_assoc() liest den Datensatz und gibt ihn als assoziatives Array zurück.

$kuNr=2001;

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

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

$stmt->execute();

$result = $stmt->get_result();

$row = $result->fetch_assoc();

echo "<pre>";

echo htmlspecialchars(print_r($row, true), ENT_QUOTES, "UTF-8");

echo "</pre>";

$result->close();

$stmt->close();

UPDATE (Datensatz ändern)

Die Zeichenfolge "si" steht für einen String und einen Integer.

$kuNr = 2001;

$name = "Meyer";

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

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

$stmt->execute();

$stmt->close();

DELETE (Datensatz löschen)

$kuNr = 2001";

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

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

$stmt->execute();

$stmt->close();

Datenbankverbindung schließen

$con->close();

Anmerkungen zu diesem Abschnitt

  • Alle vier Datenbankoperationen verwenden Prepared Statements. Die SQL-Anweisung und die zu verarbeitenden Werte werden getrennt übergeben.
  • Die Platzhalter schützen insbesondere vor SQL-Injection. Sie ersetzen jedoch nicht die Prüfung der eingegebenen Werte.
  • Die Beispiele verwenden absichtlich dieselbe Kundennummer. Deshalb müssen die Operationen in der angegebenen Reihenfolge ausgeführt werden.
  • Nach der abschließenden DELETE-Anweisung ist der zu Beginn eingefügte Kunde 2001 nicht mehr vorhanden.
  • Für die einzelnen Operationen ist keine ausdrücklich gestartete Transaktion erforderlich, weil jede Operation hier nur aus einer einzigen SQL-Anweisung besteht.
  • Sollten mehrere zusammengehörige SQL-Anweisungen gemeinsam ausgeführt werden, ist eine Transaktion erforderlich. Dies wurde bei den Beispielen oben zum Speichern und Löschen vollständiger Rechnungen gezeigt.
  • get_result() setzt bei MySQLi die PHP-Erweiterung mysqlnd voraus. In den XAMPP-Installationen steht diese Erweiterung normalerweise zur Verfügung.

7 KuReAr - Rechnungsverwaltung

Im Folgenden wird ein etwas umfangreicheres Beispiel zu den Grundzügen einer Rechnungsverwaltung vorgestellt. Dabei sollen vollständige Rechnungen mit Rechnungskopf und Rechnungspositionen erfasst werden. Die Programme für die Datenbankzugriffe verwenden PDO.

"KuReAr" steht für "Kunden - Rechnungen - Artikel".

7.1 Startseite und Maskensteuerung

Auf der obersten Ebene wird eine einfache Startseite für die Rechnungsverwaltung KuReAr bereitgestellt. Sie enthält Verweise auf die einzelnen Programme der Anwendung und wird mit folgender Adresse aufgerufen: http://localhost/KuReArStart.php.

Im ersten Schritt werden die Eingabe und die Anzeige von Rechnungen realisiert. Weitere Programme kommen später hinzu.

Ordner-Struktur

Das Programm für die Startseite heißt KuReArStart.php. Die zugehörige CSS-Datei heißt KuReArStart.css. Beide Dateien befinden sich unmittelbar im Verzeichnis htdocs.

Alle weiteren Programme befinden sich im Unterverzeichnis htdocs/KuReAr. Die Verweise der Startseite führen entsprechend zu den Programmen in diesem Unterverzeichnis.

Für die Ordnerstruktur und die Programmaufrufe gilt:

  • Startseite: htdocs/KuReArStart.php. Aufruf: http://localhost/KuReArStart.php. CSS-Datei der Startseite: htdocs/KuReArStart.css
  • Eingabe von Rechnungen: htdocs/KuReAr/EingabeRechnungen.php. Aufruf: http://localhost/KuReAr/EingabeRechnungen.php.
  • Anzeige einer Rechnung nach dem Speichern: htdocs/KuReAr/AnzeigeRechnung.php.

Dieses Programm wird normalerweise von EingabeRechnungen.php aufgerufen. Dabei wird die Rechnungsnummer als URL-Parameter übergeben, beispielsweise:

http://localhost/KuReAr/AnzeigeRechnung.php?ReNr=23001

Die CSS-Datei für obige zwei Programme ist Rechnungen.css.

7.2 Start

7.2.1 KuReArStart.php

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

<!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/EingabeRechnungen.php">

  Rechnungen eingeben</a>

 

<!-- Bearbeitung der Artikeldaten -->

<div class="btn disabled">In Vorbereitung: Artikel bearbeiten</div>

 

<!-- Bearbeitung der Kundendaten -->

<div class="btn disabled">

  In Vorbereitung: Kundendaten bearbeiten</div>

 

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

<hr class="sep">

 

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

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

  Gesamte Datenbank anzeigen</a>

 

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

<div class="btn disabled">

  In Vorbereitung: Liste der Kunden</div>

 

<!-- Ausgabe des aktuellen Artikelbestands -->

<div class="btn disabled">

  In Vorbereitung: Artikelbestand</div>

</nav>

</div>

</body>

</html>

Hinweis zum Navigationselement nav: nav ist keine PHP-Anweisung, sondern ein semantisches HTML-Element. Die Bezeichnung ist von "navigation" abgeleitet. Das Element kennzeichnet einen Bereich mit wichtigen Navigationsverweisen, beispielsweise ein Hauptmenü. Es verändert für sich genommen die Darstellung normalerweise nicht. Seine Hauptaufgabe besteht darin, die Bedeutung des Bereichs zu beschreiben. Das hilft unter anderem Browsern, Suchmaschinen und unterstützenden Programmen wie Screenreadern, den Aufbau der Webseite zu erkennen.

7.2.2 KuReArStart.css

/*

Nachfolgende Festlegung 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.2.3 Erläuterung: "Klassen überall"

In der obigen Datei KuReArStart.css tauchen sehr oft Punkt-Notationen auf, die erst mal verwirrend sind. Deshalb hier eine vertiefte Erläuterung am Beispiel der Festlegungen zu .btn.

.btn

Der Punkt kennzeichnet in CSS einen Klassenselektor . Er wählt HTML-Elemente anhand ihres class-Attributs aus. In obigem Programm:

Klassenselektor

.btn {display: block; padding: 12px 14px; border: 2px solid #000;

  background-color: #eee; color: #000; font-size: 16px;

    text-decoration: none;}

   

Passende HTML-Elemente sind hier beispielsweise:

<a class="btn" href="EingabeRechnungen.php">Rechnungen eingeben</a>

und

<div class="btn">Artikel bearbeiten</div>

Im HTML-Code wird der Klassenname ohne Punkt geschrieben: class="btn". Im CSS-Code steht vor dem Klassennamen ein Punkt: .btn

.btn:hover

Pseudoklassen

In diesem Beispiel

.btn:hover {background-color: #ddd;}

werden zwei Syntaxelemente miteinander verbunden:

  • .btn ist der Klassenselektor.
  • :hover ist eine Pseudoklasse.

:hover bezeichnet den Zustand, in dem sich der Mauszeiger über einem Element befindet. Die Regel bedeutet daher: Wenn sich der Mauszeiger über einem Element der Klasse btn befindet, erhält das Element die Hintergrundfarbe #ddd. Der Doppelpunkt vor hover hat also eine andere Bedeutung als der Punkt vor btn.

.btn.disabled

Zwei Klassen

Dieses Beispiel zeigt eine weitere Variante für den Einsatz des Punkt-Selektors.

.btn.disabled {background-color: #f5f5f5; color: #777;

  cursor: not-allowed;}

Zwischen .btn und .disabled steht kein Leerzeichen. Deshalb bedeutet der Selektor: Wähle Elemente aus, die gleichzeitig den Klassen btn und disabled angehören. Ein passendes HTML-Element ist:

<div class="btn disabled">In Vorbereitung: Artikel bearbeiten</div>

Das Element gehört zwei Klassen an: btn und disabled. Die Klassennamen werden im HTML-Attribut durch ein Leerzeichen getrennt. In der CSS-Regel werden sie dagegen ohne Leerzeichen und jeweils mit einem Punkt geschrieben:

.btn.disabled

Die Regel gilt dann nur für die Kombination, nicht für ein Element, das nur btn oder nur disabled als Klasse besitzt.

!

.btn.disabled:hover

Mit Pseudoklasse

Es ist auch möglich, obige "Zwei-Klassen-Kombination" mit einer Pseudoklasse zu kombinieren. Beispiel:

.btn.disabled:hover {background-color: #f5f5f5;}

Dieser Selektor bedeutet: Wähle ein Element aus, das gleichzeitig den Klassen btn und disabled angehört und über dem sich gerade der Mauszeiger befindet. Die Hintergrundfarbe bleibt dabei #f5f5f5. Dadurch wird die allgemeine Hover-Regel

.btn:hover {background-color: #ddd;}

für deaktivierte Schaltflächen überschrieben. Eine deaktivierte Schaltfläche wird also im obigen Beispiel beim Berühren mit dem Mauszeiger nicht dunkler.

Zusammenfassung: Selektoren mit und ohne Leerzeichen

Diese Schreibweisen haben also unterschiedliche Bedeutungen:

.btn.disabled

und

.btn .disabled

Ersteres bedeutet, dass ein einziges Element zu beiden Klassen gehört. Beispiel:

<div class="btn disabled">In Vorbereitung</div>

Zweiteres bedeutet, dass sich ein Element der Klasse disabled innerhalb eines Elements der Klasse btn befindet. Beispiel:

<div class="btn"><span class="disabled">In Vorbereitung</span></div>

Das Leerzeichen ist in einem CSS-Selektor daher bedeutungstragend.

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").

Im Feld "Anzahl Positionen" wird angegeben, wie viele Positionszeilen angezeigt werden sollen. Nach dem Betätigen der Schaltfläche "Maske anpassen" wird die Eingabemaske entsprechend erweitert oder verkleinert.

Wahl des Kunden, dem die Rechnung gilt.

Über ein Dropdown-Menü wird der Kunde ausgewählt, für den die Rechnung erstellt wird. Die Einträge stammen aus der Relation Kunden. Angezeigt werden der Name, der Vorname und die Kundennummer.

Eingabe des Rechnungsdatums

Das Rechnungsdatum wird in einem Datumsfeld eingegeben.

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. Angezeigt werden die Beschreibung, die Artikelnummer und der Preis.

In der letzten Spalte wird dann noch die Anzahl der Artikel der Position angegeben. Sind dann die Rechnungsdaten eingetragen, werden durch Betätigen der Schaltfläche

die Daten in die Datenbank eingetragen. Nach erfolgreichem Speichern wird die neue Rechnung durch das Programm AnzeigeRechnung.php angezeigt.

7.3.2 EingabeRechnungen.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 den Namen 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 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 bilden eine fachliche Einheit. 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.

IFNULL() liefert den Wert 0, wenn die Relation RechKoepfe noch keinen Datensatz enthält.

*/

$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: AnzeigeRechnung.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="Rechnungen.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 den Namen 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 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. Vollständig leere Positionszeilen werden ignoriert. Ist nur der Artikel oder nur die Anzahl angegeben, wird eine Fehlermeldung erzeugt. Gespeichert werden nur vollständig und gültig ausgefüllte Rechnungspositionen.

Für die neue Rechnung wird eine neue Rechnungsnummer (ReNr) ermittelt. Dazu dient folgende SQL-Abfrage:

SELECT IFNULL(MAX(ReNr), 0) + 1 AS NeueNr

FROM RechKoepfe

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, geeigneter.

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: AnzeigeRechnung.php?ReNr="

    . urlencode((string)$NeueReNr));

Die Rechnungsnummer wird dabei als URL-Parameter an AnzeigeRechnung.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 HTML-Seite:

  • Auswahl des Kunden über ein Dropdown-Menü
  • Eingabe des Rechnungsdatums
  • Tabelle 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

Zum Speichern des Rechnungskopfes und der Rechnungspositionen verwendet das Programm Prepared Statements. Dabei werden die SQL-Anweisungen und die einzusetzenden Werte getrennt an das Datenbanksystem übergeben. Dies schützt insbesondere vor SQL-Injection.

Prepared Statements

Die Funktion htmlspecialchars() sorgt für eine sichere Ausgabe von Daten innerhalb der HTML-Seite. Zeichen mit einer besonderen Bedeutung in HTML werden in HTML-Zeichenreferenzen umgewandelt. Dadurch werden die ausgegebenen Daten als Text dargestellt und nicht als HTML- oder JavaScript-Code interpretiert.

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

7.4 Gesamtausgabe der Rechnungen

Der folgende Button ruft das Programm ListeRechnungen.php auf. Dieses Programm gibt eine Liste aller vorhandenen Rechnungen mit den zugehörigen Kunden-, Positions- und Artikeldaten aus. Dazu werden die vier Relationen Kunden, RechKoepfe, RechPos und Artikel durch drei INNER JOIN-Operationen miteinander verknüpft.

Die Ausgabe dient insbesondere dazu, die Verknüpfung mehrerer Relationen und die Darstellung des Ergebnisses in einer HTML-Tabelle zu demonstrieren.

Außerdem soll hier das Zusammenspiel von PHP und HTML besonders ausführlich dargestellt werden.

7.4.1 ListeRechnungen.php

<?php

/*

Der erste Teil der Datei besteht vollständig aus PHP. Hier werden die Datenbankverbindung aufgebaut, die SQL-Abfrage ausgeführt und die Ergebnisse im Array $rows gespeichert.

Der Browser erhält diesen PHP-Code nicht. Der Webserver führt ihn aus und sendet anschließend nur das erzeugte HTML an den Browser.

*/

// Daten für den Verbindungsaufbau.

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

$user = "root";

$pass = "";

/*

Hilfsfunktion für eine sichere HTML-Ausgabe.

Der übergebene Wert wird zunächst in eine Zeichenkette umgewandelt. Anschließend werden Zeichen mit einer besonderen Bedeutung in HTML in HTML-Zeichenreferenzen umgewandelt.

Die Funktion wird später innerhalb des HTML-Abschnitts aufgerufen, wenn Daten aus der Datenbank ausgegeben werden.

*/

function h(mixed $value): string

{

  return htmlspecialchars(

    (string)$value, ENT_QUOTES, "UTF-8");

}

 

try {

 

// Verbindung zur Datenbank aufbauen.

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

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

/*

Daten aus den vier Relationen miteinander verknüpfen.

INNER JOIN - siehe auch den Anhang - bewirkt, dass nur

Datensätze ausgegeben werden, für die in allen beteiligten

Relationen passende Datensätze vorhanden sind.

*/

$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";

/*

Die SQL-Abfrage ausführen.

fetchAll(PDO::FETCH_ASSOC) liefert sämtliche gefundenen Datensätze als Array von assoziativen Arrays. Das äußere Array $rows enthält alle Datensätze. Jeder Datensatz ist ein inneres assoziatives Array. Beispielsweise enthält $rows[0]["KuNr"] die Kundennummer des ersten gefundenen Datensatzes.

*/

$rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);

 

} catch (PDOException $e) {

 

/*

Tritt beim Verbindungsaufbau oder beim Ausführen der SQL-Anweisung ein Fehler auf, wird eine Meldung ausgegeben und das Programm mit die() beendet.

*/

die("DB-Fehler: " . h($e->getMessage()));

}

 

/*

Das PHP-Endezeichen ?> beendet zunächst den PHP-Abschnitt.

Ab der folgenden Zeile wird der Quelltext als normales HTML behandelt. PHP wird später nur noch dort eingeschaltet, wo Entscheidungen, Schleifen oder dynamische Werte benötigt werden.

*/

?>

 

<!doctype html>

 

<html lang="de">

 

<head>

<meta charset="utf-8">

 

<title>Gesamte Datenbank KuReAr</title>

 

<!-- Externe CSS-Datei für die Gestaltung einbinden. -->

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

</head>

 

<body>

 

<!--

Der folgende Bereich ist zunächst gewöhnliches HTML. Er wird vom Browser unabhängig von der Anzahl der gefundenen Datensätze genau einmal dargestellt.

-->

<div class="box">

 

<div class="caption">

  Daten aus den vier Relationen:

</div>

 

<h2>Die gesamte Datenbank Kunden - Rechnungen - Artikel</h2>

 

<table class="rel">

 

<!--

Der Tabellenkopf besteht vollständig aus HTML. Er wird genau einmal ausgegeben.

-->

<thead>

  <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>

 

<!--

An dieser Stelle wird PHP innerhalb des HTML-Abschnitts wieder eingeschaltet.

Die alternative Schreibweise

  if (Bedingung):

eignet sich besonders, wenn eine PHP-Bedingung mit einem längeren HTML-Abschnitt verbunden wird.

count($rows) ermittelt die Anzahl der gefundenen Datensätze.

-->

<?php if (count($rows) === 0): ?>

 

<!--

Dieser HTML-Abschnitt wird nur ausgegeben, wenn die Abfrage keinen Datensatz geliefert hat.

colspan="9" verbindet alle neun Spalten der Zeile.

-->

<tr>

  <td colspan="9">Keine Rechnungen vorhanden.</td>

</tr>

 

<?php else: ?>

 

<!--

Die foreach-Schleife durchläuft alle Datensätze im Array $rows.

Bei jedem Schleifendurchlauf enthält $r genau einen Datensatz als assoziatives Array.

Der zwischen foreach und endforeach stehende HTML-Code wird für jeden Datensatz wiederholt.

-->

<?php foreach ($rows as $r): ?>

<tr>

<!--

<?= ... ?> ist die Kurzform für: <?php echo ...; ?>

Der Wert wird damit unmittelbar an dieser Stelle in das HTML-Dokument eingesetzt. Die Funktion h() sorgt dafür, dass der Wert sicher als HTML-Text ausgegeben wird.

-->

  <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>

 

<!--

Der Preis wird zunächst mit number_format() formatiert:

- zwei Nachkommastellen,

- Komma als Dezimalzeichen,

- Punkt als Tausendertrennzeichen.

Anschließend wird das Ergebnis mit h() HTML-sicher ausgegeben.

-->

<td>

  <?= h(number_format((float)$r["Preis"],2,",",".")) ?>

</td>

<td><?= h($r["Anzahl"]) ?></td>

</tr>

 

<!-- Ende der foreach-Schleife. -->

<?php endforeach; ?>

 

<!-- Ende der if/else-Bedingung. -->

<?php endif; ?>

 

</tbody>

</table>

<!--

Der folgende Abschnitt liegt außerhalb der PHP-Bedingung und der PHP-Schleife. Er wird deshalb immer genau einmal ausgegeben.

-->

<div class="actions">

  <a class="btn" href="../KuReArStart.php">

    Zurück zum Hauptmenü

  </a>

</div>

 

</div> <!-- Ende von class="box" -->

 

</body>

 

</html>

7.4.2 Programmlauf

Das Anklicken der Schaltfläche "Liste der Rechnungen" ruft das Programm ListeRechnungen.php auf. Das Programm verbindet die Daten aus den vier Relationen Kunden, RechKoepfe, RechPos und Artikel. Anschließend werden die gefundenen Rechnungspositionen in einer HTML-Tabelle ausgegeben.

Jede Zeile der Tabelle entspricht einer Rechnungsposition. Besitzt eine Rechnung mehrere Positionen, werden die Rechnungsnummer, die Kundennummer, der Kundenname und das Rechnungsdatum deshalb in mehreren Zeilen wiederholt.

Sind keine Rechnungen vorhanden, erscheint innerhalb der Tabelle die Meldung: Keine Rechnungen vorhanden.

Das Anklicken dieser Schaltfläche

führt somit zu folgender Ausgabe:

7.4.3 Anmerkungen zum Programm

Zusammenspiel von PHP und HTML

Das Programm zeigt drei typische Formen des Zusammenspiels:

  • PHP vor dem HTML-Abschnitt

Zunächst werden die Datenbankverbindung hergestellt und die Daten abgerufen:

<?php

  $rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);

?>

  • PHP steuert die Ausgabe von HTML

Eine PHP-Bedingung entscheidet, welcher HTML-Abschnitt ausgegeben wird:

<?php if (count($rows) === 0): ?>

  <p>Keine Rechnungen vorhanden.</p>

<?php else: ?>

  <!-- Ausgabe der Datensätze -->

<?php endif; ?>

  • PHP setzt einzelne Werte in HTML ein

Die Kurzform <?= ... ?> gibt einen Wert an der betreffenden Stelle aus:

<td><?= h($r["KuName"]) ?></td>

Der Webserver verarbeitet zuerst sämtliche PHP-Anweisungen. Das daraus entstehende HTML-Dokument wird anschließend an den Browser gesendet. Der Browser erhält daher beispielsweise:

<td>1002</td>

<td>Hinterhof</td>

<td>23001</td>

Er erhält jedoch nicht die zugrunde liegende PHP-Anweisung:

<td><?= h($r["KuName"]) ?></td>

Zur Programmierung

  • Die Hilfsfunktion h() fasst die wiederholt benötigte sichere HTML-Ausgabe zusammen. Sie wandelt den übergebenen Wert zunächst in eine Zeichenkette um und ruft anschließend htmlspecialchars() mit ENT_QUOTES und dem Zeichensatz "UTF-8" auf.
  • Die SQL-Abfrage enthält keine vom Benutzer eingegebenen Werte. Deshalb ist an dieser Stelle kein Prepared Statement erforderlich. Die Abfrage kann unmittelbar mit $pdo->query($sql) ausgeführt werden.
  • fetchAll(PDO::FETCH_ASSOC) liest alle gefundenen Ergebnisdatensätze und liefert sie als Array. Jeder Ergebnisdatensatz wird dabei als assoziatives Array bereitgestellt.
  • Die Abfrage verwendet drei INNER JOIN-Operationen. Dadurch werden nur Datensätze ausgegeben, für die in allen vier Relationen passende Datensätze vorhanden sind.
  • Kunden ohne Rechnungen erscheinen nicht in der Ausgabe. Ebenso erscheinen keine Rechnungsköpfe ohne Rechnungspositionen und keine Rechnungspositionen, denen kein vorhandener Artikel zugeordnet werden kann.
  • DATE_FORMAT() wandelt das gespeicherte Rechnungsdatum für die Ausgabe vom Format Jahr-Monat-Tag in das deutsche Format Tag.Monat.Jahr um. DATE_FORMAT() ist eine Funktion von MySQL beziehungsweise MariaDB und nicht Bestandteil von PHP.
  • Die Ausgabe stellt fast die gesamte Datenbank dar. Nur die Kundenvornamen wurden aus Platzgründen weggelassen.
  • Eine Rechnung mit mehreren Positionen erscheint in mehreren Ergebniszeilen. Die Angaben zum Rechnungskopf und zum Kunden werden dabei wiederholt. Diese redundante Darstellung wird hier bewusst akzeptiert, weil jede Zeile eine vollständige Rechnungsposition beschreibt.
  • Die Reihenfolge ORDER BY rk.ReNr, rp.PosNr sortiert die Ausgabe zunächst nach der Rechnungsnummer und innerhalb einer Rechnung nach der Positionsnummer.
  • number_format() formatiert den Preis mit zwei Nachkommastellen, einem Komma als Dezimaltrennzeichen und einem Punkt als Tausendertrennzeichen.

7.5 Anzeigen einer Rechnung

Dieses Programm wird derzeit nach dem Eintragen einer neuen Rechnung genutzt, um diese anzuzeigen.

7.5.1 AnzeigeRechnung.php

<?php

 

// AnzeigeRechnung.php - Anzeige einer Rechnung mit

// Kopf und Positionen

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

$user = "root";

$pass = "";

 

/*

Hilfsfunktion für eine sichere HTML-Ausgabe.

mixed erlaubt die Übergabe unterschiedlicher Datentypen. Der Wert wird vor der Übergabe an htmlspecialchars() in eine Zeichenkette umgewandelt.

*/

function h(mixed $value): string

{

  return htmlspecialchars((string)$value, ENT_QUOTES, "UTF-8");

}

 

/*

Rechnungsnummer aus dem URL-Parameter ReNr übernehmen. Wurde der Parameter nicht übertragen, wird 0 verwendet.

*/

$ReNr = isset($_GET["ReNr"]) ? (int)$_GET["ReNr"] : 0;

if ($ReNr <= 0) {

  die("Fehler: ReNr fehlt oder ist ungültig.");

}

 

// try-/catch-Block beginnt

try {

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

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

 

/*

Rechnungskopf und zugehörige Kundendaten laden. Das Fragezeichen ist ein Platzhalter für die Rechnungsnummer.

*/

$stmtHead = $pdo->prepare(

"SELECT rk.ReNr, rk.ReDatum, rk.KuNr, k.Name, k.Vorname

  FROM RechKoepfe AS rk

  INNER JOIN Kunden AS k ON rk.KuNr = k.KuNr

  WHERE rk.ReNr = ?");

 

$stmtHead->execute([$ReNr]);

$head = $stmtHead->fetch(PDO::FETCH_ASSOC);

 

if (!$head) {

  die("Fehler: Rechnung nicht gefunden (ReNr " . h($ReNr) . ").");

}

/*

Rechnungspositionen und zugehörige Artikeldaten laden.

Der Positionswert wird in der SQL-Anweisung aus Anzahl und Preis berechnet.

*/

$stmtPos = $pdo->prepare(

"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 = ?

  ORDER BY rp.PosNr"

);

$stmtPos->execute([$ReNr]);

$pos = $stmtPos->fetchAll(PDO::FETCH_ASSOC);

 

/*

Rechnungssumme aus den Positionswerten berechnen.

*/

$summe = 0.0;

 

foreach ($pos as $p) {

  $summe += (float)$p["Positionswert"];

}

} catch (PDOException $e) {

  die("DB-Fehler: " . h($e->getMessage()));

}

?>

 

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Rechnung anzeigen</title>

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

</head>

<body>

 

<div class="box">

 

<div class="caption">

Rechnung wurde gespeichert - Anzeige der neuen Rechnung

</div>

 

<h2>Rechnung <?= h($head["ReNr"]) ?></h2>

 

<div class="info">

<div><b>ReNr:</b> <?= h($head["ReNr"]) ?></div>

<div><b>ReDatum:</b> <?= h($head["ReDatum"]) ?></div>

<div><b>KuNr:</b> <?= h($head["KuNr"]) ?></div>

<div><b>Kunde:</b>

<?= h($head["Name"] . ", " . $head["Vorname"]) ?></div>

<div><b>Rechnungssumme:</b>

<?= h(number_format($summe, 2, ",", ".")) ?> EUR</div>

</div>

 

<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>

 

<?php if (count($pos) === 0): ?>

 

<tr>

<td colspan="6">Keine Rechnungspositionen vorhanden.</td>

</tr>

 

<?php else: ?>

 

<?php foreach ($pos as $p): ?>

 

<tr>

<td><?= h($p["PosNr"]) ?></td>

<td><?= h($p["ArtNr"]) ?></td>

<td><?= h($p["Beschreibung"]) ?></td>

<td><?= h(number_format((float)$p["Preis"], 2, ",", ".")) ?> EUR</td>

<td><?= h($p["Anzahl"]) ?></td>

<td><?= h(number_format((float)$p["Positionswert"], 2, ",", ".")) ?>

EUR</td>

</tr>

<?php endforeach; ?>

<?php endif; ?>

</tbody>

</table>

<div class="actions">

  <a class="btn" href="EingabeRechnungen.php">

    Neue Rechnung erfassen</a>

  <a class="btn" href="../KuReArStart.php">Zurück zum Hauptmenü</a>

</div>

</div>

</body>

</html>

7.5.2 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");

}

h() ist keine vorgegebene PHP-Funktion, sondern eine in diesem Programm selbst definierte Hilfsfunktion. Die Kurzbezeichnung steht hier sinngemäß für "HTML-sichere Ausgabe".

Die erste Zeile function h(mixed $value): string definiert die Funktion:

  • function leitet die Funktionsdefinition ein.
  • h ist der Name der Funktion.
  • $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>

Dabei wird der Inhalt von $kunde["Name"] an h() übergeben, für die HTML-Ausgabe behandelt und anschließend durch <?= ... ?> ausgegeben. Die Funktion verändert weder den ursprünglichen Wert noch den Datenbankinhalt. Sie behandelt den Wert ausschließlich für die Ausgabe in HTML.

Die Funktion h() schützt allerdings nicht allgemein vor allen Angriffen. Für SQL-Anweisungen müssen weiterhin Prepared Statements verwendet werden. Für andere Ausgabekontexte, beispielsweise JavaScript, CSS oder URLs, können ebenfalls andere Schutzmaßnahmen erforderlich sein.

7.6 Rechnungen.css

Dies ist die Stylesheet-Datei für die obigen Programme. Die Erläuterungen sind hier besonders umfangreich, weil auch fortgeschrittene CSS-Techniken verwendet werden.

/*

box-sizing: border-box bewirkt, dass die mit width und height festgelegten Größen bereits die Innenabstände und Rahmen enthalten. Dadurch lassen sich insbesondere Eingabefelder, Tabellen und die äußere Box leichter an die verfügbare Breite anpassen. Durch den Universalselektor "*" gelten diese Festlegungen für alle HTML-Elemente.

*/

* {box-sizing: border-box;}

 

/*

Grundlegende Gestaltung des sichtbaren Seitenbereichs.

font-family legt Arial als bevorzugte Schriftart fest. Falls Arial nicht verfügbar ist, verwendet der Browser eine andere serifenlose Schriftart.

line-height legt die Zeilenhöhe auf das 1,4-Fache der jeweiligen Schriftgröße fest.

margin entfernt den vom Browser standardmäßig vorgegebenen Außenabstand.

padding erzeugt auf allen Seiten einen Abstand von 20 Pixeln zwischen dem Rand des Browserfensters und dem Inhalt der Webseite.

*/

body {font-family: Arial, sans-serif; line-height: 1.4;

  margin: 0; padding: 20px;}

 

/*

Äußerer Bereich der Eingabemaske, des Hauptmenüs beziehungsweise der Rechnungsausgabe.

margin: 0 auto erzeugt oben und unten keinen zusätzlichen Außenabstand. Der Wert auto für links und rechts zentriert die Box horizontal.

max-width begrenzt die Breite auf höchstens 1200 Pixel. Diese Breite bietet auch der Gesamtausgabe mit neun Spalten ausreichend Platz.

width: 100% bewirkt, dass die Box in einem schmaleren Browserfenster grundsätzlich die gesamte verfügbare Breite einnimmt.

*/

.box {margin: 0 auto; max-width: 1200px; width: 100%;}

 

/*

Gestaltung der Hauptüberschrift des Hauptmenüs.

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

margin legt die Außenabstände fest:

- oben: 6 Pixel

- rechts: 0

- unten: 18 Pixel

- links: 0

text-align: center richtet die Überschrift innerhalb ihres Bereichs waagerecht zentriert aus.

*/

h1 {font-size: 28px; margin-bottom: 18px; margin-left: 0;

  margin-right: 0; margin-top: 6px; text-align: center;}

 

/*

Gestaltung der Überschrift zweiter Ordnung.

margin legt die Außenabstände fest:

- oben: 10 Pixel

- rechts: 0

- unten: 14 Pixel

- links: 0

text-align: center zentriert den Text innerhalb des Überschriftenbereichs.

*/

h2 {margin: 10px 0 14px; text-align: center;}

 

/*

Gestaltung der Beschriftung oberhalb der Eingabemaske beziehungsweise Rechnungsausgabe.

color 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 {color: #c00; font-weight: 700; margin: 0 0 8px;}

 

/*

Gestaltung des Informationsbereichs bei der Anzeige

einer einzelnen Rechnung.

background-color legt einen hellgrauen Hintergrund fest. border erzeugt einen schwarzen, durchgezogenen Rahmen mit einer Stärke von 2 Pixeln.

display: grid aktiviert das Grid-Verfahren. Die unmittelbar enthaltenen div-Elemente werden dabei

untereinander angeordnet.

gap erzeugt zwischen den einzelnen Angaben einen

Abstand von 6 Pixeln.

Die margin-Eigenschaften legen die Außenabstände einzeln für alle vier Seiten fest.

Die padding-Eigenschaften legen die Innenabstände einzeln für alle vier Seiten fest.

*/

.info {background-color: #f5f5f5; border: 2px solid #000;

    display: grid; gap: 6px; margin-bottom: 15px; margin-left: 0;

    margin-right: 0; margin-top: 10px; padding-bottom: 10px;

    padding-left: 12px; padding-right: 12px; padding-top: 10px;}

 

/*

Grundlegende Gestaltung der Meldungsbereiche.

background-color legt Weiß als Hintergrundfarbe fest.

border erzeugt einen schwarzen, durchgezogenen Rahmen mit einer Stärke von 2 Pixeln.

margin erzeugt oberhalb und unterhalb der Meldung einen Außenabstand von 10 Pixeln.

padding erzeugt auf allen Seiten einen Innenabstand von 10 Pixeln.

*/

.msg {background-color: #fff; border: 2px solid #000;

  margin: 10px 0; padding: 10px;}

 

/*

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 beziehungsweise der Gesamtausgabe.

border erzeugt einen schwarzen Außenrahmen mit einer Stärke von 3 Pixeln.

border-collapse: collapse führt die Rahmen benachbarter Zellen zu einer gemeinsamen Linie zusammen.

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

width: 100% bewirkt, dass die Tabelle die gesamte verfügbare Breite einnimmt.

*/

table.rel {border: 3px solid #000; border-collapse: collapse;

  font-size: 15px; width: 100%;}

 

/*

Gemeinsame Gestaltung der Kopf- und Datenzellen.

background-color legt Weiß als Hintergrundfarbe fest.

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.

*/

table.rel th, table.rel td {background-color: #fff;

  border: 2px solid #000; padding: 6px 10px; 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 beziehungsweise der Rechnungsausgabe.

display: flex aktiviert das Flexbox-Verfahren.

flex-wrap: wrap erlaubt einen Zeilenumbruch, wenn auf

einem kleinen Bildschirm nicht genügend Platz vorhanden ist.

gap legt zwischen den Schaltflächen einen Abstand von 10 Pixeln fest.

margin-top erzeugt oberhalb des Bereichs einen Außenabstand von 12 Pixeln.

*/

.actions {display: flex; flex-wrap: wrap; gap: 10px;

  margin-top: 12px;}

 

/*

Gestaltung der als Schaltflächen dargestellten Verweise.

background-color legt einen hellgrauen Hintergrund fest.

border erzeugt einen schwarzen, durchgezogenen Rahmen mit einer Stärke von 2 Pixeln.

color legt Schwarz als Schriftfarbe fest.

curor: pointer zeigt beim Berühren des Verweises mit der Maus einen Handzeiger an.

display: inline-block ermöglicht Innenabstände und einen Rahmen wie bei einem Blockelement, ohne dass der Verweis automatisch die gesamte verfügbare Breite einnimmt.

font-family und font-size übernehmen Schriftart und Schriftgröße vom übergeordneten Element.

Die padding-Eigenschaften legen die Innenabstände einzeln für alle vier Seiten fest.

text-decoration: none entfernt die für Verweise übliche Unterstreichung.

*/

.btn {background-color: #eee; border: 2px solid #000; color: #000; cursor: pointer; display: inline-block; font-family: inherit; font-size: inherit; padding-bottom: 6px; padding-left: 10px; padding-right: 10px; padding-top: 6px; text-decoration: none;}

 

/*

Gestaltung eines als Schaltfläche dargestellten Verweises, wenn sich der Mauszeiger darüber befindet.

Die etwas dunklere Hintergrundfarbe gibt dem Benutzer eine optische Rückmeldung.

*/

.btn:hover {background-color: #ddd;}

 

/*

Gestaltung des Hauptmenüs.

display: flex aktiviert das Flexbox-Verfahren.

flex-direction: column ordnet die unmittelbar im Navigationsbereich enthaltenen Elemente senkrecht untereinander an.

gap erzeugt zwischen den Menüelementen einen Abstand von 12 Pixeln.

margin-left und margin-right mit dem Wert auto zentrieren das Menü innerhalb der äußeren Box.

max-width begrenzt die Breite des Hauptmenüs auf 500 Pixel.

width: 100% bewirkt, dass das Menü in einem schmaleren Browserfenster die verfügbare Breite einnimmt.

*/

.menu {display: flex; flex-direction: column; gap: 12px; margin-left: auto; margin-right: auto; max-width: 500px; width: 100%;}

 

/*

Gestaltung der Schaltflächen innerhalb des Hauptmenüs.

display: block stellt den Verweis als Blockelement dar.

text-align: left richtet den Text linksbündig aus.

width: 100% bewirkt, dass alle Menüschaltflächen die gesamte Breite des Hauptmenüs einnehmen.

*/

.menu .btn {display: block; text-align: left; width: 100%;}

 

/*

Gestaltung eines deaktivierten Menüeintrags.

background-color legt einen besonders hellgrauen Hintergrund fest.

color legt Grau als Schriftfarbe fest.

cursor: not-allowed zeigt beim Berühren des Eintrags mit der Maus an, dass dieser nicht ausgewählt werden kann.

*/

.menu .btn.disabled {background-color: #f5f5f5; color: #777; cursor: not-allowed;}

 

/*

Die Hintergrundfarbe eines deaktivierten Menüeintrags bleibt auch dann unverändert, wenn sich der Mauszeiger darüber befindet.

*/

.menu .btn.disabled:hover {background-color: #f5f5f5;}

 

/*

Gestaltung der Trennlinie zwischen den Bereichen des Hauptmenüs.

border: none entfernt die Standarddarstellung des hr-Elements.

border-top erzeugt stattdessen eine schwarze, durchgezogene obere Linie mit einer Stärke von 2 Pixeln.

margin legt oben und unten einen Außenabstand von 6 Pixeln fest. Links und rechts entsteht kein zusätzlicher Außenabstand.

width: 100% lässt die Trennlinie die gesamte Breite des Hauptmenüs einnehmen.

*/

.sep {border: none; border-top: 2px solid #000; margin-bottom: 6px; margin-left: 0; margin-right: 0; margin-top: 6px; width: 100%;}

 

/*

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.

max-width: 100% verhindert, dass eine Auswahlliste über den umgebenden Bereich hinausragt.

width: 100% bewirkt, dass die Auswahlliste grundsätzlich die gesamte verfügbare Breite einnimmt.

*/

select {max-width: 100%; width: 100%;}

 

/*

Grundlegende Gestaltung aller wirklichen Schaltflächen.

background-color legt einen hellgrauen Hintergrund fest.

border erzeugt einen schwarzen, durchgezogenen Rahmen mit einer Stärke von 2 Pixeln.

cursor: pointer zeigt beim Berühren der Schaltfläche mit der Maus einen Handzeiger an.

font-family und font-size übernehmen Schriftart und Schriftgröße vom übergeordneten Element.

Die padding-Eigenschaften legen die Innenabstände einzeln für alle vier Seiten fest.

*/

button {background-color: #eee; border: 2px solid #000;

  cursor: pointer; font-family: inherit; font-size: inherit;

  padding-bottom: 6px; padding-left: 10px;

  padding-right: 10px; padding-top: 6px;}

 

/*

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;}

 

/*

Gestaltung des Tastaturfokus.

Wenn eines der genannten Elemente mit der Tabulatortaste ausgewählt wird, erscheint ein deutlich sichtbarer blauer Rahmen.

outline-offset erzeugt einen kleinen Abstand zwischen dem Element und dem zusätzlichen Rahmen.

*/

.btn:focus-visible, button:focus-visible, input:focus-visible,

  select:focus-visible {outline: 3px solid #06c;

  outline-offset: 2px;}

 

/*

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. Die Mindestbreite von 850 Pixeln verhindert, dass die Spalten der umfangreichen Gesamtausgabe übermäßig zusammengedrückt werden.

*/

@media (max-width: 700px) {

  .box {overflow-x: auto;}

  table.rel {min-width: 850px;}

}

8 Kompakte Übersicht

Hier folgt eine Zusammenfassung der oben beschriebenen Möglichkeiten, mit PHP auf eine relationale Datenbank zuzugreifen. Verglichen werden die beiden PHP-Schnittstellen MySQLi und PDO.

Sowohl MySQLi als auch PDO unterstützen Prepared Statements. Bei korrekter Verwendung werden SQL-Anweisung und zu verarbeitende Werte getrennt übergeben. Dies schützt insbesondere vor SQL-Injection.

8.1 MySQLi

MySQLi bedeutet "MySQL Improved". Die Schnittstelle wurde speziell für den Zugriff auf MySQL und kompatible Datenbanksysteme entwickelt. Sie kann in PHP objektorientiert oder prozedural verwendet werden. In den vorliegenden Programmen wird die objektorientierte Form verwendet.

8.1.1 Typischer Ablauf

  • MySQLi-Verbindung aufbauen
  • Zeichensatz der Verbindung festlegen
  • SQL-Anweisung mit prepare() vorbereiten
  • Werte mit bind_param() an die Platzhalter binden
  • Anweisung mit execute() ausführen
  • Bei einer SELECT-Anweisung das Ergebnis mit get_result() abrufen
  • Datensätze beispielsweise mit fetch_assoc() lesen
  • Fehler behandeln
  • Statement und Datenbankverbindung schließen

Ein typisches Beispiel:

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

$con->set_charset("utf8mb4");

 

$stmt = $con->prepare(

  "SELECT * FROM kunden WHERE KuNr = ?"

);

 

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

$stmt->execute();

 

$result = $stmt->get_result();

$row = $result->fetch_assoc();

 

$stmt->close();

$con->close();

Das Fragezeichen ist ein positionsbasierter Platzhalter. Der Aufruf

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

bindet die Variable $kuNr an diesen Platzhalter. Das Typkennzeichen "i" legt fest, dass der Wert als Integer behandelt wird.

8.1.2 Vorteile

  • Speziell für MySQL und kompatible Datenbanksysteme entwickelt
  • Unterstützung von Prepared Statements
  • Ausdrückliche Festlegung der Parametertypen durch Kennzeichen wie i, s, d und b
  • Objektorientierte und prozedurale Verwendung möglich
  • Direkter Zugriff auf MySQL-spezifische Funktionen
  • Gute Eignung für Anwendungen, die ausschließlich MySQL oder MariaDB verwenden

8.1.3 Fehlerbehandlung

In älteren PHP-Programmen wurden MySQLi-Fehler häufig nach jedem Methodenaufruf manuell geprüft:

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

  die("Fehler: " . $stmt->error);

}

In aktuellen PHP-Versionen können MySQLi-Fehler auch als Exceptions behandelt werden. Dies ist insbesondere in Verbindung mit einem try/catch-Block möglich.

Für eine ausdrückliche Festlegung des Verhaltens kann am Programmanfang geschrieben werden:

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

Anschließend können Datenbankfehler beispielsweise so behandelt werden:

try {

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

} catch (mysqli_sql_exception $e) {

  die("Datenbankfehler: " . $e->getMessage());

}

Vgl. die zahlreichen Beispiele hierzu in den Programmen oben.

8.2 PDO

PDO bedeutet "PHP Data Objects". PDO stellt eine weitgehend einheitliche Schnittstelle für den Zugriff auf unterschiedliche Datenbanksysteme zur Verfügung. Welches Datenbanksystem angesprochen wird, legt der verwendete PDO-Treiber fest.

In den hier vorliegenden Programmen wird im DSN mit mysql: der MySQL-Treiber ausgewählt.

8.2.1 Typischer Ablauf

  • PDO-Verbindung über einen DSN herstellen
  • Fehlerbehandlung mit Exceptions aktivieren
  • SQL-Anweisung mit prepare() vorbereiten
  • Werte beim Aufruf von execute() übergeben
  • Bei einer SELECT-Anweisung Ergebnisse mit fetch() oder fetchAll() abrufen
  • Fehler mit try/catch behandeln

Ein typisches Beispiel mit einem benannten Platzhalter:

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

 

$pdo = new PDO($dsn, "root", "", [

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

   

$stmt = $pdo->prepare(

  "SELECT * FROM kunden WHERE KuNr = :kuNr");

   

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

 

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

:kuNr ist ein benannter Platzhalter. Das Array beim Aufruf von execute() ordnet diesem Platzhalter den Wert der Variablen $kuNr zu.

PDO unterstützt außerdem positionsbasierte Platzhalter:

$stmt = $pdo->prepare(

  "SELECT * FROM kunden WHERE KuNr = ?");

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

Die Werte können bei PDO auch vor dem Aufruf von execute() mit bindValue() oder bindParam() gebunden werden. In den hier verwendeten Programmen werden sie meistens unmittelbar als Array an execute() übergeben.

8.2.2 Vorteile

  • Weitgehend einheitliche Schnittstelle für unterschiedliche Datenbanksysteme
  • Unterstützung von Prepared Statements
  • Unterstützung von benannten und positionsbasierten Platzhaltern
  • Einfache Parameterübergabe als Array an execute()
  • Übersichtliche Fehlerbehandlung mit Exceptions
  • Flexible Ergebnisverarbeitung mit fetch() und fetchAll()
  • Keine ausdrückliche Typangabe erforderlich, wenn die Werte als Array an execute() übergeben werden

8.2.3 Einschränkung der Datenbankunabhängigkeit

PDO vereinheitlicht den grundsätzlichen Ablauf des Datenbankzugriffs. Das bedeutet jedoch nicht, dass ein PHP-Programm ohne Änderungen mit jedem Datenbanksystem verwendet werden kann. Unterschiede können sich insbesondere ergeben bei:

  • SQL-Dialekten,
  • Datentypen,
  • eingebauten SQL-Funktionen,
  • der Behandlung automatisch erzeugter Schlüssel,
  • der Schreibweise von Datumsfunktionen,
  • datenbankspezifischen Erweiterungen.

Beispielsweise ist

DATE_FORMAT(ReDatum, '%d.%m.%Y')

eine MySQL-spezifische Funktion. Bei einem anderen Datenbanksystem müsste dieser Teil der SQL-Anweisung möglicherweise geändert werden.

8.3 Der direkte Vergleich

8.3.1 Eigenschaften


Merkmal

PDO

MySQLi

Vollständige Bezeichnung

PHP Data Objects

MySQL Improved

Unterstützte Datenbanksysteme

Mehrere Datenbanksysteme über unterschiedliche Treiber

MySQL und kompatible Systeme

Verbindungsaufbau

Über einen DSN

Über Host, Benutzer, Passwort und Datenbank

Prepared Statements

Ja

Ja

Benannte Platzhalter

Ja, zum Beispiel :kuNr

In PHP 8.2 üblicherweise nein

Positionsbasierte Platzhalter

Ja, mit ?

Ja, mit ?

Parameterübergabe

Häufig als Array an execute()

Mit bind_param()

Ausdrückliche Typkennzeichen

Beim Array für execute() normalerweise nicht erforderlich

Bei bind_param() erforderlich

Typische Typkennzeichen

Nicht erforderlich

i, s, d, b

Abruf eines Datensatzes

fetch()

fetch_assoc() über das Ergebnisobjekt

Abruf aller Datensätze

fetchAll()

Schleife mit fetch_assoc()

Fehlerbehandlung

Typischerweise Exceptions mit try/catch

Manuelle Prüfung oder Exceptions

Objektorientierte Verwendung

Ja

Ja

Prozedurale Verwendung

Nein

Ja

Datenbankwechsel

Grundstruktur kann häufig beibehalten werden

Nicht vorgesehen

SQL-Anpassungen beim Datenbankwechsel

Häufig trotzdem erforderlich

Entfällt, da auf MySQL ausgerichtet

8.3.2 Typischer Ablauf


Schritt

PDO

MySQLi

1

Verbindung mit new PDO() herstellen

Verbindung mit new mysqli() herstellen

2

SQL-Anweisung mit prepare() vorbereiten

SQL-Anweisung mit prepare() vorbereiten

3

Werte als Array an execute() übergeben und Anweisung ausführen

Werte mit bind_param() binden

4

Entfällt, da execute() im vorherigen Schritt bereits ausgeführt wurde

Anweisung mit execute() ausführen

5

Ergebnisse mit fetch() oder fetchAll() abrufen

Ergebnis mit get_result() und Datensätze mit fetch_assoc() abrufen

6

Fehler typischerweise mit try/catch behandeln

Fehler manuell oder mit try/catch behandeln

7

Verbindung wird normalerweise automatisch freigegeben

Statement und Verbindung üblicherweise mit close() schließen

9 Zusammenspiel von HTML und PHP

9.1 Aufgaben

HTML und PHP übernehmen bei einer Webanwendung unterschiedliche Aufgaben:

  • HTML beschreibt den Aufbau und den Inhalt einer Webseite.
  • CSS gestaltet die Darstellung der HTML-Elemente.
  • PHP verarbeitet Daten, prüft Bedingungen, führt Berechnungen durch, greift auf Datenbanken zu und erzeugt bei Bedarf HTML-Code.
  • Der Webserver erkennt eine PHP-Datei und lässt den darin enthaltenen PHP-Code vom PHP-Interpreter ausführen.
  • Der Browser erhält anschließend das von PHP erzeugte Ergebnis. Dabei handelt es sich häufig um ein HTML-Dokument.
  • Der PHP-Quellcode selbst wird normalerweise nicht an den Browser übertragen.

Der grundsätzliche Ablauf ist somit:

  • Der Browser fordert eine PHP-Datei beim Webserver an.
  • Der Webserver lässt den darin enthaltenen PHP-Code ausführen.
  • PHP erzeugt eine Ausgabe, beispielsweise HTML.
  • Der Webserver sendet die erzeugte Ausgabe an den Browser.
  • Der Browser verarbeitet das empfangene HTML und stellt die Webseite dar.

9.2 Möglichkeiten des Zusammenspiels

HTML und PHP können innerhalb einer PHP-Datei auf unterschiedliche Arten miteinander verbunden werden. Vgl. auch die Fundstellen zum Begriff "Zusammenspiel" im Text, insbesondere Abschnitt 7.4.

9.2.1 HTML außerhalb von PHP schreiben

Dies ist die einfachste Form, die auch oben meist gewählt wurde. Der größte Teil der Datei besteht aus gewöhnlichem 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>

Der Browser erhält nach der Ausführung beispielsweise:

<!doctype html>

<html lang="de">

<head>

  <meta charset="utf-8">

  <title>Kundendaten</title>

</head>

<body>

 

<h1>Relation Kunden</h1>

Die Daten wurden geladen.

</body>

</html>

Diese Form eignet sich besonders gut, wenn die Seite überwiegend aus HTML besteht und nur an einzelnen Stellen PHP benötigt wird.

9.2.2 HTML mit echo durch PHP ausgeben

PHP kann vollständige HTML-Elemente 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 durch doppelte Anführungszeichen begrenzten PHP-Zeichenkette ebenfalls doppelte Anführungszeichen stehen, müssen die inneren Anführungszeichen mit einem umgekehrten Schrägstrich geschützt werden:

echo "<p class=\"text-red\">Relation Kunden</p>";

Der umgekehrte Schrägstrich wird im Englischen als "backslash" bezeichnet. Er sorgt hier dafür, dass das folgende Anführungszeichen nicht als Ende der PHP-Zeichenkette interpretiert wird.

Einfacher ist in diesem Fall eine außen durch einfache Anführungszeichen begrenzte Zeichenkette:

echo '<p class="text-red">Relation Kunden</p>';

Da die äußere Zeichenkette durch einfache Anführungszeichen begrenzt wird, können die doppelten Anführungszeichen der HTML-Attribute unverändert verwendet werden.

9.2.3 Werte mit der Kurzform <?= ... ?> einsetzen

Mit der Kurzform <?= ... ?> kann das Ergebnis eines PHP-Ausdrucks unmittelbar in HTML eingesetzt werden:

<p>Kundennummer: <?= $kuNr ?></p>

Dies ist die Kurzform für:

<p>Kundennummer: <?php echo $kuNr; ?></p>

Bei Texten, die beispielsweise aus einem Formular oder einer Datenbank stammen, müssen Zeichen mit einer besonderen HTML-Bedeutung bearbeitet werden:

<p>Kundenname:

<?= htmlspecialchars((string)$name, ENT_QUOTES, "UTF-8") ?>

</p>

In den Programmen dieses Textes wird dafür häufig die Hilfsfunktion h() verwendet:

<?php

function h(mixed $value): string

{

  return htmlspecialchars((string)$value, ENT_QUOTES, "UTF-8");

}

?>

Dann genügt:

<p>Kundenname: <?= h($name) ?></p>

Diese Schreibweise ist besonders gut geeignet, um einzelne Werte in eine bereits vorhandene HTML-Struktur einzusetzen.

9.2.4 PHP-Bedingungen mit HTML verbinden

Soll ein HTML-Abschnitt nur unter einer bestimmten Bedingung ausgegeben werden, kann die alternative PHP-Schreibweise verwendet werden:

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

<p><?= h($message) ?></p>

<?php endif; ?>

Dies bedeutet: Wenn die Variable $message keine leere Zeichenkette enthält, wird das HTML-Element <p> mit dem Meldungstext ausgegeben. Der Doppelpunkt nach der Bedingung

if ($message !== ""):

leitet den bedingt auszuführenden Abschnitt ein. Die Anweisung endif; beendet diesen Abschnitt. Dieselbe Verarbeitung könnte vollständig innerhalb von PHP geschrieben werden:

<?php

if ($message !== "") {

  echo "<p>" . h($message) . "</p>";

}

?>

Die alternative Schreibweise mit if ... endif ist meistens übersichtlicher, wenn innerhalb der Bedingung ein umfangreicherer HTML-Abschnitt steht.

9.2.5 PHP-Schleifen mit HTML verbinden

Auch eine PHP-Schleife kann 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 Eintrag des Arrays $kunden den zwischen Schleifenanfang und Schleifenende stehenden HTML-Code. Die Schreibweise

<?php foreach ($kunden as $kunde): ?>

leitet die Schleife ein. Bei jedem Schleifendurchlauf enthält $kunde den jeweils aktuellen Eintrag aus dem Array $kunden. Die Anweisung

<?php endforeach; ?>

beendet die Schleife. Dieselbe Ausgabe könnte vollständig mit PHP und echo erzeugt 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 umfangreicheren HTML-Strukturen ist die erste Variante in der Regel leichter zu lesen.

9.2.6 Der Verkettungsoperator

PHP kann eine Zeichenkette aus festen Textteilen und Werten zusammensetzen:

<?php

  echo "<p>Kundenname: " . h($name) . "</p>";

?>

Der Punkt ist in PHP der Operator zur Verkettung von Zeichenketten. Er wird deshalb als Verkettungsoperator bezeichnet.

Beispiel:

"Vorname: " . $vorname

Der feste Text "Vorname: " und der Inhalt der Variablen $vorname werden zu einer Zeichenkette zusammengesetzt. Auch mehrere Bestandteile können miteinander verkettet werden:

echo "<p>" . h($name) . ", " . h($vorname) . "</p>";

Bei komplexeren HTML-Anweisungen kann diese Form schwer lesbar werden:

echo "<input type=\"number\" name=\"Anzahl_$i\" value=\""

. h($anzahl) . "\">";

Eingebettetes PHP ist dann häufig übersichtlicher:

<input type="number" name="Anzahl_<?= (int)$i ?>"

value="<?= h($anzahl) ?>">

Da $i hier eine als ganze Zahl verwendete Schleifenvariable ist, wird sie mit (int) ausdrücklich in eine ganze Zahl umgewandelt.

9.2.7 Ein HTML-Formular übergibt Daten an PHP

Das Zusammenspiel funktioniert auch in der umgekehrten Richtung: HTML stellt ein Formular bereit, der Browser überträgt die eingegebenen Formulardaten und PHP verarbeitet sie. Ein HTML-Formular kann beispielsweise folgendermaßen aussehen:

<form method="post">

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

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

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

</form>

Das Attribut method="post" legt fest, dass die Formulardaten mit der HTTP-Methode POST übertragen werden. Da kein action-Attribut angegeben ist, werden die Daten normalerweise wieder an die Datei gesendet, aus der das Formular stammt. Entscheidend für die Übertragung des Eingabewertes ist:

name="name"

Dadurch wird der eingegebene Wert unter der Bezeichnung "name" übertragen und PHP kann den Wert aus dem Array $_POST übernehmen:

<?php

  $name = $_POST["name"] ?? "";

?>

$_POST ist ein von PHP bereitgestelltes assoziatives Array. Seine Schlüssel entsprechen den name-Attributen der übertragenen Formularfelder. Der Ausdruck

$_POST["name"] ?? ""

bedeutet: Wenn ein Formulareintrag mit dem Schlüssel "name" vorhanden ist, wird dessen Wert verwendet. Andernfalls wird eine leere Zeichenkette eingesetzt. Die übergebenen Daten müssen anschließend serverseitig geprüft werden:

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

if ($name === "") {

  $message = "Bitte geben Sie einen Namen ein.";

}

Der gesamte Ablauf ist damit wie folgt:

  • 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 Formulardaten an den Webserver.
  • PHP übernimmt und prüft die Daten.
  • PHP verarbeitet die Daten, beispielsweise durch eine Datenbankoperation.
  • PHP erzeugt eine neue HTML-Ausgabe.
  • Der Browser stellt die neue 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. Beispiel:

<?php

$message = "";

function h(mixed $value): string

{

  return htmlspecialchars((string)$value, ENT_QUOTES, "UTF-8");

}

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><?= h($message) ?></p>

<?php endif; ?>

 

<form method="post">

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

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

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

</form>

 

</body>

</html>

Der obere PHP-Abschnitt übernimmt hier insbesondere:

  • das Einlesen der Formulardaten
  • die Prüfung der Eingaben
  • die Verarbeitung der Daten
  • gegebenenfalls den Datenbankzugriff
  • die Vorbereitung von Meldungen und anderen Ausgabewerten

Der anschließende HTML-Abschnitt übernimmt:

  • den Aufbau der Webseite,
  • die Darstellung der Inhalte,
  • die Bereitstellung des Formulars.

Innerhalb des HTML-Abschnitts werden nur kurze PHP-Einschübe verwendet, um:

  • Werte auszugeben,
  • HTML-Abschnitte abhängig von Bedingungen anzuzeigen,
  • HTML-Abschnitte in Schleifen zu wiederholen.

9.3 Grundregel

Als Grundregel kann damit gelten:

  • PHP verarbeitet die Daten.
  • HTML strukturiert die Ausgabe.
  • CSS gestaltet die Ausgabe.
  • Prepared Statements schützen die Verarbeitung von Werten in SQL-Anweisungen.
  • htmlspecialchars() beziehungsweise die Hilfsfunktion h() behandelt Werte für die Ausgabe innerhalb von HTML.

Auf diese Weise bleiben Verarbeitung und Darstellung trotz der gemeinsamen PHP-Datei möglichst übersichtlich voneinander getrennt.

10 Glossar

ACID-Eigenschaften
Grundlegende Eigenschaften von Transaktionen: Atomicity (Atomarität), Consistency (Konsistenz), Isolation (Isolation) und Durability (Dauerhaftigkeit).

affected_rows
Eigenschaft eines MySQLi-Statement-Objekts. Sie enthält nach einer INSERT-, UPDATE- oder DELETE-Anweisung die Anzahl der betroffenen Datensätze. Bei einer UPDATE-Anweisung kann der Wert 0 auch bedeuten, dass ein passender Datensatz gefunden wurde, die neuen Werte aber den bereits gespeicherten Werten entsprachen.

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.

beginTransaction() beziehungsweise begin_transaction()
Methoden zum Starten einer Transaktion. beginTransaction() wird bei PDO, begin_transaction() bei MySQLi verwendet.

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
siehe Verbindungsparameter

DSN (Data Source Name)
Zeichenkette mit Angaben zum Aufbau einer PDO-Datenbankverbindung. Sie enthält insbesondere den Datenbanktreiber, den Serverhost und den Datenbanknamen. Zusätzlich können weitere Angaben wie der Zeichensatz enthalten sein. Der DSN wird häufig in einer Variablen mit dem Namen $dsn gespeichert: $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 die vorbereitete SQL-Anweisung ausführt. Bei PDO können die Werte für die Platzhalter unmittelbar als Array an execute() übergeben werden. Beispiel: $stmt->execute(["kuNr" => $kuNr]);

fetch_assoc()
Methode von MySQLi-Ergebnismengen, die jeweils einen Datensatz als assoziatives Array mit Spaltennamen als Schlüssel liefert.

fetchAll()
Methode eines PDOStatement-Objekts. Sie liest alle noch vorhandenen Datensätze einer Ergebnismenge und liefert sie in einem Array zurück. Beispiel: $datensaetze = $stmt->fetchAll(PDO::FETCH_ASSOC);

Fetch-Modus
Festlegung, in welcher Form die Datensätze einer Ergebnismenge bereitgestellt werden. PDO::FETCH_ASSOC bewirkt beispielsweise, dass jeder Datensatz als assoziatives Array mit den Attributnamen als Schlüssel geliefert wird.

Fremdschlüssel (Foreign Key)
Attribut oder Attributkombination einer Relation, deren Ausprägungen auf einen eindeutigen Schlüssel derselben oder einer anderen Relation verweisen. Ein Fremdschlüssel stellt damit eine Beziehung zwischen Datensätzen her. Häufig verweist er auf einen Primärschlüssel.

get_result()
Methode eines MySQLi-Statement-Objekts. Sie liefert nach der Ausführung einer SELECT-Anweisung ein Ergebnisobjekt der Klasse mysqli_result. Die Methode setzt den MySQL Native Driver voraus, der in einer XAMPP-Umgebung normalerweise vorhanden ist.

htmlspecialchars()
PHP-Funktion zur Behandlung von Zeichen, die in HTML eine besondere Bedeutung besitzen. Sie wird verwendet, wenn Werte als Text in einer HTML-Seite ausgegeben werden sollen.

INNER JOIN
Join-Variante, bei der nur solche Kombinationen von Datensätzen ausgegeben werden, für die in allen miteinander verknüpften Relationen passende Datensätze vorhanden sind.

isset()
PHP-Funktion zur Prüfung, ob eine Variable beziehungsweise ein Array-Eintrag vorhanden ist und nicht null enthält.

Join (Verbund)
Operation zur Verknüpfung mehrerer Relationen über gemeinsame Attributwerte, meist über Primär- und Fremdschlüssel.

Klasse 
Zentrales Konzept der objektorientierten Programmierung. Eine Klasse ist ein Bauplan für Objekte. Sie legt fest, welche Eigenschaften und Methoden die zugehörigen Objekte besitzen.

Kurzschlussauswertung
Verfahren bei der Auswertung logischer Ausdrücke. Beim UND-Operator && endet die Auswertung, sobald eine Teilbedingung false ergibt. Das Gesamtergebnis kann dann nicht mehr true werden. Beim ODER-Operator || endet die Auswertung, sobald eine Teilbedingung true ergibt. Das Gesamtergebnis kann dann nicht mehr false werden. Die englische Bezeichnung lautet "short-circuit evaluation". Beim logischen XODER müssen dagegen grundsätzlich beide Operanden ausgewertet werden, weil das Ergebnis davon abhängt, ob genau einer der beiden Operanden wahr ist.

Methode
Funktion, die zu einer Klasse oder einem Objekt gehört und über dieses aufgerufen wird. Beispiele: $pdo->prepare($sql); $stmt->execute();

MySQLi Object
Repräsentiert die Datenbankverbindung. Wird mit new mysqli(...) erzeugt.

MySQLi (MySQL improved)
PHP-Erweiterung für den Zugriff auf MySQL und kompatible Datenbanksysteme. Sie unterstützt Prepared Statements, Transaktionen sowie objektorientierte und prozedurale Programmierung.

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 Operator ?? liefert den Wert seines linken Operanden, wenn dieser vorhanden ist und nicht null enthält. Andernfalls liefert er den Wert des rechten Operanden. Beispiel:
$name = $_POST["name"] ?? "";
Der Ausdruck entspricht weitgehend:
$name = isset($_POST["name"]) ? $_POST["name"] : "";

Objekt
Konkrete Instanz einer Klasse. Ein Objekt besitzt die von seiner Klasse festgelegten Eigenschaften (Attribute) und Funktionen (Methoden).

Parameterbindung
Zuordnung von Werten beziehungsweise Variablen zu den Platzhaltern eines Prepared Statements. SQL-Anweisung und Werte werden dabei getrennt verarbeitet.

PDO (PHP Data Objects)
Objektorientierte PHP-Schnittstelle für den Zugriff auf unterschiedliche Datenbanksysteme. Das konkrete Datenbanksystem wird durch den verwendeten PDO-Treiber festgelegt. Eine einheitliche Schnittstelle bedeutet allerdings nicht, dass jede SQL-Anweisung unverändert mit jedem Datenbanksystem verwendet werden kann.

PDOStatement
Objekt, das eine vorbereitete oder bereits ausgeführte SQL-Anweisung repräsentiert. Es stellt unter anderem Methoden wie execute(), fetch() und fetchAll() bereit.

prepare()
Methode zum Vorbereiten einer SQL-Anweisung mit Platzhaltern. Sie liefert ein Statement-Objekt zurück.

Prepared Statement
Vorbereitete SQL-Anweisung mit Platzhaltern. Die SQL-Anweisung und die zu verarbeitenden Werte werden getrennt übergeben. Wenn alle von außen kommenden Werte über Platzhalter übergeben werden, schützen Prepared Statements insbesondere vor SQL-Injection. Ein vorbereitetes Statement kann außerdem mehrfach mit unterschiedlichen Werten ausgeführt werden.

Primärschlüssel (Primary Key)
Attribut oder Attributkombination, die jeden Datensatz einer Relation (Tabelle) eindeutig identifiziert. Ein Primärschlüsselwert (eine Ausprägung) darf innerhalb der Relation nicht mehrfach vorkommen und nicht null sein.

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. In PHP wird eine Ergebnismenge beispielsweise durch ein mysqli_result-Objekt oder ein ausgeführtes PDOStatement bereitgestellt.

Rollback
Rücknahme einer laufenden Transaktion. Dabei werden die seit Beginn der Transaktion vorgenommenen und noch nicht dauerhaft bestätigten Änderungen verworfen.

rowCount()
Methode eines PDOStatement-Objekts. Nach einer INSERT-, UPDATE- oder DELETE-Anweisung gibt sie die Anzahl der betroffenen Datensätze zurück. Für die Anzahl der Datensätze einer SELECT-Abfrage sollte sie nicht allgemein verwendet werden.

SQL (Structured Query Language)
Standardisierte Sprache zur Definition, Abfrage und Änderung von Daten in relationalen Datenbanksystemen.

SQL Injection
Sicherheitslücke, bei der Eingabewerte so in eine SQL-Anweisung eingefügt werden, dass sie deren beabsichtigte Bedeutung verändern. Dagegen schützen insbesondere korrekt verwendete Prepared Statements, wenn alle von außen kommenden Werte über Platzhalter übergeben werden..

Statement
Im Datenbankkontext eine SQL-Anweisung beziehungsweise ein Objekt, das eine SQL-Anweisung verwaltet. Ein Statement-Objekt kann Methoden zum Ausführen der Anweisung und zum Abrufen der Ergebnisse bereitstellen.

Transaktion (Transaction)
Zusammengehörende Folge von Datenbankoperationen, die als gemeinsame logische Arbeitseinheit behandelt wird. Eine Transaktion wird entweder mit einem Commit vollständig bestätigt oder mit einem Rollback vollständig zurückgenommen.

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. Bei einer tabellarischen Darstellung entspricht ein Tupel einer Zeile.

Verbindungsparameter (Connection Parameters)
Angaben, die zum Aufbau einer Datenbankverbindung benötigt werden. Dazu gehören insbesondere Serverhost, Benutzername, Passwort und Datenbankname. Beispiel für MySQLi:
$con = new mysqli("localhost", "root", "", "KuReAr");

Verbindungsrelation (Junction Table)
Relation zur Realisierung einer n:m-Beziehung zwischen zwei anderen Relationen. Sie enthält die Fremdschlüssel der beteiligten Relationen. Häufig bilden diese Fremdschlüssel gemeinsam den Primärschlüssel. Es kann jedoch auch eine andere Schlüsselgestaltung verwendet werden.

View
Durch eine gespeicherte SQL-Abfrage definierte virtuelle Relation. Eine gewöhnliche View speichert die dargestellten Datensätze nicht selbst, sondern ermittelt sie beim Zugriff aus den zugrunde liegenden Relationen.

11 Anhang

Vertiefte Erläuterungen zu ausgewählten Konzepten, Konstrukten und Begriffen.

11.1 Vom Web zur Datenbank

Zwischen einer Weboberfläche und einer relationalen Datenbank wie KuReAr befindet sich grundsätzlich ein serverseitiges Programm. Der Browser sollte nicht unmittelbar auf das Datenbanksystem zugreifen. Das serverseitige Programm übernimmt insbesondere folgende Aufgaben:

  • Formulardaten entgegennehmen,
  • Eingaben prüfen,
  • SQL-Anweisungen ausführen,
  • Ergebnisse verarbeiten,
  • HTML oder andere Antwortdaten erzeugen.

Für dieses Zusammenspiel gibt es unterschiedliche Architekturen und Techniken.

11.1.1 Direkter serverseitiger Zugriff mit PHP

PHP mit PDO

Das Formular sendet Daten an ein PHP-Programm, das über PDO mit dem Datenbanksystem kommuniziert. Ein vereinfachtes KuReAr-Beispiel zur Suche nach einem Kunden lautet:

$stmt = $pdo->prepare(

  "SELECT * FROM Kunden WHERE KuNr = :kuNr"

);

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

Dabei ist :kuNr ein benannter Platzhalter. Der konkrete Wert wird getrennt von der SQL-Anweisung an execute() übergeben.

Vorteile von PDO:

  • Unterstützung von Prepared Statements,
  • übersichtliche Fehlerbehandlung mit Exceptions,
  • Unterstützung von Transaktionen,
  • einheitliche Programmierschnittstelle für verschiedene Datenbanksysteme,
  • Unterstützung von benannten und positionsbasierten Platzhaltern.

PDO ermöglicht grundsätzlich den Zugriff auf unterschiedliche Datenbanksysteme wie MySQL, MariaDB, PostgreSQL oder SQLite. Das setzt jeweils einen passenden PDO-Treiber voraus. Die SQL-Anweisungen sind allerdings nicht zwangsläufig vollständig übertragbar. Datenbanksysteme können sich beispielsweise bei Funktionen, Datentypen und anderen Einzelheiten des SQL-Dialekts unterscheiden.

Dialekte

Typische Einsatzfälle in KuReAr sind:

  • Kunden anzeigen oder ändern,
  • Rechnungsköpfe speichern,
  • Rechnungspositionen speichern,
  • vollständige Rechnungen innerhalb einer Transaktion verarbeiten.

11.1.2 PHP mit MySQLi

MySQLi wurde speziell für den Zugriff auf MySQL und kompatible Datenbanksysteme entwickelt. Auch MySQLi unterstützt Prepared Statements und Transaktionen. Ein vereinfachtes Beispiel:

$stmt = $con->prepare(

  "SELECT * FROM Kunden WHERE KuNr = ?"

);

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

$stmt->execute();

Das Fragezeichen ist ein positionsbasierter Platzhalter. Mit bind_param() wird die Variable $kuNr daran gebunden.

Vorteile von MySQLi:

  • speziell für MySQL und kompatible Datenbanksysteme entwickelt,
  • Unterstützung von Prepared Statements,
  • Unterstützung von Transaktionen,
  • objektorientierte und prozedurale Verwendung möglich,
  • unmittelbarer Zugriff auf MySQL-spezifische Funktionen.

MySQLi eignet sich besonders für Programme, die ausschließlich mit MySQL oder MariaDB arbeiten sollen. Im Unterschied zu PDO ist ein späterer Wechsel zu einem grundlegend anderen Datenbanksystem nicht vorgesehen.

Unsichere direkte Zusammensetzung von SQL-Anweisungen

Nicht empfohlen ist es, Formulardaten unmittelbar in eine SQL-Zeichenkette einzusetzen. Beispiel:

$sql = "SELECT * FROM Kunden WHERE KuNr = $id";

Wenn $id unmittelbar aus einer Benutzereingabe stammt, kann dadurch eine SQL-Injection möglich werden. Auch eine einfache Maskierung der Eingabe ist gegenüber einem Prepared Statement fehleranfälliger. Von außen kommende Werte sollten deshalb über Platzhalter an die SQL-Anweisung übergeben werden.

11.1.3 Zugriff über eine API

Bei einer API-Architektur ruft das Frontend festgelegte Schnittstellen des Servers auf:

Frontend -> API -> Datenbank

Auch hier greift nicht der Browser selbst auf die Datenbank zu. Ein serverseitiger API-Endpunkt übernimmt den Datenbankzugriff. Mögliche Aufrufe sind beispielsweise:

GET /api/kunden?kunr=1003

POST /api/rechnungen

Ein GET-Aufruf fordert Daten an. Ein POST-Aufruf übermittelt Daten zur Verarbeitung.

Die API kann beispielsweise in PHP entwickelt werden. Die PHP-Programme greifen anschließend mit PDO oder MySQLi auf KuReAr zu. Die API liefert häufig keine vollständige HTML-Seite, sondern Daten im JSON-Format. JavaScript verarbeitet diese Daten im Browser und fügt sie in die Webseite ein.

Vorteile einer API:

  • Frontend und Datenbankverarbeitung werden deutlicher getrennt,
  • mehrere Frontends können dieselbe API verwenden,
  • dieselbe API kann beispielsweise von einer Webseite und einer mobilen Anwendung genutzt werden,
  • größere Anwendungen lassen sich übersichtlicher gliedern.

Mögliche Einsatzfälle sind:

  • Kundenlisten als JSON laden,
  • Artikel dynamisch suchen,
  • Rechnungsdaten an einen API-Endpunkt übermitteln,
  • Listen aktualisieren, ohne die gesamte Webseite neu zu laden.

Eine vollständige Rechnung sollte möglichst in einer einzigen Anfrage mit Rechnungskopf und allen Positionen an den Server übertragen werden. Das serverseitige Programm kann dann alle Bestandteile innerhalb einer einzigen Datenbanktransaktion speichern.

Eine einzelne Transaktion sollte nicht auf mehrere unabhängige HTTP-Aufrufe verteilt werden. Zwischen diesen Aufrufen könnte ein Fehler auftreten, sodass nur ein Teil der Rechnung gespeichert würde.

API mit anderen Programmiersprachen

Eine API muss nicht in PHP entwickelt werden. Als serverseitige Programmiersprachen kommen beispielsweise auch JavaScript mit Node.js, Python oder Java infrage. Der Backend-Dienst verwendet dann einen passenden Datenbanktreiber oder ein ORM für den Zugriff auf MySQL beziehungsweise MariaDB.

Vorteile:

  • geeignet für umfangreiche Anwendungen,
  • gute Möglichkeiten zur Strukturierung größerer Projekte,
  • zahlreiche Bibliotheken und Entwicklungswerkzeuge.

Nachteil:

  • mehr Installations- und Konfigurationsaufwand als bei einem einfachen PHP-Programm,
  • für Einsteiger zunächst schwieriger zu überblicken.

11.1.4 Verwendung eines ORM

ORM bedeutet "Object-Relational Mapping". Ein ORM ordnet Objekte eines Programms den Relationen einer relationalen Datenbank zu. Beispiele sind:

  • Doctrine für PHP
  • Sequelize für JavaScript beziehungsweise Node.js
  • Hibernate für Java

Bei einem ORM wird hauptsächlich mit Klassen und Objekten gearbeitet. Das ORM erzeugt daraus die erforderlichen SQL-Anweisungen.

Vorteile:

  • weniger unmittelbar geschriebener SQL-Code
  • Beziehungen zwischen Objekten und Relationen lassen sich strukturiert abbilden
  • umfangreiche Anwendungen können einheitlicher aufgebaut werden

Nachteile:

  • zusätzliche Einarbeitung erforderlich,
  • viele Vorgänge laufen automatisch im Hintergrund ab,
  • die erzeugten SQL-Anweisungen sind für Einsteiger nicht immer unmittelbar erkennbar,
  • für kleine Lernprogramme ist ein ORM häufig zu aufwendig.

Da im vorliegenden Text das Verständnis von SQL, Prepared Statements und Transaktionen im Vordergrund steht, werden die Datenbankzugriffe unmittelbar mit PDO oder MySQLi programmiert.

11.1.5 Abläufe einer Webanwendung

Neben der eingesetzten Programmierschnittstelle lassen sich unterschiedliche Abläufe unterscheiden.

Klassische mehrseitige Webanwendung

Ein typischer Ablauf ist:

Formular anzeigen -> Daten mit POST senden -> PHP verarbeitet die Daten

-> Weiterleitung -> Ergebnis anzeigen

Nach einer erfolgreichen Verarbeitung kann PHP den Browser auf eine andere Seite weiterleiten. Dieses Verfahren wird als "Post-Redirect-Get", kurz PRG, bezeichnet. Beispiel:

header(

  "Location: AnzeigeRechnung.php?ReNr="

    . urlencode((string)$NeueReNr)

);

exit;

Die Weiterleitung verhindert unter anderem, dass dieselben Formulardaten beim Aktualisieren der Ergebnisseite versehentlich erneut übertragen werden. Dieses Vorgehen wird auch bei der Rechnungsverwaltung in KuReAr verwendet:

EingabeRechnungen.php

-> Rechnung speichern

-> AnzeigeRechnung.php

Dynamische Teilbereiche mit Fetch

Mit JavaScript und fetch() können einzelne Daten nachgeladen werden, ohne die gesamte Webseite neu anzufordern. Mögliche Beispiele sind:

  • Artikelsuche während der Eingabe,
  • dynamische Kundenfilter,
  • Aktualisierung einer Liste,
  • Prüfung einzelner Eingaben.

Der Datenbankzugriff erfolgt trotzdem serverseitig. JavaScript ruft lediglich einen API-Endpunkt auf, der die Datenbankoperation ausführt.

11.1.6 Grundlegende Anforderungen

Unabhängig von der gewählten Architektur werden bei Datenbankanwendungen in der Regel folgende Maßnahmen benötigt:

  • Prepared Statements für von außen kommende Werte
  • serverseitige Prüfung der Eingaben
  • sichere Behandlung der HTML-Ausgabe
  • Fehlerbehandlung
  • Transaktionen bei zusammengehörenden Datenbankänderungen
  • Beachtung der Primär- und Fremdschlüssel
  • geeignete Modellierung relationaler Beziehungen

Ein Beispiel für eine zusammengesetzte Datenbankoperation ist das Speichern einer Rechnung:

ein Rechnungskopf in RechKoepfe

+

mehrere Rechnungspositionen in RechPos

Alle Bestandteile werden entweder vollständig oder gar nicht gespeichert. Deshalb wird dafür eine Transaktion eingesetzt.

Ein Beispiel für eine n:m-Beziehung findet sich ebenfalls in KuReAr: Eine Rechnung kann mehrere Artikel enthalten und ein Artikel kann auf mehreren Rechnungen vorkommen. Diese Beziehung wird über die Relation RechPos realisiert.

11.2 HTML Special Characters

Die PHP-Funktion htmlspecialchars() behandelt Zeichen, die in HTML eine besondere Bedeutung besitzen. Sie wandelt diese Zeichen in HTML-Zeichenreferenzen um. Dadurch zeigt der Browser die Zeichen als gewöhnlichen Text an, anstatt sie als Bestandteile des HTML-Codes zu interpretieren. Eine typische Anwendung ist die Ausgabe von Werten, die aus einem Formular oder einer Datenbank stammen:

echo htmlspecialchars(

  (string)$_POST["name"],ENT_QUOTES,"UTF-8"

);

htmlspecialchars() ist damit eine Funktion zur Behandlung der HTML-Ausgabe. Sie ist nicht dafür zuständig, Eingaben vor dem Speichern in einer Datenbank zu behandeln. Für die sichere Übergabe von Werten an SQL-Anweisungen werden Prepared Statements verwendet.

Umgewandelte Zeichen

Bei der Verwendung von ENT_QUOTES werden insbesondere folgende Zeichen umgewandelt:

Beispielsweise wird aus:

<script>

bei der HTML-Ausgabe sinngemäß:

&lt;script&gt;

Der Browser zeigt dann den Text <script> an, anstatt ihn als HTML-Element zu interpretieren.

Bedeutung der Parameter

Der vollständige Aufruf lautet beispielsweise:

htmlspecialchars($text, ENT_QUOTES, "UTF-8");

Die Bestandteile bedeuten:

  • $text enthält den auszugebenden Wert.
  • ENT_QUOTES bewirkt, dass sowohl doppelte als auch einfache Anführungszeichen behandelt werden.
  • "UTF-8" legt fest, dass die Zeichenkette als UTF-8 verarbeitet wird.

Die Behandlung der Anführungszeichen ist insbesondere wichtig, wenn ein Wert innerhalb eines HTML-Attributs ausgegeben wird:

<input type="text" value="<?= h($name) ?>">

Ohne eine geeignete Behandlung könnte ein in der Ausprägung enthaltenes Anführungszeichen das Lesen des Attributs vorzeitig beenden.

Hilfsfunktion h()

Da htmlspecialchars() in den Programmen dieses Textes häufig benötigt wird, wurde eine Hilfsfunktion eingeführt:

function h(mixed $value): string

{

  return htmlspecialchars((string)$value,ENT_QUOTES,"UTF-8");

}

Dadurch kann die Ausgabe kürzer geschrieben werden:

<p>Kundenname: <?= h($name) ?></p>

Die Funktion h() verändert nicht den ursprünglichen Wert und nicht den Inhalt der Datenbank. Sie behandelt den Wert ausschließlich für die HTML-Ausgabe. Sie schützt auch nicht allgemein vor allen möglichen Angriffen. Für SQL-Anweisungen werden Prepared Statements benötigt. Für JavaScript, CSS und URL-Bestandteile gelten jeweils eigene Regeln für die sichere Ausgabe.

11.3 Prepared Statements

Ein Prepared Statement ist eine vorbereitete SQL-Anweisung mit Platzhaltern. Die SQL-Anweisung und die zu verarbeitenden Werte werden getrennt behandelt.

Der grundsätzliche Ablauf ist wie folgt:

  • Die SQL-Anweisung wird mit Platzhaltern vorbereitet.
  • Die Werte werden den Platzhaltern zugeordnet.
  • Die vorbereitete Anweisung wird ausgeführt.
  • Bei einer SELECT-Anweisung wird das Ergebnis abgerufen.

Wenn alle von außen kommenden Werte über Platzhalter übergeben werden, können diese Werte die Struktur der SQL-Anweisung nicht verändern. Dadurch schützen Prepared Statements insbesondere vor SQL-Injection.

Unsichere Zusammensetzung einer SQL-Anweisung

In der Relation Kunden der Datenbank KuReAr soll ein Kunde über seine Kundennummer gesucht werden. Eine unsichere Variante ist:

$id = $_GET["id"];

$sql = "SELECT * FROM Kunden WHERE KuNr = $id";

Hier wird der Wert der Variablen $id unmittelbar in die SQL-Anweisung eingefügt. Ein Angreifer könnte statt der vorgesehenen Kundennummer beispielsweise Folgendes übertragen:

1003 OR 1=1

Daraus entstünde die SQL-Anweisung:

SELECT * FROM Kunden WHERE KuNr = 1003 OR 1=1

Die Bedingung 1=1 ist immer wahr. Deshalb könnte die Abfrage alle Kunden zurückliefern. Das Einschleusen von SQL-Bestandteilen über Eingabewerte wird als SQL-Injection bezeichnet.

Variante 1: PDO

Schritt 1: SQL-Anweisung vorbereiten

$stmt = $pdo->prepare(

  "SELECT * FROM Kunden WHERE KuNr = :kuNr"

);

:kuNr ist ein benannter Platzhalter.

Schritt 2: Wert übergeben und Anweisung ausführen

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

Das Array ordnet dem Platzhalter :kuNr den Wert der Variablen $kuNr zu. Bei den Array-Schlüsseln kann der Doppelpunkt weggelassen werden. execute() übergibt den Wert und führt zugleich die vorbereitete SQL-Anweisung aus.

Schritt 3: Ergebnis abrufen

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

fetch() liefert den nächsten gefundenen Datensatz. Durch PDO::FETCH_ASSOC wird er als assoziatives Array bereitgestellt. Auf die einzelnen Werte kann beispielsweise so zugegriffen werden:

echo $row["Name"];

SQL-Struktur und Wert werden dabei getrennt behandelt:

  • SQL-Struktur: SELECT * FROM Kunden WHERE KuNr = :kuNr
  • Wert: 1003

Der Wert wird als Datenwert behandelt und nicht als Bestandteil der SQL-Struktur.

Variante 2: MySQLi

Bei MySQLi werden die Werte zunächst mit bind_param() an die Platzhalter gebunden. Danach wird das Statement ausgeführt.

Schritt 1: SQL-Anweisung vorbereiten

$stmt = $con->prepare(

  "SELECT * FROM Kunden WHERE KuNr = ?"

);

Das Fragezeichen ist ein positionsbasierter Platzhalter.

Schritt 2: Variable an den Platzhalter binden

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

Das Typkennzeichen "i" legt fest, dass der Wert als Integer behandelt wird. Weitere Typkennzeichen sind:

  • "s" für String,
  • "d" für Double,
  • "b" für BLOB beziehungsweise Binärdaten.

bind_param() bindet die Variable an den Platzhalter. Wird der Inhalt der Variablen vor der Ausführung geändert, wird beim Ausführen ihr dann aktueller Wert verwendet.

Schritt 3: Statement ausführen

$stmt->execute();

Schritt 4: Ergebnis abrufen

$result = $stmt->get_result();

$row = $result->fetch_assoc();

get_result() liefert ein Ergebnisobjekt. fetch_assoc() liest daraus den nächsten Datensatz als assoziatives Array.

Beispiel: Rechnungskopf mit PDO speichern

Ein Prepared Statement zum Speichern eines Rechnungskopfes kann folgendermaßen aufgebaut werden:

$stmt = $pdo->prepare(

  "INSERT INTO RechKoepfe (ReNr, KuNr, ReDatum)

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

);

 

$stmt->execute([

  "reNr" => $reNr,

  "kuNr" => $kuNr,

  "reDatum" => $reDatum

]);

Die benannten Platzhalter werden den entsprechenden PHP-Werten zugeordnet. Eine vollständige Rechnung besteht zusätzlich aus mehreren Rechnungspositionen. Rechnungskopf und Rechnungspositionen müssen deshalb innerhalb einer gemeinsamen Transaktion gespeichert werden.

Grenzen der Platzhalter

Platzhalter können für Werte verwendet werden:

WHERE KuNr = :kuNr

Sie können normalerweise nicht für Bezeichnungen von Relationen, Attributen oder SQL-Schlüsselwörtern verwendet werden. Folgendes ist deshalb nicht als gewöhnlicher Platzhalter möglich:

SELECT * FROM :relation

Muss beispielsweise die Sortierreihenfolge oder eine Attributbezeichnung dynamisch festgelegt werden, dürfen nur ausdrücklich zugelassene Werte verwendet werden.

Grundregel

Von außen kommende Werte dürfen nicht durch einfache Zeichenkettenverkettung in eine SQL-Anweisung eingefügt werden. Sie sollen über Platzhalter eines Prepared Statements übergeben werden.

Kurz zusammengefasst:

  • Prepared Statements behandeln Werte für SQL-Anweisungen.
  • htmlspecialchars() beziehungsweise h() behandelt Werte für die Ausgabe in HTML.

Beide Maßnahmen erfüllen unterschiedliche Aufgaben und werden häufig gemeinsam benötigt.

11.4 Exceptions und Fehlerbehandlung mit PDO

Eine Exception ist ein Objekt, das einen Fehler oder eine außergewöhnliche Situation während der Programmausführung beschreibt. Ein Exception-Objekt enthält unter anderem:

  • eine Fehlermeldung
  • einen Fehlercode
  • die Stelle, an der der Fehler aufgetreten ist
  • Informationen über den bisherigen Programmablauf

Eine Exception ist also nicht lediglich eine Fehlermeldung. Sie ist ein Objekt, das verschiedene Informationen über einen Fehler bereitstellt.

PHP kann eine Exception "werfen". Dadurch wird der normale Programmablauf unterbrochen. Befindet sich die fehlerhafte Anweisung in einem passenden try/catch-Block, wird die Verarbeitung im zugehörigen catch-Block fortgesetzt.

Fehlerbehandlung bei PDO

Beim Aufbau eines PDO-Objekts kann festgelegt werden, wie PDO auf Datenbankfehler reagieren soll:

$pdo = new PDO(

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

  "root",

  "",

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

);

Der vierte Parameter von new PDO() ist ein Array mit zusätzlichen Einstellungen für die Datenbankverbindung. Die Anweisung

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION

legt den Fehlerbehandlungsmodus fest.

PDO::ATTR_ERRMODE ist eine Klassenkonstante der Klasse PDO. Sie bezeichnet die Einstellung für den Fehlerbehandlungsmodus. Der Ausdruck PDO::ATTR_ERRMODE besteht aus:

  • PDO: Name der Klasse,
  • :: ist der Bereichsoperator für den Zugriff auf ein Element der Klasse,
  • ATTR_ERRMODE: Name der Klassenkonstanten.

Die Großschreibung ist eine übliche Schreibweise für Konstanten. Eine Bezeichnung wird aber nicht allein durch die Großbuchstaben zu einer Konstanten.

PDO::ERRMODE_EXCEPTION ist ebenfalls eine Klassenkonstante. Sie legt fest, dass PDO bei einem Datenbankfehler eine PDOException auslöst.

In PHP 8.2 ist PDO::ERRMODE_EXCEPTION bereits das Standardverhalten. Die ausdrückliche Angabe ist trotzdem sinnvoll, weil dadurch unmittelbar im Programm zu erkennen ist, wie Datenbankfehler behandelt werden sollen. Seit PHP 8.0 ist dieser Modus der Standard. In älteren PHP-Versionen war dagegen PDO::ERRMODE_SILENT voreingestellt.

Beispiel für einen Datenbankfehler

Die folgende Abfrage greift auf eine nicht vorhandene Relation zu:

$pdo->query(

  "SELECT * FROM nicht_existierende_relation"

);

PDO löst dadurch eine PDOException aus. Mit einem try/catch-Block kann das Programm kontrolliert auf den Fehler reagieren:

try {

  $pdo->query(

    "SELECT * FROM nicht_existierende_relation"

  );

} catch (PDOException $e) {

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

}

Der Ablauf ist wie folgt:

1. PHP führt die Anweisungen im try-Block aus.

2. Bei einer fehlerhaften SQL-Anweisung löst PDO eine PDOException aus.

3. Der normale Ablauf des try-Blocks wird abgebrochen.

4. PHP sucht einen passenden catch-Block.

5. Das Exception-Objekt wird der Variablen $e zugewiesen.

6. $e->getMessage() liefert den Meldungstext der Exception.

Ohne einen passenden catch-Block würde die Exception nicht vom Programm behandelt. PHP würde die Ausführung des Programms abbrechen und eine Fehlermeldung ausgeben.

Fehler beim Verbindungsaufbau

Auch beim Erzeugen des PDO-Objekts kann ein Fehler auftreten, beispielsweise wenn:

  • der Datenbankserver nicht erreichbar ist,
  • der Datenbankname falsch geschrieben wurde,
  • der Benutzername falsch ist,
  • das Passwort nicht stimmt.

Deshalb sollte bereits der Verbindungsaufbau in den try-Block aufgenommen werden:

try {

  $pdo = new PDO(

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

    "root","",

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

     

  $pdo->query(

    "SELECT * FROM Kunden");

} catch (PDOException $e) {

echo "Datenbankfehler: " . $e->getMessage();}

Ein Fehler beim Verbindungsaufbau führt unabhängig vom eingestellten Fehlermodus zu einer PDOException.

In einer Entwicklungs- und Lernumgebung ist die Ausgabe von $e->getMessage() hilfreich. Auf einer öffentlich erreichbaren Webseite sollten ausführliche interne Datenbankmeldungen normalerweise nicht unmittelbar an den Benutzer ausgegeben werden.

11.5 Die Variable $con

In den MySQLi-Programmen wird die Variable $con für das Verbindungsobjekt verwendet:

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

$con ist eine gewöhnliche PHP-Variable. Nach dem erfolgreichen Aufruf von new mysqli(...) verweist sie auf ein Objekt der Klasse mysqli. Dieses Objekt repräsentiert die Verbindung zum Datenbankserver. Es stellt Eigenschaften und Methoden bereit, über die das PHP-Programm Datenbankoperationen ausführen kann.

Die Klasse mysqli repräsentiert nach der PHP-Dokumentation eine Verbindung zwischen PHP und einem MySQL-Datenbanksystem. Beispiel:

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

Die Bestandteile bedeuten:

  • $con ist die Variable, über die anschließend auf das Verbindungsobjekt zugegriffen wird.
  • new erzeugt ein neues Objekt.
  • mysqli ist die Klasse, von der das Objekt erzeugt wird.
  • "localhost" bezeichnet den Datenbankserver.
  • "root" ist der Benutzername.
  • "" ist das hier leere Passwort.
  • "KuReAr" ist die Bezeichnung der Datenbank.

Der Konstruktor new mysqli(...) erzeugt das Objekt und versucht zugleich, eine Verbindung zum angegebenen Datenbankserver herzustellen.

Zugriff auf Eigenschaften

Über den Pfeiloperator -> kann auf Eigenschaften des Objekts zugegriffen werden:

$con->connect_error

connect_error enthält bei einer entsprechenden manuellen Fehlerbehandlung den Meldungstext eines Verbindungsfehlers. Ein älteres beziehungsweise ausdrücklich auf manuelle Fehlerbehandlung eingestelltes Programm kann beispielsweise so prüfen:

mysqli_report(MYSQLI_REPORT_OFF);

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

 

if ($con->connect_error) {

  die("Datenbankverbindung fehlgeschlagen: "

    . $con->connect_error);

}

Häufig verwendete Methoden

Über $con können Methoden des Verbindungsobjekts aufgerufen werden:

  • $con->set_charset("utf8mb4");
  • $con->prepare($sql);
  • $con->query($sql);
  • $con->begin_transaction();
  • $con->commit();
  • $con->rollback();
  • $con->close();

Die Methoden haben folgende Aufgaben:

 


Methode

Aufgabe

set_charset()

Zeichensatz der Datenbankverbindung festlegen

prepare()

Prepared Statement vorbereiten

query()

SQL-Anweisung unmittelbar ausführen

begin_transaction()

Transaktion starten

commit()

Transaktion erfolgreich abschließen

rollback()

Änderungen einer Transaktion zurücknehmen

close()

Datenbankverbindung schließen

Zusammengefasst gilt:

new mysqli(...) erzeugt ein Verbindungsobjekt. Die Variable $con verweist auf dieses Objekt und ermöglicht den Zugriff auf seine Eigenschaften und Methoden.

11.6 Array von Arrays

Mehrdimensionales Array, Ein Array von assoziativen Arrays.

Eine Rechnung kann mehrere Rechnungspositionen enthalten. Im PHP-Programm können diese Positionen in einem Array gespeichert werden:

$positionen = [

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

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

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

];

Ein solches Array wird als mehrdimensionales Array bezeichnet. Genauer handelt es sich um ein numerisch indiziertes Array, dessen Elemente wiederum assoziative Arrays sind.

Das äußere Array enthält drei Elemente. Da keine Schlüssel ausdrücklich angegeben wurden, vergibt PHP automatisch die numerischen Schlüssel:

Äußeres Array

0

1

2

Vereinfacht betrachtet hat das äußere Array somit folgende Struktur:

$positionen = [

  0 => [...],

  1 => [...],

  2 => [...]

];

Jedes Element steht für eine Rechnungsposition.

Die inneren Arrays enthalten die konkreten Daten, z.B.:

Innere Arrays

[

  "posNr" => 1,

  "artNr" => 100,

  "anzahl" => 2

]

Die Schlüssel sind Zeichenketten:

"posNr"

"artNr"

"anzahl"

Die zugehörigen Werte sind in diesem Beispiel ganze Zahlen:

1

100

2

Eine Rechnungsposition enthält damit:

  • die Positionsnummer,
  • die Artikelnummer,
  • die Anzahl.

Beschreibung des Aufbaus

Der Aufbau ist damit wie folgt: array<int, array<string, int>>. Das bedeutet:

  • Das äußere Array besitzt ganzzahlige Schlüssel.
  • Seine Werte sind wiederum Arrays.
  • Die inneren Arrays besitzen Zeichenketten als Schlüssel.
  • Die Werte der inneren Arrays sind ganze Zahlen.

Zugriff auf ein einzelnes Element

Auf den ersten Eintrag des äußeren Arrays kann über den Schlüssel 0 zugegriffen werden: echo $positionen[0]["artNr"];. Die Ausgabe lautet:

100

Dabei bedeutet:

  • $positionen[0] liefert die erste Rechnungsposition.
  • ["artNr"] liefert daraus die Artikelnummer.

Verarbeitung mit foreach

Üblicherweise werden die Rechnungspositionen mit einer foreach-Schleife verarbeitet:

foreach ($positionen as $p) {

  echo $p["artNr"];

}

Bei jedem Schleifendurchlauf verweist $p auf das assoziative Array der jeweils aktuellen Rechnungsposition. Innerhalb der Schleife können alle Werte gelesen werden:

foreach ($positionen as $p) {

  echo "Position: " . $p["posNr"] . "<br>";

  echo "Artikel: " . $p["artNr"] . "<br>";

  echo "Anzahl: " . $p["anzahl"] . "<br>";

}

PHP unterscheidet bei Array-Schlüsseln zwischen Groß- und Kleinschreibung. Deshalb sind beispielsweise $p["artNr"] und $p["ArtNr"] zwei verschiedene Zugriffe. Die Schreibweise muss innerhalb eines Programms einheitlich verwendet werden.

11.7 Die Klasse mysqli

Die Klasse mysqli repräsentiert eine Verbindung zwischen einem PHP-Programm und einem MySQL-kompatiblen Datenbanksystem. Sie stellt zahlreiche Eigenschaften und Methoden zur Verfügung. Eigenschaften liefern Informationen über den Zustand der Verbindung oder über die zuletzt ausgeführte Datenbankoperation. Methoden führen dagegen Operationen aus.

Da der Begriff "Attribut" im vorliegenden Text bereits für Bestandteile einer Relation verwendet wird, wird für Bestandteile einer PHP-Klasse die Bezeichnung "Eigenschaft" verwendet.

Eigenschaften beim Verbindungsaufbau

Bei einer manuellen Fehlerbehandlung können folgende Eigenschaften ausgewertet werden:

$con->connect_errno

$con->connect_error

connect_errno enthält die Fehlernummer eines Verbindungsfehlers:

if ($con->connect_errno !== 0) {

  echo $con->connect_errno;

}

Dabei gilt:

  • 0: Es wurde kein Verbindungsfehler festgestellt.
  • ungleich 0: Beim Verbindungsaufbau ist ein Fehler aufgetreten.

connect_error enthält den zugehörigen Meldungstext: echo $con->connect_error;

Bei der in PHP 8.2 üblichen Fehlerbehandlung mit Exceptions werden Verbindungsfehler normalerweise als mysqli_sql_exception gemeldet. connect_errno und connect_error sind vor allem dann unmittelbar von Bedeutung, wenn die automatische Exception-Behandlung mit MYSQLI_REPORT_OFF abgeschaltet wurde.

Eigenschaften nach SQL-Anweisungen

Nach einer unmittelbar über das Verbindungsobjekt ausgeführten SQL-Anweisung können folgende Eigenschaften ausgewertet werden:

$con->errno

$con->error

errno enthält die Fehlernummer der zuletzt über die Verbindung ausgeführten Datenbankoperation: echo $con->errno; error enthält den zugehörigen Meldungstext: echo $con->error; Diese Eigenschaften sind insbesondere beim Testen und bei einer manuellen Fehlerbehandlung hilfreich.

Wurde dagegen ein Prepared Statement verwendet, befinden sich entsprechende Fehlerinformationen auch im Statement-Objekt:

$stmt->errno

$stmt->error

In PHP 8.2 werden solche Fehler bei der üblichen Einstellung allerdings normalerweise als mysqli_sql_exception gemeldet.

Die Eigenschaft affected_rows gibt an, wie viele Datensätze von der zuletzt ausgeführten INSERT-, UPDATE- oder DELETE-Anweisung betroffen waren: echo $con->affected_rows;

affected_rows

Bei einem Prepared Statement wird üblicherweise die Eigenschaft des Statement-Objekts verwendet: echo $stmt->affected_rows; Sie kann beispielsweise nach einer DELETE-Anweisung geprüft werden:

$stmt->execute();

 

if ($stmt->affected_rows > 0) {

  echo "Datensatz wurde gelöscht.";

} else {

  echo "Kein Datensatz wurde gelöscht.";

}

Bei einer UPDATE-Anweisung bedeutet der Wert 0 nicht zwingend, dass kein passender Datensatz vorhanden war. Der Wert kann auch bedeuten, dass die neu übergebenen Werte bereits in der Datenbank gespeichert waren und deshalb keine tatsächliche Änderung erforderlich war.

affected_rows steht sowohl beim MySQLi-Verbindungsobjekt als auch beim MySQLi-Statement-Objekt zur Verfügung.

Die Eigenschaft insert_id liefert den automatisch erzeugten Wert einer AUTO_INCREMENT-Spalte nach einer INSERT-Anweisung: echo $con->insert_id;

insert_id

Bei einem Prepared Statement kann auch geschrieben werden: echo $stmt->insert_id; Ein typisches Beispiel ist:

$con->query(

  "INSERT INTO Beispiel (Bezeichnung) VALUES ('Test')");

$neueNummer = $con->insert_id;

insert_id ist nur dann von Bedeutung, wenn die eingefügte Relation eine automatisch erzeugte AUTO_INCREMENT-Nummer verwendet.

In den hier verwendeten KuReAr-Programmen wird die Rechnungsnummer nicht mit AUTO_INCREMENT erzeugt. Dort wird sie auf andere Weise festgelegt. Für diese Rechnungsnummer ist insert_id deshalb nicht unmittelbar anwendbar.

Weitere Eigenschaften

Die Klasse mysqli besitzt weitere Eigenschaften:

 


Eigenschaft

Bedeutung

server_info

Versionsangabe des Datenbankservers

host_info

Informationen über die Verbindung zum Server

protocol_version

verwendete Protokollversion

thread_id

Kennung der aktuellen Serververbindung

Diese Eigenschaften werden hauptsächlich für Verwaltungs- und Diagnosezwecke verwendet. In einfachen Anwendungsprogrammen spielen sie nur eine untergeordnete Rolle.

Zusammenfassung

Für die behandelten MySQLi-Programme sind vor allem folgende Eigenschaften wichtig:

 


Eigenschaft

Objekt

Bedeutung

connect_errno

$con

Fehlernummer beim Verbindungsaufbau

connect_error

$con

Fehlermeldung beim Verbindungsaufbau

errno

$con oder $stmt

Fehlernummer der letzten Operation

error

$con oder $stmt

Fehlermeldung der letzten Operation

affected_rows

$con oder $stmt

Anzahl der betroffenen Datensätze

insert_id

$con oder $stmt

erzeugter AUTO_INCREMENT-Wert

11.8 Referenzieren und Verweisen auf Objekte

In Texten zur objektorientierten Programmierung taucht häufig das Wort "referenzieren" auf. Beispielsweise so:

"new PDO() erzeugt ein Objekt der Klasse PDO. Die Variable $pdo referenziert dieses Verbindungsobjekt."

Diese Formulierung kann den Eindruck erwecken, dass die Variable $pdo das vollständige Objekt enthält. Das ist bei PHP-Objekten jedoch nicht der Fall.

Betrachten wir die Anweisung:

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

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

Dabei geschieht Folgendes:

  • new PDO(...) erzeugt ein Objekt der Klasse PDO.
  • Dieses Objekt repräsentiert die Datenbankverbindung.
  • $pdo enthält einen internen Objektbezeichner.
  • Über diesen Objektbezeichner kann auf das PDO-Objekt zugegriffen werden.

Die PHP-Dokumentation beschreibt dies so: Eine Objektvariable enthält nicht das Objekt selbst, sondern einen Objektbezeichner, über den das Objekt gefunden werden kann. Über die Variable können anschließend Eigenschaften und Methoden des Objekts angesprochen werden:

$pdo->prepare($sql);

$pdo->beginTransaction();

$pdo->commit();

Der Pfeiloperator -> ermöglicht dabei den Zugriff auf Eigenschaften und Methoden des Objekts.

Zuweisung an eine zweite Variable

Wird eine Objektvariable einer zweiten Variablen zugewiesen, wird das Objekt nicht vollständig kopiert: $pdo2 = $pdo; Vereinfacht ausgedrückt enthalten anschließend beide Variablen einen Objektbezeichner, der auf dasselbe Objekt verweist. Änderungen am Objekt sind deshalb über beide Variablen sichtbar. Dies ist nicht dasselbe wie eine PHP-Referenz, die ausdrücklich mit dem Operator & erzeugt wird: $pdo2 = &$pdo; Der Begriff "Referenz" besitzt in PHP also zusätzlich eine besondere technische Bedeutung. Deshalb wird in diesem Text bei Objekten vorzugsweise die Formulierung "verweist auf" verwendet.

11.9 isset()

Da in den Programmen viele Formulare verarbeitet werden, wird häufig isset() verwendet. isset() ist genau genommen ein PHP-Sprachkonstrukt mit einer funktionsähnlichen Schreibweise. Es prüft, ob eine Variable beziehungsweise ein Array-Element vorhanden ist und nicht den Wert null enthält. Die allgemeine Schreibweise lautet: isset($variable). Das Ergebnis ist ein boolescher Wert:

  • true: Die Variable ist vorhanden und enthält nicht null.
  • false: Die Variable ist nicht vorhanden oder enthält null.

Dieses Verhalten entspricht der Definition im PHP-Handbuch.

Einfache Prüfung

$name = "Meier";

if (isset($name)) {

  echo "Die Variable ist vorhanden.";

}

Die Variable $name ist vorhanden und enthält den Wert "Meier". Deshalb liefert isset($name) den Wert true.

Variable mit dem Wert null

$name = null;

if (isset($name)) {

  echo "Die Variable ist vorhanden.";

}

Die Variable wurde zwar angelegt, enthält aber null. Deshalb liefert isset($name) den Wert false.

Nicht vorhandene Variable

if (isset($ort)) {

  echo "Die Variable ist vorhanden.";

}

Wenn $ort zuvor nicht angelegt wurde, liefert isset($ort) ebenfalls false. Der Vorteil besteht darin, dass bei dieser Prüfung keine Warnung wegen einer nicht vorhandenen Variablen entsteht.

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:

  • PHP prüft, ob das Array $_POST ein Element mit dem Schlüssel "posCount" enthält.
  • Zusätzlich wird geprüft, ob der zugehörige Wert nicht null ist.
  • Nur wenn beide Voraussetzungen erfüllt sind, wird der Wert übernommen.
  • (int) wandelt den übernommenen Wert in eine ganze Zahl um.

Ohne die vorherige Prüfung könnte der direkte Zugriff $_POST["posCount"] eine Warnung erzeugen, wenn das Formularfeld nicht übermittelt wurde.

Unterschied zu array_key_exists()

Bei einem Array kann ein Schlüssel vorhanden sein, obwohl der zugehörige Wert null ist: $daten = ["name" => null]; Dann gilt: isset($daten["name"]) liefert false. Dagegen liefert array_key_exists("name", $daten) den Wert true, weil der Schlüssel "name" im Array vorhanden ist. isset() prüft also nicht nur das Vorhandensein, sondern zusätzlich, ob der Wert nicht null ist.

Verwendung bei einem PDO-Objekt

In den PDO-Programmen mit Transaktionsbehandlung findet sich beispielsweise:

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 kann: $pdo = new PDO(...);

Wird dabei eine Exception ausgelöst, wurde $pdo möglicherweise noch kein PDO-Objekt zugewiesen. Deshalb darf $pdo->inTransaction() nur aufgerufen werden, wenn $pdo tatsächlich vorhanden ist. Der Operator && verwendet eine Kurzschlussauswertung. Die zweite Bedingung wird nur geprüft, wenn die erste Bedingung true ergibt.

Der Ablauf lautet somit:

  • isset($pdo) prüft, ob $pdo vorhanden und nicht null ist.
  • Nur dann wird $pdo->inTransaction() aufgerufen.
  • inTransaction() prüft, ob eine Transaktion aktiv ist.
  • Nur wenn beide Bedingungen true ergeben, wird rollBack() ausgeführt.

Im vorliegenden Text kommt diese Anweisung beispielsweise in PDO-Programmen zur Verarbeitung vollständiger Rechnungen vor.

Noch genauer könnte geprüft werden, ob $pdo tatsächlich auf ein PDO-Objekt verweist:

if ($pdo instanceof PDO && $pdo->inTransaction()) {

  $pdo->rollBack();

}

Dazu müsste $pdo vor dem try-Block mit null initialisiert werden: $pdo = null;

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 angegebenen Variablen vorhanden sind und keine davon null enthält. PHP prüft die Variablen von links nach rechts. Sobald eine Variable die Bedingung nicht erfüllt, endet die Prüfung.

Zusammengefasst gilt:

isset() prüft, ob eine Variable oder ein Array-Element vorhanden ist und nicht null enthält. Ist dies der Fall, liefert isset() den Wert true, andernfalls false.

11.10 Der Null-Koaleszenzoperator

Der Null-Koaleszenzoperator ?? liefert einen Ersatzwert, wenn der Ausdruck auf seiner linken Seite nicht vorhanden ist oder null ergibt. Die allgemeine Form lautet:

$linkerAusdruck ?? $rechterAusdruck

Das Ergebnis ist:

  • der Wert des linken Ausdrucks, wenn dieser vorhanden und nicht null ist,
  • andernfalls der Wert des rechten Ausdrucks.

Verwendung bei Formulardaten

Ein typisches Beispiel ist: $name = $_POST["name"] ?? "";. Dies bedeutet:

  • Wenn $_POST["name"] vorhanden ist und nicht null enthält, wird dieser Wert verwendet.
  • Andernfalls wird die leere Zeichenkette "" verwendet.

Dadurch erhält $name auch beim ersten Aufruf der Webseite einen definierten Wert, obwohl das Formular noch nicht abgesendet wurde.

Entsprechende Schreibweise mit isset()

Die Anweisung $name = $_POST["name"] ?? ""; entspricht folgender ausführlichen Schreibweise:

if (isset($_POST["name"])) {

  $name = $_POST["name"];

} else {

  $name = "";

}

Dasselbe kann mit dem Bedingungsoperator geschrieben werden:

$name = isset($_POST["name"]) ? $_POST["name"] : "";

Der Null-Koaleszenzoperator ist somit eine kurze Schreibweise für eine Prüfung nach dem Prinzip von isset(). Er erzeugt keine Warnung, wenn das geprüfte Array-Element nicht vorhanden ist.

Nicht mit einer Prüfung auf "leer" verwechseln

Der Operator ?? prüft nicht, ob ein vorhandener Wert leer, falsch oder gleich 0 ist. Die folgenden vorhandenen Werte werden unverändert übernommen:

0

""

"0"

false

[]

Beispiel:

$anzahl = 0;

$wert = $anzahl ?? 10;

Da $anzahl vorhanden ist und nicht null enthält, erhält $wert den Wert 0. Der Ersatzwert 10 wird nicht verwendet. Nur wenn der linke Ausdruck nicht vorhanden ist oder null ergibt, kommt der rechte Ausdruck zum Einsatz:

$anzahl = null;

$wert = $anzahl ?? 10;

Nun erhält $wert den Wert 10. Weiteres Beispiel aus den Programmen: $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: $action = "";

Nach dem Absenden des Formulars kann $_POST["action"] beispielsweise den Wert "save" enthalten. Dann wird dieser Wert übernommen: $action = "save"; Der Operator prüft jedoch nicht, ob "save" ein zulässiger Wert ist. Eine erforderliche inhaltliche Prüfung muss anschließend gesondert erfolgen.

Kurzschlussauswertung

Der Null-Koaleszenzoperator arbeitet mit einer Kurzschlussauswertung. Der rechte Ausdruck wird nur ausgewertet, wenn der linke Ausdruck nicht vorhanden ist oder null ergibt. Beispiel:

$wert = $vorhanden ?? ersatzwertErmitteln();

Wenn $vorhanden einen Wert ungleich null enthält, wird die Funktion ersatzwertErmitteln() nicht aufgerufen.

Mehrere Ersatzmöglichkeiten

Mehrere Null-Koaleszenzoperatoren können miteinander verbunden werden:

$name = $_POST["name"] ?? $_GET["name"] ?? "Unbekannt";

PHP verwendet hier:

  • den Wert aus $_POST["name"], wenn er vorhanden und nicht null ist,
  • andernfalls den Wert aus $_GET["name"], wenn dieser vorhanden und nicht null ist,
  • andernfalls die Zeichenkette "Unbekannt".

11.11 Joins

Ein Join verknüpft Daten aus zwei oder mehr Relationen anhand übereinstimmender Attributwerte. In relationalen Datenbanken erfolgen solche Verknüpfungen typischerweise über Primärschlüssel und Fremdschlüssel.

SQL bietet unterschiedliche Möglichkeiten, solche Verknüpfungen zu realisieren. Die folgenden Beispiele zeigen zwei gleichwertige Schreibweisen zur Verknüpfung aller vier Relationen der Datenbank KuReAr die ausdrückliche Verwendung von INNER JOIN und die klassische Schreibweise mit Verknüpfungsbedingungen in der WHERE-Klausel.

Klassische Join-Schreibweise (impliziter Join)
Bei der klassischen Join-Schreibweise, die die Prinzipien der relationalen Theorie widerspiegelt, werden die Relationen in der FROM-Klausel aufgeführt und durch Kommas voneinander getrennt. Diese Schreibweise wird auch als impliziter Join bezeichnet. Die zugehörigen Verknüpfungsbedingungen werden in der WHERE-Klausel angegeben.
Im Gegensatz zur ausdrücklichen Schreibweise mit INNER JOIN werden bei dieser Variante die Schlüsselwörter JOIN und ON nicht verwendet.

11.11.1 Verknüpfung von Relationen mit INNER JOIN

Die folgende SQL-Anweisung verknüpft die Daten aus rechkoepfe, kunden, rechpos und artikel:

SELECT

  rk.ReNr,rk.ReDatum,k.KuNr,k.Name,rp.PosNr,a.ArtNr,

  a.Bezeichnung,rp.Menge,a.Preis

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;

Zur Syntax der Verknüpfungen:

FROM rechkoepfe AS rk

Die FROM-Klausel legt die erste an der Abfrage beteiligte Relation fest. Mit dem Schlüsselwort AS wird der Relation rechkoepfe der Aliasname rk zugewiesen. Dieser kann anschließend zur Bezeichnung ihrer Attribute verwendet werden, beispielsweise rk.ReNr.

INNER JOIN kunden AS k

Mit INNER JOIN wird eine weitere Relation in die Abfrage aufgenommen. Hier ist dies die Relation kunden, die den Aliasnamen k erhält.

ON rk.KuNr = k.KuNr

Die ON-Klausel legt die Bedingung für die Verknüpfung der beiden Relationen fest. Der Fremdschlüssel KuNr in rechkoepfe wird mit dem entsprechenden Primärschlüssel in kunden verglichen. Nur Kombinationen von Datensätzen, bei denen diese Werte übereinstimmen, werden in das Ergebnis aufgenommen.

INNER JOIN rechpos AS rp

Mit diesem Ausdruck wird die Relation rechpos aufgenommen. Sie erhält den Aliasnamen rp.

ON rk.ReNr = rp.ReNr

Die Rechnungsnummer in rechkoepfe wird mit der entsprechenden Rechnungsnummer in rechpos verglichen. Dadurch werden die Rechnungspositionen den jeweiligen Rechnungsköpfen zugeordnet.

INNER JOIN artikel AS a

Damit wird die Relation artikel aufgenommen. Sie erhält den Aliasnamen a.

ON rp.ArtNr = a.ArtNr

Die Artikelnummer in rechpos wird mit der entsprechenden Artikelnummer in artikel verglichen. Dadurch werden den Rechnungspositionen die jeweiligen Artikelbezeichnungen und Preise zugeordnet. Damit verbinden drei Verknüpfungsbedingungen die vier Relationen:

rk.KuNr = k.KuNr

rk.ReNr = rp.ReNr

rp.ArtNr = a.ArtNr

Jede INNER JOIN-Klausel legt die zusätzlich aufzunehmende Relation fest. Die zugehörige ON-Klausel enthält die Verknüpfungsbedingung.

Das Schlüsselwort INNER kann weggelassen werden. JOIN und INNER JOIN haben hier dieselbe Bedeutung. Die ausdrückliche Schreibweise INNER JOIN verdeutlicht, dass nur Datensätze mit übereinstimmenden Verknüpfungswerten berücksichtigt werden.

11.11.2 Verknüpfung von Relationen ohne das Schlüsselwort JOIN

Dieselbe Abfrage kann auch in der klassischen SQL-Schreibweise ohne das Schlüsselwort JOIN formuliert werden:

SELECT

  rk.ReNr,rk.ReDatum,k.KuNr,k.Name,rp.PosNr,a.ArtNr,

  a.Bezeichnung,rp.Menge,a.Preis

FROM rechkoepfe AS rk,kunden AS k,rechpos AS rp,

  artikel AS a

WHERE

rk.KuNr = k.KuNr AND

rk.ReNr = rp.ReNr AND

rp.ArtNr = a.ArtNr;

Bei dieser Schreibweise werden alle vier Relationen in der FROM-Klausel aufgeführt und durch Kommas voneinander getrennt. Die Verknüpfungsbedingungen stehen in der WHERE-Klausel. Dort werden die jeweils zusammengehörenden Primärschlüssel- und Fremdschlüsselwerte gleichgesetzt.

Mit dem Schlüsselwort AND werden die drei Bedingungen verbunden. Eine Kombination von Datensätzen wird nur dann in das Ergebnis aufgenommen, wenn alle drei Bedingungen erfüllt sind.

Ohne diese Bedingungen würde ein kartesisches Produkt entstehen. Dabei würde jeder Datensatz einer Relation mit jedem Datensatz der anderen Relationen kombiniert.

11.11.3 Vergleich der beiden Varianten

Beide SQL-Anweisungen liefern dasselbe Ergebnis. Der wesentliche Unterschied besteht darin, an welcher Stelle die Verknüpfungsbedingungen formuliert werden:

  • INNER JOIN: Jede zusätzliche Relation wird mit INNER JOIN aufgenommen. Die jeweilige Verknüpfungsbedingung folgt auf ON.
  • Klassische Schreibweise: Alle Relationen stehen in der FROM-Klausel. Ihre Verknüpfungsbedingungen werden gemeinsam in der WHERE-Klausel angegeben.

Die ausdrückliche Schreibweise mit INNER JOIN ist im Allgemeinen vorzuziehen, weil sie die Bedingungen zur Verknüpfung der Relationen deutlich von zusätzlichen Auswahlbedingungen trennt.

Bei der klassischen Schreibweise stehen dagegen sowohl Verknüpfungsbedingungen als auch weitere Auswahlbedingungen in der WHERE-Klausel. Soll beispielsweise nur die Rechnung mit der Nummer 23012 ausgegeben werden, könnte die klassische Abfrage um folgende Bedingung ergänzt werden:

AND rk.ReNr = 23012

Bei der Schreibweise mit INNER JOIN würde diese zusätzliche Auswahlbedingung normalerweise in einer eigenen WHERE-Klausel nach den Verknüpfungen stehen.

Für die Beispiele dieses Textes wird deshalb meist die ausdrückliche Schreibweise mit INNER JOIN bevorzugt. Die klassische Schreibweise wird ergänzend vorgestellt, um zu zeigen, wie sich relationale Verknüpfungen auch durch die Gleichsetzung zusammengehörender Schlüsselwerte in der WHERE-Klausel realisieren lassen.