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: |
|
|
|
|
Beide Erweiterungen ermöglichen es, eine Datenbankverbindung aufzubauen, SQL-Anweisungen auszuführen und die Ergebnisse zu verarbeiten. Außerdem unterstützen beide Prepared Statements und Transaktionen. |
|
MySQLi ist speziell für MySQL und die weitgehend kompatible Datenbank MariaDB vorgesehen. PDO stellt dagegen eine einheitliche Programmierschnittstelle für unterschiedliche Datenbanksysteme bereit. |
|
Die Grundideen beider Techniken beschreibt die folgende Tabelle. |
|
PDO und MySQLi - Vergleich |
|
| Merkmal |
PDO |
MySQLi |
| Vollständige Bezeichnung |
PHP Data Objects |
MySQL Improved |
| Unterstützte Datenbanksysteme |
Viele DBMS (MySQL, PostgreSQL, SQLite, ...) |
Nur MySQL und kompatible Systeme wie MariaDB |
| Programmierstil |
Objektorientiert |
Objektorientiert und prozedural |
| Prepared Statements |
Ja |
Ja |
| Transaktionen |
Ja |
Ja |
| Wechsel des Datenbanksystems |
grundsätzlich erleichtert |
nicht vorgesehen |
| Verwendete Klasse |
PDO |
mysqli |
| |
PDO
|
|
PDO stellt eine einheitliche Schnittstelle für den Zugriff auf unterschiedliche Datenbanksysteme bereit. Methoden wie prepare(), execute() und fetch() werden unabhängig vom eingesetzten Datenbanksystem auf ähnliche Weise verwendet. |
|
PDO vereinheitlicht jedoch nicht die unterschiedlichen SQL-Dialekte. Wird das Datenbanksystem gewechselt, müssen daher gegebenenfalls auch SQL-Anweisungen angepasst werden. |
|
Der Verbindungsaufbau für die Datenbank KuReAr kann folgendermaßen erfolgen: |
|
|
$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";
|
|
|
$pdo = new PDO($dsn,"root","",[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
Mit $dsn (Data Source Name) entsteht ein String, der Treiber, Datenbank und den verwendeten Zeichensatz angibt. |
|
- $pdo verweist dank new PDO() auf ein Objekt der Klasse PDO.
- mysql: bedeutet, dass der SQL-Treiber innerhalb von PDO verwendet werden soll.
- host=localhost zeigt an, dass der Datenbankserver lokal läuft.
- dbname=KuReAr gibt die zu bearbeitende Datenbank an.
- charset=utf8mb4 gibt den Zeichensatz an.
- root gibt den MySQL-Datenbankbenutzer an.
- "" bedeutet: kein Passwort
- [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] setzt den Fehlermodus so, dass PDO bei Problemen Exceptions wirft. Ohne diese Option würde PDO oft nur Fehlercodes liefern, die man manuell prüfen müsste. Siehe Anhang.
|
|
MySQLi |
|
MySQLi ist eine spezielle Erweiterung nur für MySQL-Datenbanken. Der Verbindungsaufbau geschieht mit folgenden Befehlen (am Beispiel der Datenbank KuReAr): |
|
|
$con = new mysqli("host", "root", "Passwort", "DB-Bezeichnung");
|
|
|
if ($con->connect_error) {
|
|
|
die("Verbindungsfehler: " . $con->connect_error);
|
|
|
}
|
|
- $con ist eine PHP-Variable. Sie speichert ein Objekt der Klasse mysqli, das die Verbindung vom jeweiligen Programm zur Datenbank herstellt. Ein Objekt $con verfügt unter anderem über die Eigenschaft $con->connect_error und über Methoden wie $con->query(), $con->execute_query(), $con->close(), $con->prepare. Übrigens: "con" kommt von "connection". Die Bezeichnung dieser Variablen kann aber frei gewählt werden.
- mysqli: Die entsprechende Klasse von mySQL für den Verbindungsaufbau.
- new mysqli(): Instantiierung eines mysqli-Objekts, das den jeweiligen Verbindungsaufbau realisiert.
- Host: Gibt den MySQL-Server an. Beim Testen wird hier oft localhost vorkommen. Dies bedeutet, dass der Server auf demselben Rechner (wie die sonstigen Programme) läuft. Alternativ kann hier z.B. eine IP-Adresse stehen.
- root: MySQL-Benutzerkonto
- Passwort: für das Benutzerkonto
- DB-Bezeichnung: Bezeichnung der Datenbank.
|
|
Die folgende If-Anweisung sichert gegen ein Scheitern des Verbindungsaufbaus ab: |
|
- $con->connect_error prüft, ob beim Verbindungsaufbau ein Fehler aufgetreten ist. Falls es so ist, startet die Funktion "die()" ("sterbe").
- die() oder exit() beendet das Skript sofort und gibt eine Fehlermeldung aus.
|
|
MySQLi unterstützt Prepared Statements, Stored Procedures und erlaubt mehrere gleichzeitige Abfragen. Außerdem kann es auch prozedural verwendet werden: |
|
|
$con = mysqli_connect("localhost", "root", "", "KuReAr");
|
|
Dies ist möglich, weil PHP beide Programmierstile zulässt, den prozeduralen und den objektorientierten und weil dies mit der Klasse MySQLi auch umgesetzt wurde. |
|
2.2 KundenAbfragenMySQLi_1.php |
|
2.2.1 Merkmale des Programms |
|
- Start durch Browser-Befehl
- Einfache Abfrage
- Direkter Zugriff auf die Datenbank.
- Verwendete Klasse für Verbindungsaufbau: mysqli
- HTML-Ausgabe des Ergebnisses
|
|
Das erste Programm führt eine einfache, unveränderliche SQL-Abfrage aus. Da die SQL-Anweisung keine Benutzereingaben oder anderen veränderlichen Werte enthält, ist hierfür kein Prepared Statement erforderlich. Im nachfolgenden Beispiel wird die Abfrage um einen Suchwert ergänzt und deshalb als Prepared Statement ausgeführt. |
|
2.2.2 Das Programm |
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>Relation Kunden</title>
|
|
|
<link rel="stylesheet" type="text/css" href="mySQL_1.css">
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
<p class="text-red">
|
|
|
Relation Kunden - mit Spaltenüberschriften:
|
|
|
</p>
|
|
|
<table border="1">
|
|
|
<caption class="text-bold">Relation Kunden</caption>
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>Kundennummer</th>
|
|
|
<th>Kundenname</th>
|
|
|
<th>Kundenvorname</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
|
|
|
<?php
|
|
|
|
|
|
/*
|
|
|
Verbindung zur Datenbank aufbauen.
|
|
|
mysqli steht für MySQL Improved. Mit new mysqli()wird ein Objekt
|
|
|
der Klasse mysqli erzeugt. Die Variable $con verweist auf dieses
|
|
|
Objekt und ermöglicht den Zugriff auf seine Eigenschaften und
|
|
|
Methoden.
|
|
|
*/
|
|
|
$con = new mysqli("localhost","root","","KuReAr");
|
|
|
|
|
|
/*
|
|
|
Prüfen, ob beim Aufbau der Datenbankverbindung ein Fehler
|
|
|
aufgetreten ist. Falls ein Fehler auftritt, beendet die(), nach der
|
|
|
Ausgabe der Meldung, sofort die weitere Ausführung des Programms.
|
|
|
*/
|
|
|
if ($con->connect_error) {
|
|
|
die("Datenbankverbindung fehlgeschlagen: "
|
|
|
. $con->connect_error);
|
|
|
}
|
|
|
|
|
|
// Zeichensatz für die Datenbankverbindung festlegen.
|
|
|
// utf8mb4 unterstützt Umlaute und andere Unicode-Zeichen.
|
|
|
$con->set_charset("utf8mb4");
|
|
|
|
|
|
// SQL-Anweisung festlegen. Mit AS erhalten die ausgewählten Spalten
|
|
|
// neue Bezeichnungen. Diese Bezeichnungen werden später als
|
|
|
// Schlüssel des assoziativen Arrays verwendet.
|
|
|
$sql = "SELECT KuNr AS Kundennummer, Name AS Kundenname,
|
|
|
Vorname AS Kundenvorname FROM kunden ORDER BY KuNr";
|
|
|
|
|
|
/*
|
|
|
SQL-Anweisung ausführen.
|
|
|
execute_query() ist eine Methode der Klasse mysqli. Bei einer
|
|
|
SELECT-Anweisung liefert sie ein Objekt der Klasse mysqli_result
|
|
|
zurück. Die Variable $res verweist auf dieses Ergebnisobjekt.
|
|
|
*/
|
|
|
$res = $con->execute_query($sql);
|
|
|
|
|
|
// num_rows ist eine Eigenschaft des Ergebnisobjekts. Sie enthält
|
|
|
// die Anzahl der durch die Abfrage gefundenen Datensätze.
|
|
|
if ($res->num_rows === 0) {
|
|
|
echo "
|
|
|
<tr>
|
|
|
<td colspan=\"3\">
|
|
|
Keine Ergebnisse
|
|
|
</td>
|
|
|
</tr>
|
|
|
";
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
fetch_assoc() liest jeweils einen Datensatz aus dem Ergebnisobjekt
|
|
|
und liefert ihn als assoziatives Array.
|
|
|
Die Spaltenbezeichnungen, beziehungsweise die mit AS vergebenen
|
|
|
Namen bilden die Schlüssel des Arrays.
|
|
|
Die Schleife wird ausgeführt, solange ein weiterer Datensatz
|
|
|
vorhanden ist.
|
|
|
*/
|
|
|
while ($dsatz = $res->fetch_assoc()) {
|
|
|
echo "<tr>";
|
|
|
echo "<td>" . htmlspecialchars((string)$dsatz["Kundennummer"],
|
|
|
ENT_QUOTES,"UTF-8") . "</td>";
|
|
|
echo "<td>" . htmlspecialchars($dsatz["Kundenname"],
|
|
|
ENT_QUOTES,"UTF-8") . "</td>";
|
|
|
echo "<td>" . htmlspecialchars($dsatz["Kundenvorname"],
|
|
|
ENT_QUOTES, "UTF-8") . "</td>";
|
|
|
echo "</tr>";
|
|
|
}
|
|
|
|
|
|
// Ergebnisobjekt schließen und die dafür verwendeten Ressourcen
|
|
|
// freigeben.
|
|
|
$res->close();
|
|
|
|
|
|
// Datenbankverbindung schließen und die damit
|
|
|
// verbundenen Ressourcen freigeben.
|
|
|
$con->close();
|
|
|
?>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</body>
|
|
|
</html>
|
|
Die Konsolenausgabe |
|

|
|
2.2.3 Die Stylesheets |
|
Datei mySQL_1.css |
|
|
body {margin: 10px;color: black;font-family: Arial, sans-serif;}
|
|
|
h1 {margin: 5px 0 10px;color: blue;font-family: Calibri, Arial, sans-serif;font-size: 16pt;font-weight: bold;text-align: left;}
|
|
|
p {margin: 10px 0;}
|
|
|
table {border-collapse: collapse;font-family: inherit;}
|
|
|
thead {font-size: 10pt;font-weight: bold;}
|
|
|
td,th {padding: 3px;border: 1px solid black;text-align: left;vertical-align: middle;}
|
|
|
.text-red {color: red;font-size: 14pt;}
|
|
|
.text-bold {font-size: 14pt;font-weight: bold;}
|
|
2.2.4 Anmerkungen |
|
Folgende Klassen kommen hier zum Einsatz |
|
- mysqli: Erlaubt die Einrichtung einer Verbindung zu einer MySQL-Datenbank. Jedes erzeugte Objekt, z.B. $con, verfügt über Attribute und Methoden zur Arbeit mit der Datenbank. Eine Methode von mysqli ist execute_query(). Diese führt eine SQL-Abfrage aus und liefert ein Ergebnisobjekt der Klasse mysqli_result.
- mysqli_result: Ein Objekt der Klasse mysqli_result enthält somit das Ergebnis der jeweiligen SQL-Abfrage. Zu den Attributen der Klasse gehört num_rows. Eine Methode ist fetch_assoc(). Sie liest jeweils eine Zeile aus dem Ergebnis und gibt sie als assoziatives Array zurück.
|
|
Verschmelzung |
|
Das Programm macht deutlich, wie prozedurale und objektorientierte Programmierelemente in PHP miteinander verbunden werden können. Mit der Anweisung |
|
|
$con = new mysqli("localhost", "root", "", "KuReAr");
|
|
wird ein Objekt der Klasse mysqli erzeugt. Die PHP-Variable $con verweist anschließend auf dieses Objekt. Über $con kann auf die Eigenschaften und Methoden des Objekts zugegriffen werden. |
|
Beispielsweise: |
|
|
$con->connect_error // Eigenschaft
|
|
|
$con->query($sql) // Methode
|
|
Zur Programmierung |
|
- fetch_assoc() gibt die Datensätze nicht selbst aus. Die Methode liest jeweils einen Datensatz und liefert ihn als assoziatives Array zurück. Die Ausgabe erfolgt anschließend beispielsweise mit echo.
- Die aus der Datenbank gelesenen Werte werden vor ihrer Einfügung in die HTML-Ausgabe mit htmlspecialchars() behandelt (vgl. Anhang). Dadurch werden Sonderzeichen umgewandelt und nicht als Bestandteile des HTML-Codes interpretiert.
- Die Methode execute_query() steht seit PHP 8.2 zur Verfügung. Enthält die SQL-Anweisung keine Platzhalter, kann bei älteren PHP-Versionen stattdessen $con->query($sql) verwendet werden. Enthält sie Platzhalter, muss die Abfrage mit prepare(), bind_param() und execute() als Prepared Statement ausgeführt werden.
|
|
2.3 KundenAbfragenMySQLi_2.php |
|
2.3.1 Merkmale des Programms |
|
- Start durch Browser-Befehl
- Abfrage mit Hilfe eines Prepared Statements
|
|
Das zweite Beispiel zeigt wiederum die grundsätzliche Struktur eines PHP-Programms, das mit einem Browser gestartet wird und eine Datenbank mit der Klasse mysqli abfragt. Die Abfrage erfolgt nun aber als Prepared Statement und nicht mit einer einfachen SQL-Abfrage, was aus Sicherheitsgründen sehr zu empfehlen ist. |
Prepared Statement |
2.3.2 Das Programm |
|
|
<?php
|
|
|
/*
|
|
|
Kundennummer, nach der gesucht werden soll.
|
|
|
In diesem Beispiel wird die Kundennummer unmittelbar
|
|
|
im Programm festgelegt. In einer Webanwendung würde
|
|
|
sie normalerweise aus einem Eingabeformular stammen.
|
|
|
*/
|
|
|
$id = 1002;
|
|
|
|
|
|
/*
|
|
|
Das HTML-Element <pre> sorgt dafür, dass Leerzeichen und
|
|
|
Zeilenumbrüche der Eingabe bei der Ausgabe erhalten bleiben.
|
|
|
Dadurch lässt sich das Ergebnis von print_r() übersichtlich
|
|
|
darstellen.
|
|
|
*/
|
|
|
echo "<pre>";
|
|
|
echo "--- Verbindungsaufbau mit MySQLi ---\n";
|
|
|
echo "--- KuReAr, Relation Kunden ---\n\n";
|
|
|
|
|
|
/*
|
|
|
Verbindung zur Datenbank aufbauen.
|
|
|
Mit new mysqli() wird ein Objekt der Klasse mysqli
|
|
|
erzeugt:
|
|
|
- "localhost" bezeichnet den Datenbankserver.
|
|
|
- "root" ist der MySQL-Benutzer.
|
|
|
- "" ist das hier leere Passwort.
|
|
|
- "KuReAr" ist die Bezeichnung der Datenbank.
|
|
|
Die Variable $con verweist auf das erzeugte
|
|
|
mysqli-Verbindungsobjekt.
|
|
|
*/
|
|
|
$con = new mysqli("localhost","root","","KuReAr");
|
|
|
|
|
|
/* Prüfen, ob beim Verbindungsaufbau ein Fehler aufgetreten ist.
|
|
|
connect_error enthält im Fehlerfall die entsprechende
|
|
|
Fehlermeldung. die() gibt die Meldung aus und beendet
|
|
|
anschließend die Ausführung des Programms.
|
|
|
*/
|
|
|
if ($con->connect_error) {die(
|
|
|
"MySQLi-Verbindungsfehler: ". $con->connect_error. "\n");
|
|
|
}
|
|
|
|
|
|
// Zeichensatz für die Datenbankverbindung festlegen. utf8mb4
|
|
|
// unterstützt alle üblichen Unicode-Zeichen.
|
|
|
$con->set_charset("utf8mb4");
|
|
|
|
|
|
/*
|
|
|
Prepared Statement vorbereiten.
|
|
|
Das Fragezeichen ist ein Platzhalter für die Kundennummer.
|
|
|
Der konkrete Wert wird erst anschließend mit bind_param() an
|
|
|
diesen Platzhalter gebunden. prepare() liefert ein Objekt der
|
|
|
Klasse mysqli_stmt.
|
|
|
Die Variable $stmt verweist auf dieses Statement-Objekt.
|
|
|
*/
|
|
|
$stmt = $con->prepare(
|
|
|
"SELECT * FROM Kunden WHERE KuNr = ?"
|
|
|
);
|
|
|
|
|
|
// Prüfen, ob das Prepared Statement erfolgreich
|
|
|
// vorbereitet werden konnte.
|
|
|
if (!$stmt) {
|
|
|
die("Vorbereiten der SQL-Anweisung fehlgeschlagen: "
|
|
|
. $con->error . "\n"
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
Im positiven Fall geht es hier weiter:
|
|
|
Den Wert der Variablen $id an den Platzhalter binden.
|
|
|
Das Typkennzeichen "i" bedeutet, dass der übergebene
|
|
|
Wert als Integer behandelt wird.
|
|
|
*/
|
|
|
$stmt->bind_param("i",$id);
|
|
|
|
|
|
/*
|
|
|
Prepared Statement ausführen. Erst jetzt wird die SQL-Anweisung
|
|
|
mit der gebundenen Kundennummer an den Datenbankserver übermittelt.
|
|
|
Zum Kontrollfluss an dieser Stelle vgl. den Exkurs unten.
|
|
|
*/
|
|
|
if (!$stmt->execute()) {
|
|
|
die("Ausführen der SQL-Anweisung fehlgeschlagen: "
|
|
|
. $stmt->error . "\n"
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
Ergebnis der SELECT-Abfrage abrufen. get_result() liefert ein
|
|
|
Objekt der Klasse mysqli_result. Dieses Ergebnisobjekt kann
|
|
|
anschließend Datensatz für Datensatz gelesen werden.
|
|
|
*/
|
|
|
$result = $stmt->get_result();
|
|
|
|
|
|
// Prüfen, ob ein Datensatz mit der angegebenen
|
|
|
// Kundennummer gefunden wurde.
|
|
|
if ($result->num_rows === 0) {
|
|
|
echo "Kein Kunde mit der Kundennummer " . $id . " gefunden.\n";}
|
|
|
|
|
|
/*
|
|
|
Ergebnis Datensatz für Datensatz durchlaufen.
|
|
|
fetch_assoc() liefert den jeweils nächsten Datensatz als
|
|
|
assoziatives Array. Die Attributnamen der Relation bilden die
|
|
|
Schlüssel und die Attributwerte die zugehörigen Werte des Arrays.
|
|
|
Da KuNr normalerweise der Primärschlüssel ist, kann diese Abfrage
|
|
|
höchstens einen einzigen Datensatz liefern.
|
|
|
*/
|
|
|
while ($row = $result->fetch_assoc()) {
|
|
|
print_r($row);
|
|
|
echo "\n";
|
|
|
}
|
|
|
|
|
|
// Ergebnisobjekt schließen und die dafür verwendeten
|
|
|
// Ressourcen freigeben.
|
|
|
$result->close();
|
|
|
|
|
|
// Statement-Objekt schließen und die dafür verwendeten
|
|
|
// Ressourcen freigeben.
|
|
|
$stmt->close();
|
|
|
|
|
|
// Datenbankverbindung schließen.
|
|
|
$con->close();
|
|
|
|
|
|
// HTML-Element für die vorformatierte Ausgabe schließen.
|
|
|
echo "</pre>";
|
|
|
?>
|
|
2.3.3 Die Konsolenausgabe |
|

|
|
Ergebnis eines Programmlaufs, falls die Kundennummer nicht vorhanden ist |
|

|
|
2.3.4 Exkurs |
|
Wo ist der Kontrollfluss in folgendem Programmfragment (von oben): |
|
|
if (!$stmt->execute()) { die("Ausführen der SQL-Anweisung fehlgeschlagen: "
|
|
|
. $stmt->error . "\n");
|
|
|
}
|
|
Die Anweisung |
|
|
$stmt->execute()
|
|
wird tatsächlich ausgeführt, während PHP die Bedingung der if-Anweisung auswertet. Das Ergebnis dieses Methodenaufrufs wird dann sofort geprüft. Damit werden Ausführung und Fehlerprüfung in einer if-Anweisung miteinander verbunden. |
|
Der Ablauf im Einzelnen: |
|
Zuerst führt PHP diese Methode aus: |
|
|
$stmt->execute()
|
|
Die Methode liefert normalerweise einen booleschen Wert: |
|
- true - die SQL-Anweisung wurde erfolgreich ausgeführt
- false - bei der Ausführung ist ein Fehler aufgetreten
|
|
Vor dem Methodenaufruf steht der logische NICHT-Operator: ! Dieser kehrt den zurückgegebenen Wahrheitswert um. |
|
Erfolgreiche Ausführung |
|
Liefert execute() den Wert true, ergibt sich !true. Das Ergebnis ist also false. Damit ist die if-Bedingung nicht erfüllt. Der Fehlerblock wird übersprungen: |
|
|
die(...);
|
|
Das Programm wird nach der if-Anweisung fortgesetzt. |
|
Fehlerhafte Ausführung |
|
Liefert execute() den Wert false, ergibt sich !false. Das Ergebnis ist true. Damit ist die if-Bedingung erfüllt. Die Fehlermeldung wird ausgegeben und das Programm beendet: |
|
|
die(
|
|
|
"Ausführen der SQL-Anweisung fehlgeschlagen: " . $stmt->error
|
|
|
. "\n"
|
|
|
);
|
|
Gleichbedeutende ausführlichere Schreibweise |
|
Man könnte Ausführung und Prüfung auch auf zwei Anweisungen verteilen: |
|
|
$erfolgreich = $stmt->execute();
|
|
|
if (!$erfolgreich) {
|
|
|
die(
|
|
|
"Ausführen der SQL-Anweisung fehlgeschlagen: "
|
|
|
. $stmt->error . "\n"
|
|
|
);
|
|
|
}
|
|
2.3.5 Anmerkungen |
|
Zusammenfassung der Klassen und Methoden |
|
Die Klasse mysqli repräsentiert eine Verbindung zu einer MySQL-Datenbank. Sie ist also eine objektorientierte MySQLi-Schnittstelle mit dem Konstruktor |
mysqli |
|
new mysqli(host, user, password, database).
|
|
Sie hat das Attribut (im Programmierumfeld wird oft von Eigenschaft gesprochen) |
|
|
$con->connect_error
|
|
die eine Fehlermeldung enthält, falls die Verbindung fehlgeschlagen ist und u.a. folgende Methoden |
|
$con->prepare(string $sql), die ein Prepared Statement erstellt. |
|
$con->close(), mit der die Verbindung zur Datenbank geschlossen wird |
|
Insgesamt gilt: |
mysqli_stmt |
- Die Objekte der Klasse mysqli_stmt repräsentieren jeweils ein Prepared Statement.
- Die Methode $stmt->bind_param(string $types, mixed &$var, mixed &...$vars) bindet die PHP-Variablen an die Platzhalter der SQL-Anweisung. Das Zeichen & kennzeichnet die Übergabe als Referenz. Deshalb müssen Variablen und keine unmittelbar angegebenen Werte übergeben werden.
- Die Methode $stmt->execute() führt das vorbereitete Statement aus. Dies geschieht auf dem Server.
- Die Methode $stmt->get_result() liefert ein Ergebnisobjekt (mysqli_result).
- Die Methode $stmt->close() schließt das Statement.
|
|
Beispiel für mehrere Platzhalter: |
|
|
$stmt = $con->prepare(
|
|
|
"select * from kunden where KuNr = ? AND Name = ?"
|
|
|
);
|
|
|
$stmt->bind_param("is", $id, $name);
|
|
Erster Parameter integer, zweiter Parameter string. |
|
Die Klasse mysqli_result erfasst die Ergebnisse von SQL-Abfragen. Ihre Methode $result->fetch_assoc() liest eine Ergebniszeile als assoziatives Array. Die Schleife läuft, bis false zurückgegeben wird, also keine weitere Zeile mehr vorliegt. |
mysqli_result |
2.3.6 Zum Programm |
|
- bind_param() setzt den Wert nicht durch gewöhnliche Textersetzung in die SQL-Anweisung ein. SQL-Anweisung und Parameter werden getrennt an die Datenbank übermittelt. Es entsteht daher intern nicht einfach die Zeichenkette SELECT * FROM kunden WHERE KuNr = 1002. Sinngemäß wird diese Abfrage ausgeführt, technisch bleibt der Parameter aber von der SQL-Anweisung getrennt.
- Prepared Statements (vgl. auch den Anhang) schützen vor SQL-Injection, weil ein gebundener Wert als Datenwert und nicht als Bestandteil des SQL-Codes behandelt wird. Sie sollten grundsätzlich verwendet werden, wenn Werte von außen in eine SQL-Anweisung gelangen. Dies gilt nicht nur außerhalb eines lokalen XAMPP-Systems.
- Da nach einer Kundennummer gesucht wird, die normalerweise eindeutig ist, liefert die Abfrage höchstens einen einzigen Datensatz. Die while-Schleife ist trotzdem korrekt und lässt sich später leicht für Abfragen mit mehreren Ergebnissen weiterverwenden.
- get_result() setzt bei PHP in der Regel den MySQL-Native-Driver mysqlnd voraus. Dieser ist in üblichen XAMPP-Installationen vorhanden.
- Die Ausgabe mit print_r() eignet sich vor allem zum Lernen und zur Fehlersuche. Für eine fertige Webseite sollten die Werte in einer HTML-Tabelle ausgegeben und dabei mit htmlspecialchars() (vgl. Anhang) behandelt werden.
|
|
2.4 KundenAbfragenPDO_1.php |
|
2.4.1 Merkmale des Programms |
|
- Verwendete Klasse für Verbindungsaufbau: PDO
- Absicherung durch Prepared Statements
- Start durch Browser-Befehl
- HTML-Ausgabe des Ergebnisses
|
|
Nun die Verbindungsaufnahme mit Hilfe der Klasse PDO. Wieder erfolgt der Zugriff auf die Relation Kunden der Datenbank KuReAr (Kunden - Rechnungen - Artikel). |
|
2.4.2 Das Programm |
|
|
<?php
|
|
|
|
|
|
/*
|
|
|
Kundennummer, nach der gesucht werden soll. Auch in diesem Beispiel
|
|
|
wird der Suchwert unmittelbar im Programm festgelegt. In einer
|
|
|
Webanwendung würde er üblicherweise aus einem Eingabeformular
|
|
|
stammen.
|
|
|
*/
|
|
|
$id = 1002;
|
|
|
echo "<pre>";
|
|
|
echo "--- Verbindungsaufbau mit PDO ---\n";
|
|
|
echo "--- KuReAr, Relation kunden ---\n\n";
|
|
|
|
|
|
try {
|
|
|
/*
|
|
|
Verbindung zur Datenbank aufbauen.
|
|
|
PDO steht für PHP Data Objects. Die Variable $pdo verweist auf das
|
|
|
erzeugte PDO-Objekt. Dieses Objekt stellt die Verbindung zur
|
|
|
Datenbank dar.
|
|
|
Der DSN (Data Source Name) enthält:
|
|
|
- den verwendeten Datenbanktreiber: mysql
|
|
|
- den Datenbankserver: localhost
|
|
|
- die Bezeichnung der Datenbank: KuReAr
|
|
|
- den Zeichensatz: utf8mb4
|
|
|
"root" ist der MySQL-Benutzer. Die darauffolgenden
|
|
|
Anführungszeichenbedeuten, dass kein Passwort verwendet wird.
|
|
|
PDO::ERRMODE_EXCEPTION bewirkt, dass PDO bei einem Datenbankfehler
|
|
|
eine PDOException auslöst.
|
|
|
*/
|
|
|
$pdo = new PDO(
|
|
|
"mysql:host=localhost;dbname=KuReAr;charset=utf8mb4",
|
|
|
"root","",[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
|
|
|
|
/*
|
|
|
Prepared Statement vorbereiten.
|
|
|
:id ist ein benannter Platzhalter für die gesuchte Kundennummer.
|
|
|
prepare() liefert ein Objekt der Klasse PDOStatement.
|
|
|
Die Variable $stmt verweist auf dieses Statement-Objekt.
|
|
|
*/
|
|
|
$stmt = $pdo->prepare(
|
|
|
"SELECT * FROM kunden WHERE KuNr = :id"
|
|
|
);
|
|
|
|
|
|
/*
|
|
|
Prepared Statement ausführen.
|
|
|
Das assoziative Array ordnet dem Platzhalter :id den Wert der
|
|
|
Variablen $id zu. Beim Array-Schlüssel kann der Doppelpunkt
|
|
|
weggelassen werden. SQL-Anweisung und Parameterwert bleiben dabei
|
|
|
voneinander getrennt.
|
|
|
*/
|
|
|
$stmt->execute(["id" => $id]);
|
|
|
|
|
|
/*
|
|
|
Alle gefundenen Datensätze abrufen.
|
|
|
fetchAll() liefert ein Array, das sämtliche Ergebnisdatensätze
|
|
|
enthält.
|
|
|
PDO::FETCH_ASSOC legt fest, dass jeder Datensatz als assoziatives
|
|
|
Array bereitgestellt wird. Die Spaltennamen der Relation bilden die
|
|
|
Schlüssel des Arrays.
|
|
|
*/
|
|
|
$datensaetze = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
|
|
// Prüfen, ob ein Datensatz gefunden wurde.
|
|
|
if (count($datensaetze) === 0) {
|
|
|
echo "Kein Kunde mit der Kundennummer " . $id . " gefunden.\n";
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
Die gefundenen Datensätze nacheinander ausgeben. print_r() ist eine
|
|
|
eingebaute PHP-Funktion. Sie gibt den Inhalt und die Struktur einer
|
|
|
Variablen, insbesondere eines Arrays, in einer für Menschen gut
|
|
|
lesbaren Form aus.
|
|
|
Falls die Anzahl der Datensätze Null ist, ist $datensaetze ein
|
|
|
leeres Array. Die foreach-Schleife wird deshalb kein einziges Mal
|
|
|
durchlaufen. Die Schleife beendet sich, ohne dass print_r() oder
|
|
|
echo "\n" ausgeführt werden.
|
|
|
*/
|
|
|
foreach ($datensaetze as $row) {
|
|
|
print_r($row);
|
|
|
echo "\n";
|
|
|
}
|
|
|
|
|
|
//von try (ganz oben)
|
|
|
} catch (PDOException $e) {
|
|
|
|
|
|
// PDO-Datenbankfehler abfangen. getMessage() liefert die zur
|
|
|
// Exception gehörende Fehlermeldung.
|
|
|
echo "PDO-Fehler: " . $e->getMessage() . "\n";
|
|
|
} //schließt catch
|
|
|
|
|
|
// HTML-Element für die vorformatierte Ausgabe schließen.
|
|
|
echo "</pre>";
|
|
|
?>
|
|
Die Konsolenausgabe |
|

|
|
2.4.3 Anmerkungen |
|
Exkurs: Was genau ist $stmt? |
|
Betrachten wir folgende Anweisung: |
|
|
$stmt = $pdo->prepare("SELECT * FROM kunden WHERE KuNr = :id");
|
|
Die Methode prepare() bereitet die SQL-Anweisung zur späteren Ausführung vor. Die Ausführung selbst erfolgt zu diesem Zeitpunkt noch nicht. Nach erfolgreicher Ausführung von prepare() verweist die Variable $stmt auf ein Objekt der Klasse PDOStatement. Dieses Statement-Objekt repräsentiert die vorbereitete SQL-Anweisung und stellt Methoden bereit, mit denen Platzhalter gebunden, die Anweisung ausgeführt und Abfrageergebnisse abgerufen werden können. |
|
Der benannte Platzhalter :id wird zunächst noch nicht durch einen konkreten Wert ersetzt. Der Wert kann beispielsweise mit bindValue() gebunden werden: |
|
|
$stmt->bindValue(":id", $id, PDO::PARAM_INT);
|
|
Anschließend wird die vorbereitete SQL-Anweisung ausgeführt: $stmt->execute(); Alternativ kann der Wert unmittelbar beim Aufruf von execute() übergeben werden: |
|
|
$stmt->execute(["id" => $id]);
|
|
Wichtige Methoden des PDOStatement-Objekts sind: |
|
- bindParam() bindet eine Variable als Referenz an einen Platzhalter
- bindValue() bindet einen Wert an einen Platzhalter
- execute() führt die vorbereitete SQL-Anweisung aus
- fetch() ruft den nächsten Ergebnisdatensatz ab
- fetchAll() ruft alle Ergebnisdatensätze ab
|
|
Zum Programm |
|
Das Programm demonstriert, wie ein einzelner Datensatz aus der Datenbank abgefragt und ausgegeben wird. Dabei wird die PDO-Erweiterung verwendet, die eine objektorientierte und sichere Schnittstelle zur Datenbank bereitstellt. |
|
Zu Beginn wird mit der Variablen $id ein Suchkriterium festgelegt. Es handelt sich um eine Ganzzahl, die den Schlüsselwert eines bestimmten Datensatzes repräsentiert. Dieser Wert wird später in die SQL-Abfrage eingesetzt. |
|
Die Ausgabe erfolgt mit der Funktion echo. Durch die Verwendung des HTML-Tags <pre> wird eine formatierte Darstellung erreicht, bei der Zeilenumbrüche und Einrückungen erhalten bleiben. Dies ist besonders hilfreich für Debugging-Zwecke oder zur Veranschaulichung von Datenstrukturen. |
|
Die eigentliche Datenbankoperation ist in einen try/catch-Block eingebettet. Dieses Konstrukt dient der strukturierten Fehlerbehandlung. Im try-Block steht der Code, der ausgeführt werden soll, während der catch-Block aufgerufen wird, falls ein Fehler auftritt. Im Unterschied zu einem einfachen Abbruch mit die() ermöglicht dieses Vorgehen eine kontrollierte Reaktion auf Fehler. |
try/catch-Block |
Innerhalb des try-Blocks wird zunächst eine Verbindung zur Datenbank aufgebaut, indem ein PDO-Objekt erzeugt wird. Die Verbindungsparameter werden in Form eines sogenannten DSN (Data Source Name) übergeben. Die Eingabe |
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
|
stellt sicher, dass Fehler als Exceptions gemeldet werden. |
|
Anschließend wird ein Prepared Statement vorbereitet. Die SQL-Anweisung enthält mit :id einen benannten Platzhalter. Dieser wird erst beim Ausführen der Anweisung mit einem konkreten Wert belegt. Das erhöht die Sicherheit, da es vor SQL-Injection schützt, und sorgt zugleich für eine klare Trennung von SQL-Code und Daten. |
|
Mit der Methode execute() wird die vorbereitete Anweisung ausgeführt. Dabei wird der Platzhalter :id mit dem Wert der Variablen $id verknüpft. |
|
Die Ergebnismenge wird anschließend mit fetchAll(PDO::FETCH_ASSOC) abgerufen. Dabei werden alle Treffer als assoziatives Array geliefert, bei dem die Spaltennamen als Schlüssel dienen. Mit einer foreach-Schleife werden die einzelnen Ergebniszeilen durchlaufen und mit print_r() lesbar ausgegeben. |
|
Tritt während der Ausführung ein Fehler auf, wird im catch-Block eine Fehlermeldung ausgegeben. Die Methode getMessage() liefert dabei eine genauere Beschreibung des aufgetretenen Problems. |
|
2.5 KundenAbfragenPDO_2.php |
|
2.5.1 Merkmale des Programms |
|
- Verwendete Klasse für Verbindungsaufbau: PDO
- Start über Maskeneingabe (Formular)
- Absicherung der Abfrage durch ein Prepared Statement
- HTML-Formular mit Posting für die Eingabe
- HTML-Ausgabe des Ergebnisses
|
|
Um das Zusammenspiel von PHP mit HTML zu demonstrieren, erweitern wir obiges Programm so, dass es über eine Eingabe per Maske gestartet wird. |
|
So soll das Eingabeformular aussehen: |
|

|
|
Und so das positive Ergebnis, falls 1001 eingegeben wurde: |
|

|
|
2.5.2 Das Programm |
|
|
<?php
|
|
|
|
|
|
/* Mit echo werden - wie oben ja auch schon mehrfach - die
|
|
|
Bestandteile des HTML-Dokuments an den Browser ausgegeben.
|
|
|
Der Browser interpretiert den erzeugten HTML-Code und stellt daraus
|
|
|
die Webseite dar. Er führt den PHP-Code nicht aus. Dieser wird zuvor
|
|
|
auf dem Webserver verarbeitet.
|
|
|
*/
|
|
|
echo "<!doctype html>";
|
|
|
echo "<html lang=\"de\">";
|
|
|
echo "<head>";
|
|
|
echo "<meta charset=\"UTF-8\">";
|
|
|
echo "<title>Kundensuche</title>";
|
|
|
echo "</head>";
|
|
|
echo "<body>";
|
|
|
echo "<h2>Abfrage Kundendaten</h2>";
|
|
|
|
|
|
/*
|
|
|
HTML-Formular ausgeben.
|
|
|
method="post" legt fest, dass die eingegebene Kundennummer
|
|
|
mit der HTTP-Methode POST übertragen wird.
|
|
|
Da kein action-Attribut angegeben ist, werden die Daten
|
|
|
an dieselbe Seite gesendet, d.h. an dieselbe URL.
|
|
|
Zu den Schrägstrichen:
|
|
|
Die PHP-Anweisung echo gibt eine Zeichenkette aus, die hier
|
|
|
HTML-Code enthält. Da sowohl die PHP-Zeichenkette als auch die
|
|
|
HTML-Attribute doppelte Anführungszeichen verwenden, müssen die
|
|
|
inneren Anführungszeichen durch einen vorangestellten umgekehrten
|
|
|
Schrägstrich (backslash) \ geschützt werden. Die Zeichenfolge \"
|
|
|
bedeutet, dass das Anführungszeichen zum Inhalt der Zeichenkette
|
|
|
gehört und diese nicht beendet. Der Backslash wird selbst nicht
|
|
|
ausgegeben.
|
|
|
Der normale Schrägstrich / in </form> oder </label> gehört dagegen
|
|
|
zum HTML-Code und kennzeichnet das Ende eines HTML-Elements.
|
|
|
*/
|
|
|
echo "<form method=\"post\">";
|
|
|
echo "<label for=\"id\">Kundennummer:</label> ";
|
|
|
echo "<input type=\"number\" id=\"id\" name=\"id\" min=\"1\"
|
|
|
required> ";
|
|
|
echo "<input type=\"submit\" value=\"Start\">";
|
|
|
echo "</form>";
|
|
|
|
|
|
/*
|
|
|
Prüfen, ob das Formular mit der HTTP-Methode POST abgesendet wurde.
|
|
|
Beim ersten Aufruf der Seite ist die Bedingung nicht erfüllt. Dann
|
|
|
wird lediglich das Formular angezeigt.
|
|
|
*/
|
|
|
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|
|
|
|
|
/*
|
|
|
Eingabewert aus den POST-Daten übernehmen.
|
|
|
Der Null-Koaleszenz-Operator ?? sorgt dafür, dass bei einem
|
|
|
fehlenden Formularelement ersatzweise die leere Zeichenkette
|
|
|
verwendet wird.
|
|
|
Die explizite Typumwandlung (int) wandelt den Wert anschließend
|
|
|
in eine ganze Zahl um.
|
|
|
*/
|
|
|
$id = (int)($_POST["id"] ?? "");
|
|
|
|
|
|
echo "<pre>";
|
|
|
echo "--- Verbindungsaufbau mit PDO ---\n";
|
|
|
echo "--- KuReAr, Relation kunden ---\n\n";
|
|
|
|
|
|
/*
|
|
|
Die Typumwandlung allein ist noch keine vollständige Eingabeprüfung.
|
|
|
Deshalb wird zusätzlich geprüft, ob die Kundennummer größer
|
|
|
als 0 ist.
|
|
|
*/
|
|
|
if ($id <= 0) {
|
|
|
echo "Bitte geben Sie eine gültige Kundennummer ein.\n";
|
|
|
} else {
|
|
|
try {
|
|
|
|
|
|
/*
|
|
|
Verbindung zur Datenbank aufbauen.
|
|
|
Der DSN enthält den Datenbanktreiber, den Datenbankserver,
|
|
|
die Datenbankbezeichnung und den Zeichensatz.
|
|
|
PDO::ERRMODE_EXCEPTION bewirkt, dass PDO bei Datenbankfehlern
|
|
|
eine PDOException auslöst.
|
|
|
*/
|
|
|
$pdo = new PDO("mysql:host=localhost;" . "dbname=KuReAr;"
|
|
|
. "charset=utf8mb4", "root","",
|
|
|
[PDO::ATTR_ERRMODE =>PDO::ERRMODE_EXCEPTION]
|
|
|
);
|
|
|
|
|
|
// Prepared Statement vorbereiten.
|
|
|
// :id ist ein benannter Platzhalter für die
|
|
|
// gesuchte Kundennummer.
|
|
|
$stmt = $pdo->prepare("SELECT * FROM kunden WHERE KuNr = :id");
|
|
|
|
|
|
// Prepared Statement ausführen.
|
|
|
// Der Wert der Variablen $id wird dem Platzhalter :id zugeordnet.
|
|
|
$stmt->execute(["id" => $id]);
|
|
|
|
|
|
// Alle gefundenen Datensätze als assoziative Arrays abrufen.
|
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
|
|
// Prüfen, ob ein Kunde gefunden wurde.
|
|
|
if (count($rows) === 0) {
|
|
|
echo "Kein Kunde mit der Kundennummer " . $id . " gefunden.\n";
|
|
|
} else {
|
|
|
|
|
|
// Gefundene Datensätze nacheinander
|
|
|
// ausgeben.
|
|
|
foreach ($rows as $row) {
|
|
|
print_r($row);
|
|
|
echo "\n";
|
|
|
}
|
|
|
}
|
|
|
} catch (PDOException $e) {
|
|
|
|
|
|
// Datenbankfehler abfangen und ausgeben.
|
|
|
echo "PDO-Fehler: " . $e->getMessage() . "\n";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// HTML-Element für die vorformatierte Ausgabe schließen.
|
|
|
echo "</pre>";
|
|
|
}
|
|
|
|
|
|
// HTML-Dokument beenden.
|
|
|
echo "</body>";
|
|
|
echo "</html>";
|
|
|
?>
|
|
2.5.3 Anmerkungen |
|
- Der Browser führt die ausgegebenen HTML-Zeilen nicht als "Programmzeilen" aus. PHP wird auf dem Webserver ausgeführt; der Browser empfängt den dabei erzeugten HTML-Code, interpretiert ihn und stellt die Webseite dar.
- Das label-Element wurde mit dem Eingabefeld verbunden. Dazu gehören for="id" beim Label und id="id" beim Eingabefeld.
- min="1" verhindert im Browser die Eingabe einer Kundennummer kleiner als 1. Die serverseitige Prüfung in PHP bleibt trotzdem erforderlich.
- (int) ist eine explizite Typumwandlung, aber allein keine "sichere" oder vollständige Eingabeprüfung. Beispielsweise werden ungültige Zeichenketten zu 0 umgewandelt. Deshalb wird anschließend $id <= 0 geprüft.
- $_POST["id"] wurde durch $_POST["id"] ?? "" abgesichert. Dadurch entsteht keine Warnung, falls der Wert wider Erwarten fehlt.
|
|
3 Eintragen von Daten |
|
In diesem Kapitel wird gezeigt, wie aus PHP-Programmen in Relationale Datenbanken Daten eingetragen werden. Zuerst am Beispiel MySQLi, dann mit PDO. |
|
|
|
3.1 KundenEintragenMySQLi |
|
3.1.1 Das Programm |
|
|
<?php
|
|
|
|
|
|
// Variable für Rückmeldungen an den Benutzer.
|
|
|
// Sie bleibt zunächst leer und wird später mit einer
|
|
|
// Erfolgs- oder Fehlermeldung beschrieben.
|
|
|
$message = "";
|
|
|
|
|
|
// Zugangsdaten zur Datenbank.
|
|
|
$host = "localhost";
|
|
|
$user = "root";
|
|
|
$pass = "";
|
|
|
$db = "KuReAr";
|
|
|
|
|
|
// Prüfen, ob das Formular mit der HTTP-Methode POST abgeschickt
|
|
|
// wurde. Der PHP-Code zum Einfügen wird erst nach dem Absenden des
|
|
|
// Formulars ausgeführt.
|
|
|
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|
|
|
|
|
/*
|
|
|
Eingaben aus dem Formular übernehmen. Der Operator ?? liefert
|
|
|
einen Standardwert, falls kein Wert übertragen wurde. trim()
|
|
|
entfernt Leerzeichen am Anfang und Ende. (int) wandelt den
|
|
|
übernommenen Wert in eine ganze Zahl um.
|
|
|
*/
|
|
|
$kuNr = (int)($_POST["kuNr"] ?? 0);
|
|
|
$name = trim($_POST["name"] ?? "");
|
|
|
$vorname = trim($_POST["vorname"] ?? "");
|
|
|
|
|
|
// Eingaben auf Plausibilität prüfen.
|
|
|
// Die Kundennummer muss größer als 0 sein.
|
|
|
// Name und Vorname dürfen nicht leer sein.
|
|
|
if ($kuNr <= 0 || $name === "" || $vorname === "") {
|
|
|
$message = "Bitte alle Felder korrekt ausfüllen.";
|
|
|
} else {
|
|
|
|
|
|
// Verbindung zur Datenbank aufbauen.
|
|
|
// Die Variable $con verweist auf das erzeugte
|
|
|
// mysqli-Objekt.
|
|
|
$con = new mysqli($host, $user, $pass, $db);
|
|
|
|
|
|
// Prüfen, ob beim Verbindungsaufbau ein Fehler
|
|
|
// aufgetreten ist. die() ("sterben!") gibt die Meldung aus
|
|
|
// und beendet das Programm.
|
|
|
if ($con->connect_error) {
|
|
|
die("DB-Verbindung fehlgeschlagen: " . $con->connect_error);
|
|
|
}
|
|
|
|
|
|
// Zeichensatz für die Datenbankverbindung festlegen.
|
|
|
// utf8mb4 unterstützt Umlaute und andere
|
|
|
// Unicode-Zeichen.
|
|
|
$con->set_charset("utf8mb4");
|
|
|
|
|
|
// SQL-Anweisung vorbereiten.
|
|
|
// Die Fragezeichen dienen als Platzhalter für
|
|
|
// Kundennummer, Name und Vorname.
|
|
|
$stmt = $con->prepare(
|
|
|
"INSERT INTO kunden (KuNr, Name, Vorname)
|
|
|
VALUES (?, ?, ?)"
|
|
|
);
|
|
|
|
|
|
// Prüfen, ob das Prepared Statement erstellt
|
|
|
// werden konnte.
|
|
|
if (!$stmt) {
|
|
|
$message = "Prepare fehlgeschlagen: " . $con->error;
|
|
|
} else {
|
|
|
|
|
|
/*
|
|
|
Werte an die Platzhalter binden.
|
|
|
"i" bezeichnet einen Integer-Wert.
|
|
|
"s" bezeichnet einen String-Wert.
|
|
|
"iss" steht daher für Integer, String, String.
|
|
|
*/
|
|
|
$stmt->bind_param("iss",$kuNr,$name,$vorname);
|
|
|
|
|
|
// Prepared Statement ausführen.
|
|
|
if ($stmt->execute()) {
|
|
|
$message = "Datensatz wurde eingefügt.";
|
|
|
} else {
|
|
|
|
|
|
// Fehlermeldung des Statements übernehmen.
|
|
|
$message = "Fehler beim Einfügen: " . $stmt->error;
|
|
|
}
|
|
|
|
|
|
// Prepared Statement schließen und die dafür
|
|
|
// verwendeten Ressourcen freigeben.
|
|
|
$stmt->close();
|
|
|
}
|
|
|
|
|
|
// Datenbankverbindung schließen und die damit
|
|
|
// verbundenen Ressourcen freigeben.
|
|
|
$con->close();
|
|
|
}
|
|
|
}
|
|
|
?>
|
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>Kunde eintragen mit MySQLi</title>
|
|
|
<style>body {font-family: Arial, sans-serif;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
<h2>Kunde eintragen (MySQLi)</h2>
|
|
|
<!--
|
|
|
Formular zur Eingabe der Kundendaten.
|
|
|
method="post" legt fest, dass die Formulardaten
|
|
|
mit der HTTP-Methode POST übertragen werden.
|
|
|
-->
|
|
|
<form method="post">
|
|
|
<label for="kuNr">Kundennummer:</label>
|
|
|
<input type="number" id="kuNr" name="kuNr" min="1"
|
|
|
required>
|
|
|
<br><br>
|
|
|
|
|
|
<label for="name">Name:</label>
|
|
|
<input type="text" id="name" name="name" maxlength="10"
|
|
|
required>
|
|
|
<br><br>
|
|
|
<label for="vorname">Vorname:</label>
|
|
|
<input type="text" id="vorname" name="vorname"
|
|
|
maxlength="10" required>
|
|
|
<br><br>
|
|
|
|
|
|
<!-- Schaltfläche zum Absenden des Formulars. -->
|
|
|
<button type="submit">Speichern</button>
|
|
|
</form>
|
|
|
|
|
|
<!--
|
|
|
Wenn eine Rückmeldung vorliegt, wird sie in Fettschrift ausgegeben.
|
|
|
Zu htmlspecialchars() vgl. den Anhang.
|
|
|
-->
|
|
|
<?php if ($message !== ""): ?>
|
|
|
<p><strong>
|
|
|
<?php
|
|
|
echo htmlspecialchars($message,ENT_QUOTES,"UTF-8");
|
|
|
?>
|
|
|
</strong>
|
|
|
</p>
|
|
|
<?php endif; ?>
|
|
|
</body>
|
|
|
</html>
|
|
3.1.2 Anmerkungen |
|
- Die Platzhalter werden nicht durch eine gewöhnliche Textersetzung "gefüllt". bind_param() bindet die Werte getrennt an die SQL-Anweisung.
- htmlspecialchars() sorgt hier dafür, dass Zeichen wie <, > und Anführungszeichen nicht als HTML interpretiert werden. Vgl. Anhang.
- Die Attribute for und id ordnen jede Beschriftung eindeutig ihrem Eingabefeld zu.
- min="1" ergänzt die Prüfung der Kundennummer im Browser. Die serverseitige PHP-Prüfung bleibt trotzdem erforderlich.
- Existiert die angegebene Kundennummer bereits, wird das Einfügen bei einem Primärschlüssel beziehungsweise einer eindeutigen Einschränkung abgelehnt und die Datenbankfehlermeldung ausgegeben.
|
|
3.2 RechnungSpeichernMySQLi |
|
3.2.1 Merkmale des Programms |
|
- Speichern in mehreren verknüpften Dateien
- Transaktionen
- Klasse MySQLi
|
|
3.2.2 Aufgabe |
|
Es wird eine Rechnung in die Datenbank KuReAr eingetragen, konkret der Rechnungskopf mit seinen Positionen sowie die Kundennummer. Insgesamt macht das Transaktionen nötig, deren Realisierung damit hier ebenfalls gezeigt wird. Realisiert wird das mit Hilfe des Programmes RechnungSpeichernMySQLi.php, diesmal ohne Eingabe über die Webseite. |
Transaktionen |
Die neue Rechnung hat die Rechnungsnummer 23012, bezieht sich auf den Kunden mit der Kundennummer 1002 und eingekauft werden die Artikel mit den Artikelnummern 100, 101 und 102. Nach dem erfolgten Eintrag soll folgende Abfrage möglich sein: |
|
|
SELECT r.ReNr, r.KuNr, r.reDatum, p.PosNr, p.ArtNr, p.Anzahl, k.Name,
|
|
|
k.Vorname FROM rechkoepfe r, rechpos p, kunden k
|
|
|
where r.kunr=k.kunr and r.renr=p.ReNr and r.kunr=1002
|
|
|
and r.renr=23012;
|
|
|
|
|
| ReNr |
KuNr |
reDatum |
PosNr |
ArtNr |
Anzahl |
Name |
Vorname |
| 23012 |
1002 |
2023-03-28 |
1 |
100 |
2 |
Hinterhof |
Rudolf |
| 23012 |
1002 |
2023-03-28 |
2 |
101 |
1 |
Hinterhof |
Rudolf |
| 23012 |
1002 |
2023-03-28 |
3 |
102 |
5 |
Hinterhof |
Rudolf |
| |
|
|
Anmerkung: Um die Rechnung speichern zu können sollten die oben angeführten Einträge in kunden (Kundenummer 1002) und artikel (Artikelnummern 100, 101 und 102) in der Datenbank vorhanden sein. In rechnung darf die Rechnungsnummer 23012 noch nicht vergeben sein. |
|
3.2.3 Das Programm |
|
|
<?php
|
|
|
|
|
|
// Daten für den Verbindungsaufbau.
|
|
|
$host = "localhost";
|
|
|
$user = "root";
|
|
|
$pass = "";
|
|
|
$db = "KuReAr";
|
|
|
|
|
|
// Daten des Rechnungskopfes.
|
|
|
$reNr = 23012;
|
|
|
$kuNr = 1002;
|
|
|
$reDatum = "2023-03-28";
|
|
|
|
|
|
// Daten der Rechnungspositionen.
|
|
|
// In diesem Beispiel werden die Daten unmittelbar im Programm
|
|
|
// festgelegt. In einer Webanwendung stammen sie üblicherweise
|
|
|
// aus einem Formular.
|
|
|
$positionen = [
|
|
|
["posNr" => 1,"artNr" => 100,"anzahl" => 2],
|
|
|
["posNr" => 2,"artNr" => 101,"anzahl" => 1],
|
|
|
["posNr" => 3,"artNr" => 102,"anzahl" => 5]];
|
|
|
|
|
|
// Die Variable enthält zunächst kein Verbindungsobjekt.
|
|
|
// Nach erfolgreichem Verbindungsaufbau verweist sie
|
|
|
// auf das erzeugte mysqli-Objekt.
|
|
|
$con = null;
|
|
|
try {
|
|
|
|
|
|
// Verbindung zur Datenbank aufbauen.
|
|
|
$con = new mysqli($host,$user,$pass,$db);
|
|
|
|
|
|
// Prüfen, ob beim Verbindungsaufbau ein Fehler
|
|
|
// aufgetreten ist. In diesem Fall wird eine
|
|
|
// Exception ausgelöst.
|
|
|
if ($con->connect_error) {throw new Exception(
|
|
|
"MySQLi-Verbindungsfehler: " . $con->connect_error);
|
|
|
}
|
|
|
|
|
|
// Zeichensatz für die Datenbankverbindung festlegen.
|
|
|
$con->set_charset("utf8mb4");
|
|
|
|
|
|
// Transaktion starten.
|
|
|
// Der Rechnungskopf und alle Rechnungspositionen
|
|
|
// bilden eine gemeinsame Arbeitseinheit. Sie werden
|
|
|
// entweder vollständig oder gar nicht gespeichert.
|
|
|
$con->begin_transaction();
|
|
|
|
|
|
// Prepared Statement zum Einfügen des Rechnungskopfes
|
|
|
// vorbereiten. Die Fragezeichen dienen als Platzhalter.
|
|
|
$stmtKopf = $con->prepare("INSERT INTO rechkoepfe
|
|
|
(ReNr, KuNr, ReDatum) VALUES (?, ?, ?)");
|
|
|
|
|
|
// Prüfen, ob das Prepared Statement erstellt
|
|
|
// werden konnte.
|
|
|
if (!$stmtKopf) {throw new Exception(
|
|
|
"Prepare fehlgeschlagen (Rechnungskopf): "
|
|
|
. $con->error);
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
Werte an die Platzhalter binden.
|
|
|
"i" bezeichnet einen Integer-Wert.
|
|
|
"s" bezeichnet einen String-Wert.
|
|
|
"iis" steht also für Integer, Integer, String.
|
|
|
*/
|
|
|
$stmtKopf->bind_param("iis",$reNr,$kuNr,$reDatum);
|
|
|
|
|
|
// Prepared Statement ausführen.
|
|
|
if (!$stmtKopf->execute()) {throw new Exception(
|
|
|
"Execute fehlgeschlagen (Rechnungskopf): "
|
|
|
. $stmtKopf->error);
|
|
|
}
|
|
|
|
|
|
// Statement für den Rechnungskopf schließen und die
|
|
|
// dafür verwendeten Ressourcen freigeben.
|
|
|
$stmtKopf->close();
|
|
|
|
|
|
// Prepared Statement zum Einfügen der
|
|
|
// Rechnungspositionen vorbereiten.
|
|
|
$stmtPos = $con->prepare("INSERT INTO rechpos
|
|
|
(ReNr, PosNr, ArtNr, Anzahl) VALUES (?, ?, ?, ?)");
|
|
|
|
|
|
// Prüfen, ob das Prepared Statement erstellt
|
|
|
// werden konnte.
|
|
|
if (!$stmtPos) {throw new Exception(
|
|
|
"Prepare fehlgeschlagen (Rechnungspositionen): "
|
|
|
. $con->error);
|
|
|
}
|
|
|
|
|
|
// Variablen für die Werte einer Rechnungsposition.
|
|
|
// Sie werden in jedem Schleifendurchlauf neu beschrieben.
|
|
|
$posNr = 0;
|
|
|
$artNr = 0;
|
|
|
$anzahl = 0;
|
|
|
|
|
|
/*
|
|
|
Variablen an die Platzhalter binden. Alle vier Werte sind ganze
|
|
|
Zahlen. MySQLi bindet hier die Variablen, nicht nur deren
|
|
|
gegenwärtige Werte. Deshalb können sie innerhalb der Schleife
|
|
|
neu beschrieben und das Statement mehrfach ausgeführt werden.
|
|
|
*/
|
|
|
$stmtPos->bind_param("iiii",$reNr,$posNr,$artNr,$anzahl);
|
|
|
|
|
|
// Die Rechnungspositionen nacheinander bearbeiten.
|
|
|
foreach ($positionen as $p) {
|
|
|
|
|
|
// Werte aus dem jeweiligen Array-Element übernehmen
|
|
|
// und in ganze Zahlen umwandeln.
|
|
|
$posNr = (int)$p["posNr"];
|
|
|
$artNr = (int)$p["artNr"];
|
|
|
$anzahl = (int)$p["anzahl"];
|
|
|
|
|
|
// Anzahl auf Plausibilität prüfen.
|
|
|
if ($anzahl <= 0) {throw new Exception(
|
|
|
"Anzahl muss größer als 0 sein " . "(PosNr $posNr).");
|
|
|
}
|
|
|
|
|
|
// Prepared Statement mit den Werten der aktuellen
|
|
|
// Rechnungsposition ausführen.
|
|
|
if (!$stmtPos->execute()) {throw new Exception(
|
|
|
"Execute fehlgeschlagen " . "(PosNr $posNr): "
|
|
|
. $stmtPos->error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Statement für die Rechnungspositionen schließen und
|
|
|
// die dafür verwendeten Ressourcen freigeben.
|
|
|
$stmtPos->close();
|
|
|
|
|
|
/*
|
|
|
Transaktion erfolgreich abschließen.
|
|
|
Erst dadurch werden der Rechnungskopf und alle
|
|
|
Rechnungspositionen dauerhaft gespeichert.
|
|
|
*/
|
|
|
$con->commit();
|
|
|
echo "Rechnung erfolgreich gespeichert.";
|
|
|
} catch (Exception $e) {
|
|
|
|
|
|
// Wenn eine Datenbankverbindung besteht, werden die seit Beginn der
|
|
|
// Transaktion vorgenommenen Änderungen rückgängig gemacht.
|
|
|
if ($con instanceof mysqli) {
|
|
|
$con->rollback();
|
|
|
}
|
|
|
echo "Fehler beim Speichern. " . "Alles wurde zurückgesetzt: "
|
|
|
. $e->getMessage();
|
|
|
} finally {
|
|
|
|
|
|
// Datenbankverbindung schließen und die damit
|
|
|
// verbundenen Ressourcen freigeben.
|
|
|
if ($con instanceof mysqli) {$con->close();
|
|
|
}
|
|
|
}
|
|
|
?>
|
|
Daran denken: Falls Sie das Programm mehrfach erfolgreich ausführen wollen, müssen Sie diese Rechnung (23012) löschen und dabei, wegen der Fremdschlüssel, die Rechnungspositionen zuerst. |
|
3.3 KundenEintragenPDO.php |
|
3.3.1 Merkmale des Programms |
|
- Verwendete Klasse für Verbindungsaufbau: PDO
- Absicherung durch Prepared Statements
- Start durch Browser-Befehl
- HTML-Ausgabe des Ergebnisses
|
|
3.3.2 Das Programm |
|
|
<?php
|
|
|
|
|
|
// Variable für Rückmeldungen an den Benutzer.
|
|
|
// Sie bleibt zunächst leer und wird später mit einer
|
|
|
// Erfolgs- oder Fehlermeldung beschrieben.
|
|
|
$message = "";
|
|
|
|
|
|
/*
|
|
|
Daten für den Verbindungsaufbau. Der DSN (Data Source Name) enthält:
|
|
|
- den verwendeten Datenbanktreiber: mysql
|
|
|
- den Datenbankserver: localhost
|
|
|
- die Bezeichnung der Datenbank: KuReAr
|
|
|
- den Zeichensatz: utf8mb4
|
|
|
*/
|
|
|
$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";
|
|
|
$user = "root";
|
|
|
$pass = "";
|
|
|
|
|
|
// Prüfen, ob das Formular mit der HTTP-Methode POST
|
|
|
// abgeschickt wurde. Der Code zum Einfügen wird erst
|
|
|
// nach dem Absenden des Formulars ausgeführt.
|
|
|
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|
|
|
|
|
/*
|
|
|
Eingaben aus dem Formular übernehmen. Der Operator ?? liefert
|
|
|
einen Standardwert, falls kein entsprechender Wert übertragen wurde.
|
|
|
trim() entfernt Leerzeichen am Anfang und Ende.
|
|
|
(int) wandelt den übernommenen Wert in eine ganze Zahl um.
|
|
|
*/
|
|
|
$kuNr = (int)($_POST["kuNr"] ?? 0);
|
|
|
$name = trim($_POST["name"] ?? "");
|
|
|
$vorname = trim($_POST["vorname"] ?? "");
|
|
|
|
|
|
// Eingaben auf Plausibilität prüfen.
|
|
|
// Die Kundennummer muss größer als 0 sein.
|
|
|
// Name und Vorname dürfen nicht leer sein.
|
|
|
if ($kuNr <= 0 || $name === "" || $vorname === "") {
|
|
|
$message = "Bitte alle Felder korrekt ausfüllen.";
|
|
|
} else {
|
|
|
try {
|
|
|
|
|
|
/*
|
|
|
Verbindung zur Datenbank aufbauen.
|
|
|
PDO steht für PHP Data Objects. Die Variable $pdo verweist auf das
|
|
|
erzeugte PDO-Objekt.
|
|
|
PDO::ATTR_ERRMODE legt fest, wie PDO auf Fehler reagieren soll.
|
|
|
PDO::ERRMODE_EXCEPTION bewirkt, dass bei einem Datenbankfehler eine
|
|
|
Exception ausgelöst wird.
|
|
|
*/
|
|
|
$pdo = new PDO( $dsn, $user, $pass,
|
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]);
|
|
|
|
|
|
// Prepared Statement vorbereiten. :kuNr, :name und :vorname sind
|
|
|
// benannte Platzhalter. Die Eingabewerte werden erst beim
|
|
|
// Ausführen an diese Platzhalter gebunden.
|
|
|
$stmt = $pdo->prepare( "INSERT INTO kunden (KuNr, Name, Vorname)
|
|
|
VALUES (:kuNr, :name, :vorname)"
|
|
|
);
|
|
|
|
|
|
// Prepared Statement ausführen. Das Array ordnet jedem benannten
|
|
|
// Platzhalter den zugehörigen Wert zu. Bei den Array-Schlüsseln
|
|
|
// kann der Doppelpunkt weggelassen werden.
|
|
|
$stmt->execute(["kuNr" => $kuNr,"name" => $name,
|
|
|
"vorname" => $vorname]);
|
|
|
|
|
|
// Diese Zeile wird nur erreicht, wenn execute()
|
|
|
// keine Exception ausgelöst hat.
|
|
|
$message = "Datensatz wurde eingefügt.";
|
|
|
} catch (PDOException $e) {
|
|
|
|
|
|
// PDO-Datenbankfehler abfangen und eine entsprechende Rückmeldung
|
|
|
// erzeugen.
|
|
|
$message = "Fehler beim Einfügen: " . $e->getMessage();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
?>
|
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>Kunde eintragen mit PDO</title>
|
|
|
<style>
|
|
|
body {font-family: Arial, sans-serif;}
|
|
|
</style>
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
<h2>Kunde eintragen (PDO)</h2>
|
|
|
|
|
|
<!--
|
|
|
Formular zur Eingabe der Kundendaten.
|
|
|
method="post" legt fest, dass die Formulardaten mit der HTTP-Methode
|
|
|
POST übertragen werden. Da kein action-Attribut angegeben ist,
|
|
|
werden die Daten an dieselbe PHP-Datei gesendet (d.h. an die, von
|
|
|
der der Aufruf kommt.
|
|
|
-->
|
|
|
<form method="post">
|
|
|
<label for="kuNr">Kundennummer:</label>
|
|
|
<input type="number" id="kuNr" name="kuNr" min="1" required>
|
|
|
<br><br>
|
|
|
<label for="name">Name:</label>
|
|
|
<input type="text" id="name" name="name" maxlength="10" required>
|
|
|
<br><br>
|
|
|
<label for="vorname">Vorname:</label>
|
|
|
<input type="text" id="vorname" name="vorname" maxlength="10"
|
|
|
required>
|
|
|
<br><br>
|
|
|
|
|
|
<!-- Schaltfläche zum Absenden des Formulars. -->
|
|
|
<button type="submit">Speichern</button>
|
|
|
</form>
|
|
|
|
|
|
<!--
|
|
|
Wenn eine Rückmeldung vorliegt, wird sie in
|
|
|
Fettschrift ausgegeben.
|
|
|
Zu htmlspecialchars() vgl. den Anhang.
|
|
|
-->
|
|
|
<?php if ($message !== ""): ?>
|
|
|
<p><strong>
|
|
|
<?php echo htmlspecialchars($message,ENT_QUOTES,"UTF-8"); ?>
|
|
|
</strong>
|
|
|
</p>
|
|
|
<?php endif; ?>
|
|
|
</body>
|
|
|
</html>
|
|
3.3.3 Anmerkungen |
|
- PDO ist eine einheitliche Schnittstelle für den Zugriff auf unterschiedliche Datenbanksysteme. Hier wird durch mysql: im DSN der MySQL-Treiber ausgewählt.
- Im Unterschied zur vorherigen MySQLi-Fassung wird der Zeichensatz bereits im DSN mit charset=utf8mb4 festgelegt.
- PDO verwendet hier benannte Platzhalter wie :kuNr. Dadurch ist unmittelbar erkennbar, welcher Wert an welcher Stelle eingesetzt wird.
- Die Werte werden getrennt von der SQL-Anweisung an execute() übergeben. Das schützt insbesondere vor SQL-Injection.
- Weil PDO::ERRMODE_EXCEPTION aktiviert ist, verzweigt das Programm bei einem Datenbankfehler unmittelbar in den catch-Block.
- Ein ausdrückliches Schließen von Statement und Datenbankverbindung ist bei PDO normalerweise nicht erforderlich. Die zugehörigen Ressourcen werden spätestens am Ende der Skriptausführung freigegeben.
- required, min und maxlength prüfen Eingaben zunächst im Browser. Die Prüfung in PHP bleibt dennoch erforderlich, weil Browserprüfungen umgangen werden können.
- Das Programm benötigt keine Transaktion, weil nur ein einzelner Datensatz mit einer einzigen INSERT-Anweisung gespeichert wird. Eine einzelne SQL-Anweisung wird von der Datenbank bereits als unteilbare Operation behandelt.
|
|
3.4 RechnungSpeichernPDO.php |
|
3.4.1 Merkmale des Programms: |
|
- Speichern verknüpfter Relationen ("Rechnung")
- Verwendete Klasse für Verbindungsaufbau: PDO
- Transaktionen
- Absicherung durch Prepared Statements
- Start durch Browser-Befehl
- HTML-Ausgabe des Ergebnisses
|
|
3.4.2 Aufgabe |
|
Es wird eine Rechnung in die Datenbank KuReAr eingetragen, konkret der Rechnungskopf mit seinen Positionen sowie die Kundennummer. Insgesamt macht das Transaktionen nötig, deren Realisierung damit hier ebenfalls gezeigt wird. Realisiert wird das mit Hilfe des Programmes RechnungSpeichernPDO.php, diesmal ohne Eingabe über die Webseite. |
Transaktionen |
Die neue Rechnung hat die Rechnungsnummer 23002, bezieht sich auf den Kunden mit der Kundennummer 1010 und eingekauft werden die Artikel mit den Artikelnummern 100, 101 und 102. Nach dem erfolgten Eintrag soll folgende Abfrage möglich sein: |
|
|
SELECT r.ReNr, r.KuNr, r.reDatum, p.PosNr, p.ArtNr, p.Anzahl, k.Name,
|
|
|
k.Vorname FROM rechkoepfe r, rechpos p, kunden k
|
|
|
where r.kunr=k.kunr and r.renr=p.ReNr and r.renr=23002;
|
|
|
|
| ReNr |
KuNr |
reDatum |
PosNr |
ArtNr |
Anzahl |
Name |
Vorname |
| 23002 |
1010 |
2026-03-28 |
1 |
100 |
2 |
Anger |
Albert |
| 23002 |
1010 |
2026-03-28 |
2 |
101 |
1 |
Anger |
Albert |
| 23002 |
1010 |
2026-03-28 |
3 |
102 |
5 |
Anger |
Albert |
| |
3.4.3 Das Programm |
|
|
<?php
|
|
|
|
|
|
/*
|
|
|
Das Programm speichert einen Rechnungskopf und die zugehörigen
|
|
|
Rechnungspositionen in einer Transaktion. Dadurch wird
|
|
|
sichergestellt, dass die Rechnung entweder vollständig oder gar
|
|
|
nicht in der Datenbank gespeichert wird.
|
|
|
|
|
|
Daten für den Verbindungsaufbau.
|
|
|
DSN bedeutet Data Source Name. Diese Zeichenkette enthält:
|
|
|
- den verwendeten Datenbanktreiber: mysql
|
|
|
- den Datenbankserver: localhost
|
|
|
- die Bezeichnung der Datenbank: KuReAr
|
|
|
- den Zeichensatz: utf8mb4
|
|
|
*/
|
|
|
$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";
|
|
|
$user = "root";
|
|
|
$pass = "";
|
|
|
|
|
|
/*
|
|
|
Zentrale Daten des neuen Rechnungskopfes.
|
|
|
In einer Webanwendung würden diese Werte üblicherweise aus einem
|
|
|
Formular stammen.
|
|
|
*/
|
|
|
$reNr = 23011;
|
|
|
$kuNr = 1004;
|
|
|
$reDatum = "2026-03-28";
|
|
|
|
|
|
// Daten der Rechnungspositionen.
|
|
|
// Das äußere Array enthält die Rechnungspositionen als Ganzes.
|
|
|
// Jedes innere Array beschreibt eine einzelne Position.
|
|
|
$positionen = [["posNr" => 1,"artNr" => 100,"anzahl" => 2],
|
|
|
["posNr" => 2,"artNr" => 101,"anzahl" => 1],
|
|
|
["posNr" => 3,"artNr" => 102,"anzahl" => 5]];
|
|
|
try {
|
|
|
|
|
|
/*
|
|
|
Verbindung zur Datenbank aufbauen.
|
|
|
PDO steht für PHP Data Objects. Die Variable $pdo verweist auf das
|
|
|
erzeugte PDO-Objekt.
|
|
|
PDO::ERRMODE_EXCEPTION bewirkt, dass PDO bei einem Datenbankfehler
|
|
|
eine Exception auslöst.
|
|
|
*/
|
|
|
$pdo = new PDO($dsn,$user,$pass,
|
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
|
|
|
|
// Transaktion starten. Alle folgenden Datenbankoperationen gehören
|
|
|
// nun zu einer gemeinsamen Arbeitseinheit. Dauerhaft gespeichert
|
|
|
// werden die Änderungen erst durch commit().
|
|
|
$pdo->beginTransaction();
|
|
|
|
|
|
// Prepared Statement für den Rechnungskopf vorbereiten.
|
|
|
// :reNr, :kuNr und :reDatum sind benannte Platzhalter. Sie werden
|
|
|
// beim Aufruf von execute() mit konkreten Werten verbunden.
|
|
|
$stmtKopf = $pdo->prepare(
|
|
|
"INSERT INTO rechkoepfe (ReNr, KuNr, ReDatum)
|
|
|
VALUES (:reNr, :kuNr, :reDatum)");
|
|
|
|
|
|
// Prepared Statement für den Rechnungskopf ausführen.
|
|
|
// Das assoziative Array ordnet jedem benannten Platzhalter den
|
|
|
// zugehörigen Wert zu.
|
|
|
$stmtKopf->execute(["reNr" => $reNr,"kuNr" => $kuNr,
|
|
|
"reDatum" => $reDatum]);
|
|
|
|
|
|
// Prepared Statement für die Rechnungspositionen vorbereiten.
|
|
|
// Das Statement wird nur einmal vorbereitet und danach
|
|
|
// für jede Rechnungsposition erneut ausgeführt.
|
|
|
$stmtPos = $pdo->prepare("INSERT INTO rechpos
|
|
|
(ReNr, PosNr, ArtNr, Anzahl) VALUES
|
|
|
(:reNr, :posNr, :artNr, :anzahl)");
|
|
|
|
|
|
// Rechnungspositionen nacheinander bearbeiten.
|
|
|
foreach ($positionen as $p) {
|
|
|
|
|
|
// Werte der aktuellen Rechnungsposition übernehmen und in ganze
|
|
|
// Zahlen umwandeln.
|
|
|
$posNr = (int)$p["posNr"];
|
|
|
$artNr = (int)$p["artNr"];
|
|
|
$anzahl = (int)$p["anzahl"];
|
|
|
|
|
|
// Einfache Plausibilitätsprüfung.
|
|
|
// Eine Anzahl kleiner oder gleich 0 soll nicht gespeichert werden.
|
|
|
if ($anzahl <= 0) {throw new Exception(
|
|
|
"Anzahl muss größer als 0 sein " . "(PosNr $posNr).");}
|
|
|
|
|
|
// Prepared Statement für die aktuelle Rechnungsposition ausführen.
|
|
|
$stmtPos->execute(["reNr" => $reNr,"posNr" => $posNr,
|
|
|
"artNr" => $artNr,"anzahl" => $anzahl]);
|
|
|
}
|
|
|
|
|
|
// Transaktion erfolgreich abschließen. Erst durch commit() werden
|
|
|
// der Rechnungskopf und sämtliche Rechnungspositionen dauerhaft
|
|
|
// gespeichert.
|
|
|
$pdo->commit();
|
|
|
echo "Rechnung erfolgreich gespeichert.";
|
|
|
} catch (Exception $e) {
|
|
|
|
|
|
// Prüfen, ob das PDO-Objekt vorhanden und noch eine Transaktion
|
|
|
// aktiv ist. Ist dies der Fall, werden mit rollBack() sämtliche
|
|
|
// Änderungen seit beginTransaction() zurückgenommen.
|
|
|
if (isset($pdo) && $pdo->inTransaction()) {
|
|
|
$pdo->rollBack();
|
|
|
}
|
|
|
|
|
|
// Fehlermeldung ausgeben.
|
|
|
echo "Fehler beim Speichern. " . "Alles wurde zurückgesetzt: "
|
|
|
. $e->getMessage();
|
|
|
}
|
|
|
?>
|
|
Daran denken: Ein wiederholter Programmlauf erfordert das Löschen der obigen Rechnung. |
|
3.4.4 Anmerkungen |
|
- Die Positionswerte werden vor ihrer Prüfung und Speicherung ausdrücklich in int umgewandelt.
- Das Prepared Statement für die Positionen wird nur einmal vorbereitet. Innerhalb der Schleife wird es mit jeweils anderen Werten mehrfach ausgeführt.
- commit() speichert alle Änderungen der Transaktion dauerhaft. Bei einem Fehler nimmt rollBack() sowohl den Rechnungskopf als auch bereits eingefügte Rechnungspositionen zurück.
- inTransaction() verhindert, dass rollBack() aufgerufen wird, obwohl keine aktive Transaktion besteht.
- Die Kundennummer 1004 muss bereits in kunden vorhanden sein. Ebenso müssen die Artikelnummern 100, 101 und 102 in der Artikelrelation existieren. Andernfalls verhindert ein Fremdschlüssel das Speichern.
- Auch die Rechnungsnummer 23011 darf noch nicht vorhanden sein, wenn ReNr Primärschlüssel oder eindeutig ist.
|
|
3.4.5 Programmbeschreibung |
|
Das Programm realisiert das Eintragen einer vollständigen Rechnung mit PDO und verwendet dabei eine Transaktion. Das ist notwendig, weil eine Rechnung aus zwei zusammengehörigen Teilen besteht: aus dem Rechnungskopf in der Relation rechkoepfe und aus den Rechnungspositionen in der Relation rechpos. Beide Teile müssen vollständig gespeichert werden. Es darf also nicht vorkommen, dass zwar der Rechnungskopf eingetragen wird, einzelne Positionen aber wegen eines Fehlers fehlen. |
|
Zu Beginn werden die Verbindungsdaten zur Datenbank in der Variablen $dsn sowie in $user und $pass festgelegt. Danach folgen die Daten des neuen Rechnungskopfes, also Rechnungsnummer, Kundennummer und Rechnungsdatum. Die zugehörigen Positionen werden in einem mehrdimensionalen Array gespeichert. Jedes innere Array steht dabei für genau eine Rechnungsposition mit Positionsnummer, Artikelnummer und Anzahl. |
|
Anschließend beginnt ein try/catch-Block. Im try-Teil wird zunächst ein PDO-Objekt erzeugt, das die Verbindung zur Datenbank herstellt. Die Eingabe |
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
|
bewirkt, dass PDO bei Fehlern Exceptions auslöst. Dadurch können Fehler zentral im catch-Teil behandelt werden. |
|
Nach dem Verbindungsaufbau wird mit beginTransaction() eine Transaktion gestartet. Ab diesem Zeitpunkt gehören alle folgenden Änderungen zu einer gemeinsamen Arbeitseinheit. Dauerhaft in der Datenbank gespeichert werden sie erst dann, wenn am Ende commit() ausgeführt wird. |
|
Danach wird ein prepared statement für den Rechnungskopf vorbereitet. In der SQL-Anweisung stehen mit :reNr, :kuNr und :redatum sogenannte named parameter. Das sind Platzhalter, die erst beim Aufruf von execute() mit konkreten Werten belegt werden. Auf diese Weise werden SQL-Anweisung und Daten voneinander getrennt. |
|
Im nächsten Schritt wird ein zweites prepared statement für die Rechnungspositionen vorbereitet. Dieses Statement wird nur einmal erzeugt und anschließend in der Schleife für jede Position erneut verwendet. Die foreach-Schleife durchläuft nacheinander alle Positionen des Arrays $positionen. Vor dem Eintragen wird hier noch ein einfacher Plausibilitätscheck durchgeführt: Die Anzahl muss größer als 0 sein. Ist das nicht der Fall, wird mit throw new Exception(...) eine Exception ausgelöst. Dadurch wird der normale Programmablauf sofort unterbrochen, und die Verarbeitung springt in den catch-Block. |
|
Wenn dagegen alle Positionen korrekt eingetragen werden konnten, wird am Ende mit commit() die gesamte Transaktion dauerhaft gespeichert. Erst in diesem Moment werden also Rechnungskopf und Rechnungspositionen verbindlich in die Datenbank übernommen. |
|
Tritt irgendwo im try-Block ein Fehler auf, so wird der catch-Block ausgeführt. Dort wird mit rollBack() die laufende Transaktion vollständig zurückgenommen. Damit werden alle bis dahin vorgenommenen Änderungen rückgängig gemacht. Genau das ist der entscheidende Vorteil der Transaktion: Die Rechnung wird entweder vollständig gespeichert oder gar nicht. Auf diese Weise bleiben die Daten in der Datenbank konsistent. |
|
4 Löschen von Daten |
|
Hier wird jeweils zuerst das Löschen in einer einzigen Relation (Kunden) und dann das Löschen in mehreren zusammenhängenden (Rechnung) gezeigt. Zuerst mit MySQLi, dann mit PDO. |
|
|
|
4.1 KundenLoeschenMySQLi.php |
|
4.1.1 Merkmale des Programms: |
|
- Verwendete Klasse für Verbindungsaufbau: mysqli
- Löschen in einer Relation (Kunden)
- Absicherung durch Prepared Statements
- Start durch Browser-Befehl
- HTML-Ausgabe des Ergebnisses
|
|
Zum Nachvollziehen: Zuerst einen Kunden anlegen ohne Fremdschlüsselbeziehungen zu Rechnungsköpfen, z.B. mit dem Programm KundenEintragenMySQLi.php. Der kann dann mit diesem Programm gelöscht werden. |
|
4.1.2 Das Programm |
|
|
<?php
|
|
|
|
|
|
// Variable für Rückmeldungen an den Benutzer.
|
|
|
// Sie speichert Erfolgs- oder Fehlermeldungen,
|
|
|
// die später im HTML-Abschnitt ausgegeben werden.
|
|
|
$message = "";
|
|
|
|
|
|
// Daten für den Verbindungsaufbau.
|
|
|
$host = "localhost";
|
|
|
$user = "root";
|
|
|
$pass = "";
|
|
|
$db = "KuReAr";
|
|
|
|
|
|
// Prüfen, ob das Formular mit der HTTP-Methode POST
|
|
|
// abgeschickt wurde.
|
|
|
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|
|
|
|
|
// Kundennummer aus dem Formular übernehmen.
|
|
|
// Der Operator ?? liefert 0, wenn der Formulareintrag fehlt.
|
|
|
// (int) wandelt den übernommenen Wert in eine ganze Zahl um.
|
|
|
$kuNr = (int)($_POST["kuNr"] ?? 0);
|
|
|
|
|
|
// Eingabe prüfen. Die Kundennummer muss größer als 0 sein.
|
|
|
if ($kuNr <= 0) {
|
|
|
$message = "Bitte eine gültige Kundennummer eingeben.";
|
|
|
} else {
|
|
|
|
|
|
// Verbindung zur Datenbank aufbauen.
|
|
|
// Die Variable $con verweist auf das erzeugte mysqli-Objekt.
|
|
|
$con = new mysqli($host, $user, $pass, $db);
|
|
|
|
|
|
// Prüfen, ob beim Verbindungsaufbau ein Fehler aufgetreten ist.
|
|
|
// die() gibt die Meldung aus und beendet das Programm.
|
|
|
if ($con->connect_error) {die("DB-Verbindung fehlgeschlagen: "
|
|
|
. $con->connect_error
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// Zeichensatz für die Datenbankverbindung festlegen,
|
|
|
// damit Sonderzeichen korrekt verarbeitet werden.
|
|
|
$con->set_charset("utf8mb4");
|
|
|
|
|
|
// SQL-Anweisung vorbereiten.
|
|
|
// Das Fragezeichen dient als Platzhalter für die Kundennummer.
|
|
|
$stmt = $con->prepare("DELETE FROM kunden WHERE KuNr = ?");
|
|
|
|
|
|
// Prüfen, ob das Prepared Statement erstellt werden konnte.
|
|
|
if (!$stmt) {
|
|
|
$message = "Prepare fehlgeschlagen: " . $con->error;
|
|
|
} else {
|
|
|
|
|
|
// Kundennummer an den Platzhalter binden.
|
|
|
// "i" bezeichnet den Datentyp Integer.
|
|
|
$stmt->bind_param("i", $kuNr);
|
|
|
|
|
|
// Prepared Statement ausführen.
|
|
|
if ($stmt->execute()) {
|
|
|
|
|
|
// affected_rows enthält die Anzahl der Datensätze, die durch die
|
|
|
// DELETE-Anweisung gelöscht wurden.
|
|
|
if ($stmt->affected_rows > 0) {
|
|
|
$message = "Datensatz wurde gelöscht.";
|
|
|
} else {$message = "Kein Datensatz mit dieser "
|
|
|
. "Kundennummer gefunden.";
|
|
|
}
|
|
|
} else {$message = "Fehler beim Löschen: "
|
|
|
. $stmt->error;
|
|
|
}
|
|
|
|
|
|
// Prepared Statement schließen und die dafür verwendeten Ressourcen
|
|
|
// freigeben.
|
|
|
$stmt->close();
|
|
|
}
|
|
|
|
|
|
// Datenbankverbindung schließen und die damit verbundenen
|
|
|
// Ressourcen freigeben.
|
|
|
$con->close();
|
|
|
}
|
|
|
}
|
|
|
?>
|
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>Kunde löschen mit MySQLi</title>
|
|
|
<style>
|
|
|
body {font-family: Arial, sans-serif;}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<h2>Kunde löschen (MySQLi)</h2>
|
|
|
|
|
|
<!--
|
|
|
Formular zur Eingabe der Kundennummer.
|
|
|
method="post" legt fest, dass die Formulardaten mit der
|
|
|
HTTP-Methode POST übertragen werden. Da kein action-Attribut
|
|
|
angegeben ist, werden sie an die sendende PHP-Datei gesendet.
|
|
|
-->
|
|
|
<form method="post">
|
|
|
<label for="kuNr">Kundennummer:</label>
|
|
|
<input type="number" id="kuNr" name="kuNr" min="1" required>
|
|
|
<button type="submit">Löschen</button>
|
|
|
</form>
|
|
|
|
|
|
<!--Zu htmlspecialchars() vgl. den Anhang.
|
|
|
-->
|
|
|
<?php if ($message !== ""): ?>
|
|
|
<p><strong>
|
|
|
<?php echo htmlspecialchars($message,ENT_QUOTES,"UTF-8");?>
|
|
|
</strong>
|
|
|
</p>
|
|
|
<?php endif; ?>
|
|
|
</body>
|
|
|
</html>
|
|
4.1.3 Programmlauf |
|
Wichtig: Geben Sie einen Kunden an, der nicht durch Schlüssel-/Fremdschlüsselbeziehungen eingebunden ist. Also: Mit obigem Programm KundenEintragen einen "isolierten" Kunden eintragen und diesen dann hier löschen. |
|
So sieht die Eingabemaske aus. |
|

|
|
4.2 RechnungLoeschenMySQLi.php |
|
4.2.1 Merkmale des Programms |
|
- Verwendete Klasse für Verbindungsaufbau: mysqli
- Löschen in mehreren relational verknüpften Dateien der Datenbank KuReAr
- Durchführung einer Transaktion
- Absicherung durch ein Prepared Statement
- Start durch ein Formular
- HTML-Ausgabe des Ergebnisses
|
|
4.2.2 Aufgabe |
|
Hier wird eine Rechnung in der Datenbank KuReAr gelöscht, d.h., der Rechnungskopf mit seinen Positionen. Insgesamt macht das Transaktionen nötig, deren Realisierung damit hier ebenfalls gezeigt wird. Realisiert wird das mit Hilfe des Programmes RechnungLoeschenMySQLi.php. Das Programm soll über die Konsole gestartet werden. |
Transaktionen |
Das folgende Beispiel wird unten verwendet, eine Rechnung des (natürlich) fiktiven Kunden Rudolf Hinterhof mit zwei Positionen. |
|
|
SELECT r.ReNr, r.KuNr, r.Datum, p.PosNr, p.ArtNr, p.Anzahl, k.Name,
|
|
|
k.Vorname FROM rechkoepfe r, rechpos p, kunden k
|
|
|
where r.kunr=k.kunr and r.renr=p.ReNr and r.renr=23001;
|
|
| ReNr |
KuNr |
Datum |
PosNr |
ArtNr |
Anzahl |
Name |
Vorname |
| 23001 |
1002 |
2026-03-27 |
1 |
101 |
2 |
Hinterhof |
Rudolf |
| 23001 |
1002 |
2026-03-27 |
2 |
554 |
1 |
Hinterhof |
Rudolf |
| |
|
|
Die Webseite für die Dateneingabe soll wie folgt sein: |
|

|
|
4.2.3 Das Programm |
|
|
<?php
|
|
|
|
|
|
// Daten für den Verbindungsaufbau. Oben schon mehrfach beschrieben
|
|
|
$host = "localhost";
|
|
|
$user = "root";
|
|
|
$pass = "";
|
|
|
$db = "KuReAr";
|
|
|
|
|
|
// Variable für Rückmeldungen an den Benutzer.
|
|
|
$message = "";
|
|
|
|
|
|
// Verbindung zur Datenbank aufbauen.
|
|
|
// Die Variable $con verweist auf das erzeugte mysqli-Objekt.
|
|
|
$con = new mysqli($host, $user, $pass, $db);
|
|
|
|
|
|
// Prüfen, ob beim Verbindungsaufbau ein Fehler aufgetreten ist.
|
|
|
// die() gibt die Meldung aus und beendet das Programm.
|
|
|
if ($con->connect_error) {
|
|
|
die("DB-Verbindung fehlgeschlagen: " . $con->connect_error);
|
|
|
}
|
|
|
|
|
|
// Zeichensatz für die Datenbankverbindung festlegen.
|
|
|
$con->set_charset("utf8mb4");
|
|
|
|
|
|
// Prüfen, ob das Formular mit der HTTP-Methode POST
|
|
|
// abgeschickt wurde.
|
|
|
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|
|
|
|
|
// Rechnungsnummer aus dem Formular übernehmen.
|
|
|
// Der Operator ?? liefert 0, wenn der Formulareintrag fehlt.
|
|
|
// (int) wandelt den übernommenen Wert in eine ganze Zahl um.
|
|
|
$reNr = (int)($_POST["reNr"] ?? 0);
|
|
|
|
|
|
// Eingabe prüfen. Die Rechnungsnummer muss größer als 0 sein.
|
|
|
if ($reNr <= 0) {
|
|
|
$message = "Bitte eine gültige Rechnungsnummer eingeben.";
|
|
|
} else {
|
|
|
|
|
|
// try/catch-Block zum Abfangen möglicher Fehler.
|
|
|
try {
|
|
|
|
|
|
// Transaktion starten. $con ist das MySQLi-Verbindungsobjekt.
|
|
|
// Alle folgenden Löschoperationen werden dadurch zu
|
|
|
// einer gemeinsamen Einheit zusammengefasst.
|
|
|
$con->begin_transaction();
|
|
|
|
|
|
/*
|
|
|
Prüfen, ob ein Rechnungskopf mit der eingegebenen Rechnungsnummer
|
|
|
existiert.
|
|
|
$stmtCheck verweist auf ein Statement-Objekt der Klasse mysqli_stmt.
|
|
|
Die Methode prepare() erzeugt ein Prepared Statement. Das
|
|
|
Fragezeichen dient als Platzhalter für die Rechnungsnummer.
|
|
|
*/
|
|
|
$stmtCheck = $con->prepare("SELECT 1 FROM rechkoepfe
|
|
|
WHERE ReNr = ?");
|
|
|
|
|
|
// Prüfen, ob das Prepared Statement erstellt wurde.
|
|
|
if (!$stmtCheck) {throw new Exception(
|
|
|
"Prepare fehlgeschlagen (Prüfung): " . $con->error);
|
|
|
}
|
|
|
|
|
|
// Rechnungsnummer an den Platzhalter binden.
|
|
|
// "i" bezeichnet den Datentyp Integer.
|
|
|
$stmtCheck->bind_param("i", $reNr);
|
|
|
|
|
|
// Prepared Statement ausführen.
|
|
|
if (!$stmtCheck->execute()) {
|
|
|
throw new Exception("Execute fehlgeschlagen (Prüfung): "
|
|
|
. $stmtCheck->error
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// Ergebnismenge zwischenspeichern, damit anschließend
|
|
|
// die Anzahl der gefundenen Zeilen ermittelt werden kann.
|
|
|
$stmtCheck->store_result();
|
|
|
|
|
|
// Prüfen, ob ein Rechnungskopf gefunden wurde.
|
|
|
if ($stmtCheck->num_rows === 0) {$stmtCheck->close();
|
|
|
|
|
|
// Es gibt nichts zu löschen. Die Transaktion wird
|
|
|
// ohne dauerhafte Änderungen beendet.
|
|
|
$con->rollback();
|
|
|
$message = "Keine Rechnung mit ReNr = $reNr gefunden.";
|
|
|
} else {
|
|
|
$stmtCheck->close();
|
|
|
|
|
|
/*
|
|
|
1. Die zur Rechnung gehörenden Positionen löschen.
|
|
|
Die Positionen müssen vor dem Rechnungskopf gelöscht werden, falls
|
|
|
der Fremdschlüssel nicht mit ON DELETE CASCADE definiert wurde.
|
|
|
*/
|
|
|
$stmtPosDel = $con->prepare("DELETE FROM rechpos
|
|
|
WHERE ReNr = ?");
|
|
|
if (!$stmtPosDel) {throw new Exception("Prepare fehlgeschlagen "
|
|
|
. "(Positionen löschen): " . $con->error);
|
|
|
}
|
|
|
$stmtPosDel->bind_param("i", $reNr);
|
|
|
if (!$stmtPosDel->execute()) {throw new Exception(
|
|
|
"Execute fehlgeschlagen " . "(Positionen löschen): "
|
|
|
. $stmtPosDel->error);
|
|
|
}
|
|
|
|
|
|
// Anzahl der gelöschten Rechnungspositionen ermitteln.
|
|
|
$deletedPos = $stmtPosDel->affected_rows;
|
|
|
$stmtPosDel->close();
|
|
|
|
|
|
// 2. Den Rechnungskopf löschen.
|
|
|
$stmtKopfDel = $con->prepare("DELETE FROM rechkoepfe
|
|
|
WHERE ReNr = ?");
|
|
|
if (!$stmtKopfDel) {throw new Exception("Prepare fehlgeschlagen "
|
|
|
. "(Rechnungskopf löschen): " . $con->error);
|
|
|
}
|
|
|
$stmtKopfDel->bind_param("i", $reNr);
|
|
|
if (!$stmtKopfDel->execute()) {throw new Exception(
|
|
|
"Execute fehlgeschlagen " . "(Rechnungskopf löschen): "
|
|
|
. $stmtKopfDel->error);
|
|
|
}
|
|
|
|
|
|
// Anzahl der gelöschten Rechnungsköpfe ermitteln. Bei einer
|
|
|
// eindeutigen Rechnungsnummer sollte der Wert 1 sein.
|
|
|
$deletedKopf = $stmtKopfDel->affected_rows;
|
|
|
$stmtKopfDel->close();
|
|
|
|
|
|
// Transaktion erfolgreich abschließen. Erst dadurch werden die
|
|
|
// Löschungen dauerhaft in der Datenbank gespeichert.
|
|
|
$con->commit();
|
|
|
$message = "Rechnung ReNr = $reNr gelöscht "
|
|
|
. "(Kopf: $deletedKopf, " . "Positionen: $deletedPos).";
|
|
|
}
|
|
|
} catch (Exception $e) {
|
|
|
|
|
|
// Im Fehlerfall alle seit Beginn der Transaktion vorgenommenen
|
|
|
// Änderungen rückgängig machen.
|
|
|
$con->rollback();
|
|
|
$message = "Fehler beim Löschen. " . "Alles wurde zurückgesetzt: "
|
|
|
. $e->getMessage();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
?>
|
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>KuReAr - Rechnung löschen</title>
|
|
|
<style>body {font-family: Arial, sans-serif;}</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<h2>Rechnung löschen (MySQLi)</h2>
|
|
|
|
|
|
<!--
|
|
|
Formular zur Eingabe der Rechnungsnummer.
|
|
|
Da kein action-Attribut angegeben ist, werden die Daten
|
|
|
an die sendende PHP-Datei gesendet.
|
|
|
-->
|
|
|
<form method="post">
|
|
|
<label for="reNr">Rechnungsnummer (ReNr):</label>
|
|
|
<input type="number" id="reNr" name="reNr" min="1"
|
|
|
required>
|
|
|
<button type="submit">Löschen</button>
|
|
|
</form>
|
|
|
|
|
|
<!--
|
|
|
Wenn eine Rückmeldung vorliegt, wird sie in Fettschrift
|
|
|
ausgegeben. ZU htmlspecialchars() vgl. den Anhang.
|
|
|
-->
|
|
|
<?php if ($message !== ""): ?>
|
|
|
<p><strong>
|
|
|
<?php echo htmlspecialchars($message,ENT_QUOTES,"UTF-8"); ?>
|
|
|
</strong>
|
|
|
</p>
|
|
|
<?php endif; ?>
|
|
|
</body>
|
|
|
</html>
|
|
|
<?php
|
|
|
|
|
|
// Datenbankverbindung schließen und die damit verbundenen
|
|
|
// Ressourcen freigeben.
|
|
|
$con->close();
|
|
|
?>
|
|
4.2.4 Erläuterung des Programms |
|
Das Programm realisiert das Löschen einer Rechnung mit MySQLi und verwendet dabei eine Transaktion, um die Konsistenz der Datenbank sicherzustellen. Eine Rechnung besteht in der Datenbank KuReAr aus zwei zusammengehörigen Teilen: dem Rechnungskopf in der Relation rechkoepfe und den zugehörigen Rechnungspositionen in der Relation rechpos. Aufgrund dieser Abhängigkeit dürfen beide Teile nicht unabhängig voneinander gelöscht werden. |
|
Zu Beginn werden die Verbindungsdaten zur Datenbank festgelegt. Anschließend wird mit new mysqli(...) eine Datenbankverbindung aufgebaut. Tritt dabei ein Fehler auf, wird das Programm mit die() sofort beendet und eine Fehlermeldung ausgegeben. Danach wird der Zeichensatz auf utf8mb4 gesetzt, um eine korrekte Verarbeitung von Sonderzeichen zu gewährleisten. |
|
Wenn das Formular abgeschickt wird, liest das Programm die eingegebene Rechnungsnummer und prüft, ob es sich um einen gültigen Wert handelt. Nur positive Zahlen werden akzeptiert. Ist die Eingabe ungültig, wird eine entsprechende Meldung ausgegeben. |
|
Bei gültiger Eingabe beginnt ein try/catch-Block. Zunächst wird mit begin_transaction() eine Transaktion gestartet. Alle folgenden Datenbankoperationen gehören damit zu einer gemeinsamen logischen Einheit, die entweder vollständig ausgeführt oder komplett zurückgenommen wird. |
|
Im nächsten Schritt wird überprüft, ob der angegebene Rechnungskopf überhaupt existiert. Dazu wird ein Prepared Statement mit einem Platzhalter (?) verwendet. Mit store_result() kann anschließend festgestellt werden, ob eine passende Zeile gefunden wurde. Existiert keine Rechnung, wird die Transaktion mit rollback() beendet (ohne Änderungen) und eine entsprechende Meldung ausgegeben. |
|
Existiert die Rechnung, erfolgt das Löschen in zwei Schritten. Zunächst werden alle zugehörigen Einträge in rechpos gelöscht. Dieser Schritt ist zwingend erforderlich, da aufgrund der Fremdschlüsselbeziehung ein Rechnungskopf nicht gelöscht werden darf, solange noch abhängige Positionen vorhanden sind. Die Anzahl der gelöschten Datensätze wird über die Eigenschaft affected_rows ermittelt. |
|
Anschließend wird der Rechnungskopf selbst aus der Relation rechkoepfe gelöscht. Auch hier wird die Anzahl der betroffenen Zeilen gespeichert. Sind beide Löschoperationen erfolgreich, wird die Transaktion mit commit() abgeschlossen. Erst zu diesem Zeitpunkt werden die Änderungen dauerhaft in der Datenbank gespeichert. |
|
Tritt hingegen während der Ausführung ein Fehler auf, wird eine Exception ausgelöst. Die Programmausführung springt dann in den catch-Block, in dem mit rollback() alle bisherigen Änderungen vollständig zurückgenommen werden. Dadurch wird sichergestellt, dass keine unvollständigen Löschvorgänge entstehen. Die Rechnung wird also entweder vollständig (inklusive aller Positionen) gelöscht oder überhaupt nicht. Auf diese Weise bleibt die Datenbank stets in einem konsistenten Zustand. |
|
4.2.5 Anmerkung zur Programmierung |
|
- $message enthält keine Benutzereingaben, sondern Rückmeldungen an den Benutzer.
- die() bedeutet hier nicht einfach "sterben", also "beenden", sondern: Meldung ausgeben und die Ausführung des Programms beenden.
- Die bislang ungeprüfte Ausführung von $stmtCheck->execute() wird jetzt ebenfalls kontrolliert.
|
|
Die HTML-Anweisung <input type="number" id="reNr" name="reNr" min="1" required> erzeugt in einem Formular ein Eingabefeld für eine Zahl, hier für eine Rechnungsnummer. Die einzelnen Bestandteile bedeuten: |
|
- <input ...> Erzeugt ein Eingabefeld in einem HTML-Formular.
- type="number" legt fest, dass in das Feld eine Zahl eingegeben werden soll. Der Browser stellt dafür häufig zusätzlich kleine Pfeile zum Erhöhen und Verringern des Wertes bereit.
- id="reNr" gibt dem Eingabefeld die eindeutige Kennung reNr. Diese Kennung wird beispielsweise benötigt, um das Feld mit einem <label> zu verbinden oder über CSS bzw. JavaScript anzusprechen.
- name="reNr" legt den Namen fest, unter dem der eingegebene Wert beim Absenden des Formulars an den Server übertragen wird. Bei einem Formular mit method="post" kann PHP beispielsweise mit $_POST["reNr"] auf den eingegebenen Wert zugreifen.
- min="1" legt den kleinsten zulässigen Wert fest. Es dürfen also nur Zahlen ab 1 eingegeben werden.
- required kennzeichnet das Feld als Pflichtfeld. Der Browser lässt das Formular normalerweise nicht absenden, solange keine gültige Eingabe vorhanden ist.
|
|
Insgesamt also |
|
Die HTML-Anweisung erzeugt ein Eingabefeld für eine Zahl. Das Feld erhält die Kennung und den Namen reNr. Der eingegebene Wert muss mindestens 1 betragen. Da das Attribut required angegeben ist, handelt es sich um ein Pflichtfeld. Beim Absenden des Formulars wird der eingegebene Wert unter dem Namen reNr an den Server übertragen. |
|
Wichtig dabei ist dabei der Unterschied zwischen id und name: id identifiziert das Element innerhalb der Webseite. name bestimmt dagegen insbesondere, unter welchem Namen der eingegebene Wert beim Absenden übertragen wird. |
|
4.3 KundenLoeschenPDO.php |
|
Ein Kundeneintrag wird gelöscht. |
|
4.3.1 Merkmale des Programms |
|
- Verwendete Klasse für den Verbindungsaufbau: PDO
- Start durch Browser-Befehl
- HTML-Ausgabe des Ergebnisses
|
|
4.3.2 Das Programm |
|
|
<?php
|
|
|
|
|
|
// Variable für Erfolgs- und Fehlermeldungen.
|
|
|
$message = "";
|
|
|
|
|
|
// Verbindungsdaten für PDO.
|
|
|
// DSN bedeutet "Data Source Name". Der DSN enthält hier den Host,
|
|
|
// die Bezeichnung der Datenbank und den verwendeten Zeichensatz.
|
|
|
$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";
|
|
|
$user = "root";
|
|
|
$pass = "";
|
|
|
|
|
|
// Prüfen, ob das Formular mit der HTTP-Methode POST abgeschickt wurde.
|
|
|
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|
|
|
|
|
// Eingabewert aus dem Formular übernehmen.
|
|
|
// Der Operator ?? liefert 0, wenn der Formulareintrag nicht vorhanden ist.
|
|
|
// (int) wandelt den übernommenen Wert in eine ganze Zahl um.
|
|
|
$kuNr = (int)($_POST["kuNr"] ?? 0);
|
|
|
|
|
|
// Eingabe prüfen. Die Kundennummer muss größer als 0 sein.
|
|
|
if ($kuNr <= 0) {
|
|
|
$message = "Bitte eine gültige Kundennummer eingeben.";
|
|
|
} else {
|
|
|
|
|
|
// try/catch-Block zum Abfangen möglicher Datenbankfehler.
|
|
|
try {
|
|
|
|
|
|
// Datenbankverbindung mit PDO aufbauen. Der Fehlermodus bewirkt,
|
|
|
// dass PDO bei einem Fehler // eine Exception auslöst.
|
|
|
$pdo = new PDO($dsn,$user,$pass,
|
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
|
|
|
|
// SQL-Anweisung als Prepared Statement vorbereiten.
|
|
|
// :kuNr ist ein benannter Platzhalter.
|
|
|
$stmt = $pdo->prepare("DELETE FROM kunden WHERE KuNr = :kuNr");
|
|
|
|
|
|
// Den Wert der PHP-Variablen $kuNr an den Platzhalter
|
|
|
// binden und die vorbereitete SQL-Anweisung ausführen.
|
|
|
$stmt->execute(["kuNr" => $kuNr]);
|
|
|
|
|
|
// Prüfen, ob tatsächlich ein Datensatz gelöscht wurde.
|
|
|
// rowCount() liefert hier die Anzahl der gelöschten Datensätze.
|
|
|
if ($stmt->rowCount() > 0) {$message = "Datensatz wurde gelöscht.";
|
|
|
} else {
|
|
|
|
|
|
// Wenn kein Datensatz gelöscht wurde, existierte kein Kunde
|
|
|
// mit der eingegebenen Kundennummer.
|
|
|
$message = "Kein Datensatz mit dieser Kundennummer gefunden.";
|
|
|
}
|
|
|
} catch (PDOException $e) {
|
|
|
|
|
|
// Fehlerbehandlung: Tritt beim Verbindungsaufbau oder bei der
|
|
|
// Ausführung der SQL-Anweisung ein Fehler auf, wird hier eine
|
|
|
// Fehlermeldung erzeugt.
|
|
|
$message = "Fehler: " . $e->getMessage();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
?>
|
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>Kunde löschen mit PDO</title>
|
|
|
<style>body {font-family: Arial, sans-serif;}</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<h2>Kunde löschen (PDO)</h2>
|
|
|
|
|
|
<!--
|
|
|
Formular zur Eingabe der Kundennummer.
|
|
|
method="post" legt fest, dass die Formulardaten mit der
|
|
|
HTTP-Methode POST übertragen werden. Da kein action-Attribut
|
|
|
angegeben ist, werden sie an sendende PHP-Datei gesendet.
|
|
|
-->
|
|
|
<form method="post">
|
|
|
<label for="kuNr">Kundennummer:</label>
|
|
|
<input type="number" id="kuNr" name="kuNr" min="1" required>
|
|
|
|
|
|
<!-- Schaltfläche zum Absenden des Formulars. -->
|
|
|
<button type="submit">Löschen</button>
|
|
|
</form>
|
|
|
|
|
|
<!--
|
|
|
Wenn eine Meldung vorliegt, wird sie in Fettschrift ausgegeben.
|
|
|
htmlspecialchars() verhindert, dass enthaltene Sonderzeichen als
|
|
|
Bestandteile des HTML-Codes interpretiert werden. Vgl. auch den
|
|
|
Anhang.
|
|
|
-->
|
|
|
<?php if ($message !== ""): ?>
|
|
|
<p><strong>
|
|
|
<?php echo htmlspecialchars($message,ENT_QUOTES,"UTF-8"); ?>
|
|
|
</strong>
|
|
|
</p>
|
|
|
<?php endif; ?>
|
|
|
</body>
|
|
|
</html>
|
|
4.3.3 Programmlauf |
|
Auch hier gilt: Zuerst einen isolierten Kunden (d.h. einen ohne Rechnung) eingeben. Dieser kann dann problemlos gelöscht werden. |
|
Die Konsoleneingabe |
|

|
|
4.3.4 Erläuterung des Programms |
|
Das Programm zeigt, wie ein Kundendatensatz aus der Datenbank KuReAr mithilfe der PDO-Erweiterung (PHP Data Objects) gelöscht werden kann. Zu Beginn wird eine Variable $message definiert. Sie dient dazu, Statusmeldungen zu speichern, die den Benutzer darüber informieren, ob die Operation erfolgreich war oder ob ein Fehler aufgetreten ist. Anschließend werden die Verbindungsparameter in Form eines DSN (Data Source Name) sowie eines Benutzernamens und Passworts festgelegt. Der DSN enthält alle notwendigen Informationen zum Aufbau der Datenbankverbindung. |
PHP Data Objects |
Nach dem Absenden des Formulars prüft das Programm, ob die Anfrage mit der Methode POST erfolgt ist. Dadurch wird sichergestellt, dass die Daten aus dem Formular stammen und nicht durch einen direkten Aufruf der URL übergeben wurden. Die eingegebene Kundennummer wird anschließend ausgelesen und in eine Ganzzahl umgewandelt. Falls kein Wert übergeben wird, wird standardmäßig der Wert 0 verwendet. |
|
Im nächsten Schritt erfolgt eine Validierung der Eingabe. Es werden nur positive Werte als gültige Kundennummern akzeptiert. Ist die Eingabe ungültig, wird eine entsprechende Fehlermeldung gespeichert und keine Datenbankoperation durchgeführt. |
|
Ist die Eingabe gültig, wird ein try/catch-Block ausgeführt. Innerhalb des try-Abschnitts wird ein PDO-Objekt erzeugt, um die Verbindung zur Datenbank herzustellen. Die Option |
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
|
bewirkt, dass auftretende Datenbankfehler als Exceptions gemeldet werden, die zentral behandelt werden können. |
|
Das Löschen des Datensatzes erfolgt über ein Prepared Statement. Die SQL-Anweisung enthält einen benannten Parameter (:customer_id), der als Platzhalter dient. Dieser Platzhalter wird erst bei der Ausführung der Anweisung durch den tatsächlichen Wert ersetzt. Dieses Vorgehen erhöht sowohl die Sicherheit (insbesondere Schutz vor SQL-Injection) als auch die Übersichtlichkeit des Codes. |
|
Nach der Ausführung wird mit der Methode rowCount() ermittelt, wie viele Datensätze betroffen sind. Wurde mindestens ein Datensatz gelöscht, wird eine Erfolgsmeldung ausgegeben. Andernfalls informiert das Programm darüber, dass kein entsprechender Datensatz gefunden wurde. |
|
Tritt während der Ausführung ein Fehler auf, wird eine PDOException ausgelöst und im catch-Block abgefangen. Die entsprechende Fehlermeldung wird anschließend in der Variablen $message gespeichert. |
|
Im abschließenden HTML-Teil wird ein einfaches Formular zur Eingabe der Kundennummer bereitgestellt. Ist die Variable $message nicht leer, wird ihr Inhalt ausgegeben. Die Funktion htmlspecialchars() sorgt dafür, dass Sonderzeichen korrekt kodiert werden und verhindert so mögliche Sicherheitsprobleme wie HTML-Injection. Vgl. auch den Anhang. |
|
4.4 RechnungLoeschenPDO.php |
|
Nun das Löschen einer ganzen Rechnung aus der Datenbank KuReAr. |
|
4.4.1 Das Programm |
|
|
<?php
|
|
|
|
|
|
// Variable für Rückmeldungen an den Benutzer.
|
|
|
$message = "";
|
|
|
|
|
|
// Verbindungsdaten für PDO. DSN bedeutet "Data Source Name".
|
|
|
// Der DSN enthält den Host, die Bezeichnung der Datenbank
|
|
|
// und den verwendeten Zeichensatz.
|
|
|
$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";
|
|
|
$user = "root";
|
|
|
$pass = "";
|
|
|
|
|
|
// Die Variable wird zunächst mit null beschrieben.
|
|
|
// Später verweist sie auf das erzeugte PDO-Objekt.
|
|
|
$pdo = null;
|
|
|
try {
|
|
|
|
|
|
// Verbindung zur Datenbank aufbauen.
|
|
|
// PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION bewirkt,
|
|
|
// dass PDO bei einem Datenbankfehler eine Exception auslöst.
|
|
|
$pdo = new PDO($dsn,$user,$pass,
|
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
|
|
|
|
// Prüfen, ob das Formular mit der HTTP-Methode POST
|
|
|
// abgeschickt wurde.
|
|
|
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|
|
|
|
|
// Rechnungsnummer aus dem Formular übernehmen.
|
|
|
// Der Operator ?? liefert 0, wenn der Formulareintrag fehlt.
|
|
|
// (int) wandelt den übernommenen Wert in eine ganze Zahl um.
|
|
|
$reNr = (int)($_POST["reNr"] ?? 0);
|
|
|
|
|
|
// Eingabe prüfen. Die Rechnungsnummer muss größer als 0 sein.
|
|
|
if ($reNr <= 0) {
|
|
|
$message = "Bitte eine gültige Rechnungsnummer eingeben.";
|
|
|
} else {
|
|
|
try {
|
|
|
|
|
|
// Transaktion starten. Alle folgenden Löschoperationen gehören
|
|
|
// damit zu // einer gemeinsamen Einheit. Sie werden entweder
|
|
|
// vollständig durchgeführt oder vollständig rückgängig gemacht.
|
|
|
$pdo->beginTransaction();
|
|
|
|
|
|
// Prüfen, ob ein Rechnungskopf mit der eingegebenen
|
|
|
// Rechnungsnummer existiert.
|
|
|
// :reNr ist ein Platzhalter für die Rechnungsnummer.
|
|
|
$stmtCheck = $pdo->prepare(
|
|
|
"SELECT 1 FROM rechkoepfe WHERE ReNr = :reNr");
|
|
|
|
|
|
// Prepared Statement ausführen.
|
|
|
// Der Wert der PHP-Variablen $reNr wird dem
|
|
|
// Platzhalter :reNr zugeordnet.
|
|
|
$stmtCheck->execute(["reNr" => $reNr]);
|
|
|
|
|
|
// fetchColumn() liefert den Inhalt der ersten Spalte der gefundenen
|
|
|
// Zeile. Wird keine Zeile gefunden, liefert die Methode false.
|
|
|
$rechnungVorhanden = $stmtCheck->fetchColumn();
|
|
|
|
|
|
// Prüfen, ob ein Rechnungskopf gefunden wurde.
|
|
|
if ($rechnungVorhanden === false) {
|
|
|
|
|
|
// Es gibt nichts zu löschen. Die Transaktion wird
|
|
|
// ohne dauerhafte Änderungen beendet.
|
|
|
$pdo->rollBack();
|
|
|
$message = "Keine Rechnung mit ReNr = $reNr gefunden.";
|
|
|
} else {
|
|
|
|
|
|
/*
|
|
|
1. Die zur Rechnung gehörenden Positionen löschen.
|
|
|
Die Positionen müssen vor dem Rechnungskopf gelöscht werden,
|
|
|
sofern der Fremdschlüssel nicht mit ON DELETE CASCADE
|
|
|
definiert wurde.
|
|
|
*/
|
|
|
$stmtPosDel = $pdo->prepare("DELETE FROM rechpos
|
|
|
WHERE ReNr = :reNr");
|
|
|
$stmtPosDel->execute(["reNr" => $reNr]);
|
|
|
|
|
|
// rowCount() liefert bei dieser DELETE-Anweisung
|
|
|
// die Anzahl der gelöschten Rechnungspositionen.
|
|
|
$deletedPos = $stmtPosDel->rowCount();
|
|
|
|
|
|
// 2. Den Rechnungskopf löschen.
|
|
|
$stmtKopfDel = $pdo->prepare("DELETE FROM rechkoepfe
|
|
|
WHERE ReNr = :reNr");
|
|
|
$stmtKopfDel->execute(["reNr" => $reNr]);
|
|
|
|
|
|
// Anzahl der gelöschten Rechnungsköpfe ermitteln.
|
|
|
// Bei einer eindeutigen Rechnungsnummer sollte
|
|
|
// der Wert 1 sein.
|
|
|
$deletedKopf = $stmtKopfDel->rowCount();
|
|
|
|
|
|
// Zusätzliche Sicherheitsprüfung:
|
|
|
// Wurde kein Rechnungskopf gelöscht, wird eine
|
|
|
// Exception ausgelöst.
|
|
|
if ($deletedKopf !== 1) {throw new Exception(
|
|
|
"Der Rechnungskopf konnte nicht gelöscht werden.");
|
|
|
}
|
|
|
|
|
|
// Transaktion erfolgreich abschließen. // Erst dadurch werden
|
|
|
// die Löschungen dauerhaft in der Datenbank gespeichert.
|
|
|
$pdo->commit();
|
|
|
$message = "Rechnung ReNr = $reNr gelöscht "
|
|
|
. "(Kopf: $deletedKopf, " . "Positionen: $deletedPos).";
|
|
|
}
|
|
|
} catch (Exception $e) {
|
|
|
|
|
|
// Prüfen, ob noch eine Transaktion aktiv ist.
|
|
|
// Nur dann darf rollBack() aufgerufen werden.
|
|
|
if ($pdo->inTransaction()) {
|
|
|
|
|
|
// Alle seit Beginn der Transaktion vorgenommenen
|
|
|
// Änderungen rückgängig machen.
|
|
|
$pdo->rollBack();
|
|
|
}
|
|
|
$message = "Fehler beim Löschen. " . "Alles wurde zurückgesetzt: "
|
|
|
. $e->getMessage();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
} catch (PDOException $e) {
|
|
|
|
|
|
// Fehler beim Aufbau der Datenbankverbindung.
|
|
|
$message = "DB-Verbindung fehlgeschlagen: " . $e->getMessage();
|
|
|
}
|
|
|
?>
|
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>KuReAr - Rechnung löschen</title>
|
|
|
<style>body {font-family: Arial, sans-serif;}</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<h2>Rechnung löschen (PDO)</h2>
|
|
|
|
|
|
<!--
|
|
|
Formular zur Eingabe der Rechnungsnummer.
|
|
|
method="post" legt fest, dass die Formulardaten mit der
|
|
|
HTTP-Methode POST übertragen werden. Da kein action-Attribut
|
|
|
angegeben ist, werden sie an dieselbe PHP-Datei gesendet.
|
|
|
-->
|
|
|
<form method="post">
|
|
|
<label for="reNr">Rechnungsnummer (ReNr):</label>
|
|
|
<input type="number" id="reNr" name="reNr" min="1" required>
|
|
|
<button type="submit">Löschen</button>
|
|
|
</form>
|
|
|
|
|
|
<!--
|
|
|
Wenn eine Rückmeldung vorliegt, wird sie in Fettschrift
|
|
|
ausgegeben. htmlspecialchars() verhindert, dass enthaltene
|
|
|
Sonderzeichen als HTML-Code interpretiert werden. Vgl. Anhang.
|
|
|
-->
|
|
|
<?php if ($message !== ""): ?>
|
|
|
<p>
|
|
|
<strong>
|
|
|
<?php echo htmlspecialchars($message,ENT_QUOTES,"UTF-8"); ?>
|
|
|
</strong>
|
|
|
</p>
|
|
|
<?php endif; ?>
|
|
|
</body>
|
|
|
</html>
|
|
4.4.2 Programmlauf |
|
So sieht das Ergebnis nach einem erfolgreichen Programmlauf aus. |
|

|
|
4.4.3 Erläuterung des Programms |
|
Das Programm dient dazu, eine Rechnung mit Rechnungskopf und Rechnungspositionen aus der Datenbank zu löschen. Es verwendet dazu PDO und eine Transaktion. Der Einsatz einer Transaktion ist hier unabdiingbar, da eine Rechnung aus zwei miteinander verknüpften Teilen besteht: dem Rechnungskopf in der Relation Rechkoepfe und den zugehörigen Rechnungspositionen in der Relation Rechpos. Beide Teile müssen konsistent behandelt werden, sodass entweder alle zugehörigen Löschungen gelöscht werden oder keine Änderung dauerhaft gespeichert wird. |
|
Zu Beginn werden die Verbindungsdaten für PDO festgelegt. Der DSN (Data Source Name) enthält den Host, die Bezeichnung der Datenbank und den verwendeten Zeichensatz utf8mb4. Anschließend wird mit new PDO(...) eine Verbindung zur Datenbank aufgebaut. Die Option PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION bewirkt, dass PDO bei Datenbankfehlern Exceptions auslöst. Tritt bereits beim Verbindungsaufbau ein Fehler auf, wird dieser in einem catch-Block für PDOException abgefangen und eine entsprechende Fehlermeldung erzeugt. |
|
Nach dem Absenden des Formulars wird die eingegebene Rechnungsnummer eingelesen und überprüft. Nur positive Werte werden als gültig akzeptiert. Ist die Eingabe ungültig, wird eine entsprechende Fehlermeldung ausgegeben. |
|
Bei gültiger Eingabe wird ein weiterer try/catch-Block durchlaufen. Zunächst wird mit beginTransaction() eine Transaktion gestartet. Ab diesem Zeitpunkt gehören die folgenden Datenbankoperationen zu einer gemeinsamen Arbeitseinheit. |
|
Als Nächstes prüft das Programm, ob die angegebene Rechnung existiert. Dazu wird mit prepare() ein Prepared Statement mit dem benannten Platzhalter :reNr vorbereitet. Beim Aufruf von execute() wird diesem Platzhalter die eingegebene Rechnungsnummer zugeordnet. Die Methode fetchColumn() liefert den Wert der ersten Spalte der gefundenen Zeile. Wird keine passende Zeile gefunden, liefert sie false. In diesem Fall wird die Transaktion mit rollBack() beendet und eine entsprechende Meldung ausgegeben. Da bis dahin noch keine Datensätze gelöscht wurden, müssen allerdings keine Änderungen rückgängig gemacht werden. |
|
Existiert die Rechnung, erfolgt das Löschen in zwei Schritten. Zuerst werden alle zugehörigen Datensätze aus Rechpos gelöscht. Dieser Schritt ist erforderlich, da die Rechnungspositionen aufgrund der Fremdschlüsselbeziehung vom Rechnungskopf abhängen. Solange sie noch existieren, kann der Rechnungskopf nicht gelöscht werden, sofern für den Fremdschlüssel nicht ON DELETE CASCADE festgelegt wurde. Die Anzahl der gelöschten Rechnungspositionen wird mit rowCount() ermittelt. |
|
Anschließend wird der Rechnungskopf aus Rechkoepfe gelöscht. Auch hier wird die Anzahl der gelöschten Datensätze mit rowCount() ermittelt. Da die Rechnungsnummer den Rechnungskopf eindeutig kennzeichnet, sollte genau ein Datensatz gelöscht worden sein. Ist dies nicht der Fall, löst das Programm eine Exception aus. |
|
Wurden beide Löschoperationen erfolgreich durchgeführt, wird die Transaktion mit commit() abgeschlossen. Erst dadurch werden die Änderungen dauerhaft in der Datenbank gespeichert. Anschließend wird eine Meldung ausgegeben, die auch die Anzahl der gelöschten Rechnungsköpfe und Rechnungspositionen enthält. |
|
Tritt während der Transaktion ein Fehler auf, wird dieser im catch-Block abgefangen. Zunächst prüft inTransaction(), ob noch eine aktive Transaktion besteht. Ist dies der Fall, werden mit rollBack() alle seit Beginn der Transaktion vorgenommenen Änderungen rückgängig gemacht. Dadurch wird verhindert, dass beispielsweise die Rechnungspositionen gelöscht werden, der zugehörige Rechnungskopf aber erhalten bleibt. Die Rechnung wird somit entweder vollständig gelöscht oder gar nicht. Auf diese Weise bleibt die Datenbank in einem konsistenten Zustand. |
|
In früheren Zeiten sprach man von einer Stammdatenkrise , wenn inkonsistente Datenbestände für Verwirrung in den Datenbeständen und den auswertenden Programmen sorgten. Dies ist, auch wenn der Begriff aus der Mode kam, sicherlich auch heute noch der Fall. |
Stamm-datenkrise |
Ein ausdrückliches Schließen der Datenbankverbindung ist bei PDO normalerweise nicht erforderlich. Die Verbindung wird spätestens am Ende der Skriptausführung freigegeben, wenn das PDO-Objekt nicht mehr benötigt wird. |
|
7 KuReAr - Rechnungsverwaltung |
|
Hier nun ein etwas umfangreicheres Beispiel zu den Grundzügen einer Rechnungsverwaltung. Es sollen ganze Rechnungen eingegeben werden. Die Programme hier sind mit der Klasse PDO realisiert. |
|
"KuReAr" meint "Kunden - Rechnungen -Artikel". |
|
|
|
7.1 Maskensteuerung |
|
Auf der obersten Ebene (leitseite) wird eine einfache Web-Oberfläche für die Datenbank KuReAr erstellt. Sie wird mit http://localhost/kurearstart.php aufgerufen: |
|

|
|
Im ersten Schritt wird die Eingabe von Rechnungen realisiert, die übrigen Punkte kommen später. Das Programm für die Leitseite hat die Bezeichnung KuReArStart.php. Die notwendige CSS-Datei ist KuReArStart.css. Diese beiden befinden sich im Verzeichnis htdocs. Alle weiteren Programme kommen in ein Unterverzeichnis /htdocs/KuReAr. Dorthin verweisen entsprechend die Links in den Schaltflächen. Insgesamt gilt also für die Ordnerstruktur: |
Ordner-Struktur |
- Für KuReArStart.php und KuReArStart.css: http://localhost/KuReArStart.php
- Für Rechnungen eingeben: http://localhost/KuReAr/KuReArRechnungen.php
- Für Artikel bearbeiten: http://localhost/KuReAr/KuReArArtikel.php
- Für Kundendaten bearbeiten: http://localhost/KuReAr/KuReArKunden.php
- usw.
|
|
7.2 Start |
|
7.2.1 KuReArStart.php |
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>Rechnungsverwaltung</title>
|
|
|
<link rel="stylesheet" href="KuReArStart.css">
|
|
|
</head>
|
|
|
<body>
|
|
|
|
|
|
<!--
|
|
|
Äußerer Bereich der Startseite.
|
|
|
Die Klasse "box" kann in der CSS-Datei beispielsweise
|
|
|
für Breite, Hintergrundfarbe, Rahmen und Abstände
|
|
|
verwendet werden.
|
|
|
-->
|
|
|
<div class="box">
|
|
|
<h1>Rechnungsverwaltung</h1>
|
|
|
|
|
|
<!--
|
|
|
Navigationsbereich mit Verweisen auf die einzelnen
|
|
|
Programme der Rechnungsverwaltung.
|
|
|
Die Klasse "menu" dient der Gestaltung und
|
|
|
Anordnung der Menüeinträge.
|
|
|
-->
|
|
|
<nav class="menu">
|
|
|
<!-- Erfassung neuer Rechnungen -->
|
|
|
<a class="btn" href="KuReAr/KuReArRechnungen.php">
|
|
|
Rechnungen eingeben</a>
|
|
|
|
|
|
<!-- Bearbeitung der Artikeldaten -->
|
|
|
<a class="btn" href="KuReAr/KuReArArtikel.php">
|
|
|
Artikel bearbeiten</a>
|
|
|
|
|
|
<!-- Bearbeitung der Kundendaten -->
|
|
|
<a class="btn" href="KuReAr/KuReArKunden.php">
|
|
|
Kundendaten bearbeiten</a>
|
|
|
|
|
|
<!-- Trennlinie zwischen Verwaltung und Auswertung -->
|
|
|
<hr class="sep">
|
|
|
|
|
|
<!-- Ausgabe einer Liste der Rechnungen -->
|
|
|
<a class="btn" href="KuReAr/KuReArAusw1.php">
|
|
|
Liste der Rechnungen</a>
|
|
|
|
|
|
<!-- Ausgabe einer Liste der Kunden -->
|
|
|
<a class="btn" href="KuReAr/KuReArAusw2.php">
|
|
|
Liste der Kunden</a>
|
|
|
|
|
|
<!-- Ausgabe des aktuellen Artikelbestands -->
|
|
|
<a class="btn" href="KuReAr/KuReArAusw3.php">
|
|
|
Artikelbestand</a>
|
|
|
</nav>
|
|
|
</div>
|
|
|
</body>
|
|
|
</html>
|
|
Hinweis zum Navigationselement nav: nav ist keine Anweisung, sondern ein semantisches HTML-Element. Die Bezeichnung ist von "navigation" abgeleitet. Es kennzeichnet einen Bereich mit wichtigen Navigationsverweisen, beispielsweise ein Hauptmenü. Es verändert für sich genommen die Darstellung normalerweise nicht wesentlich. Seine Hauptaufgabe besteht darin, die Bedeutung des Bereichs zu beschreiben. Das hilft insbesondere Browsern und Suchmaschinen. |
|
7.2.2 KuReArStart.css |
|
Wie immer gilt: Die Programmerläuterungen beziehen sich, wenn nicht anders angegeben, auf den nachfolgenden Programmabschnitt. |
|
|
/*
|
|
|
Diese Regel gilt für alle HTML-Elemente.
|
|
|
box-sizing: border-box bewirkt, dass die mit width und
|
|
|
height festgelegten Größen bereits die Innenabstände
|
|
|
und Rahmen enthalten.
|
|
|
Der Stern im folgenden Befehl ist der sog. Universalselektor.
|
|
|
Er wählt alle HTML-Elemente der Webseite aus.
|
|
|
*/
|
|
|
* {box-sizing: border-box;}
|
|
|
|
|
|
/*
|
|
|
Grundlegende Gestaltung des sichtbaren Seitenbereichs.
|
|
|
margin: 0 entfernt den vom Browser normalerweise
|
|
|
vorgegebenen Außenabstand.
|
|
|
padding: 20px erzeugt einen Innenabstand zwischen dem
|
|
|
Rand des Browserfensters und dem Inhalt der Webseite.
|
|
|
Als Schriftart wird Arial verwendet. Falls Arial nicht
|
|
|
verfügbar ist, wählt der Browser eine andere serifenlose
|
|
|
Schriftart.
|
|
|
*/
|
|
|
body {margin: 0; padding: 20px; font-family: Arial, sans-serif;}
|
|
|
|
|
|
/*
|
|
|
Äußerer Bereich der Rechnungsverwaltung.
|
|
|
width: 100% bewirkt, dass die Box grundsätzlich die
|
|
|
verfügbare Breite einnimmt.
|
|
|
max-width: 500px begrenzt ihre Breite jedoch auf
|
|
|
höchstens 500 Pixel. In schmalen Browserfenstern kann
|
|
|
die Box dadurch entsprechend kleiner werden.
|
|
|
margin: 20px auto legt oben und unten einen Außenabstand
|
|
|
von 20 Pixeln fest. Der Wert auto für links und rechts
|
|
|
zentriert die Box horizontal.
|
|
|
padding legt die Innenabstände fest:
|
|
|
- oben: 18 Pixel
|
|
|
- rechts: 18 Pixel
|
|
|
- unten: 22 Pixel
|
|
|
- links: 18 Pixel
|
|
|
border erzeugt einen schwarzen, durchgezogenen Rahmen
|
|
|
mit einer Stärke von 3 Pixeln.
|
|
|
*/
|
|
|
.box {width: 100%; max-width: 500px;margin: 20px auto;
|
|
|
padding: 18px 18px 22px; border: 3px solid #000;
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
Gestaltung der Hauptüberschrift.
|
|
|
margin legt die Außenabstände fest:
|
|
|
- oben: 6 Pixel
|
|
|
- rechts: 0
|
|
|
- unten: 18 Pixel
|
|
|
- links: 0
|
|
|
font-size legt die Schriftgröße auf 28 Pixel fest.
|
|
|
text-align: center zentriert den Text innerhalb des
|
|
|
Überschriftenbereichs.
|
|
|
*/
|
|
|
h1 {margin: 6px 0 18px; font-size: 28px; text-align: center;}
|
|
|
|
|
|
/*
|
|
|
Anordnung des Navigationsmenüs mit Flexbox.
|
|
|
display: flex aktiviert das Flexbox-Verfahren.
|
|
|
flex-direction: column ordnet die unmittelbar
|
|
|
untergeordneten Elemente senkrecht untereinander an.
|
|
|
gap legt zwischen den Menüelementen einen Abstand
|
|
|
von 12 Pixeln fest.
|
|
|
*/
|
|
|
.menu {display: flex; flex-direction: column; gap: 12px;}
|
|
|
|
|
|
/*
|
|
|
Gestaltung der Verweise als Schaltflächen. "btn" steht für Button.
|
|
|
display: block macht aus dem Verweis ein Blockelement.
|
|
|
Dadurch kann er die verfügbare Breite einnehmen.
|
|
|
padding legt den Innenabstand fest:
|
|
|
- oben und unten: 12 Pixel
|
|
|
- links und rechts: 14 Pixel
|
|
|
border erzeugt einen schwarzen, durchgezogenen Rahmen
|
|
|
mit einer Stärke von 2 Pixeln.
|
|
|
background-color legt einen hellgrauen Hintergrund fest.
|
|
|
color: #000 legt Schwarz als Schriftfarbe fest.
|
|
|
font-size legt die Schriftgröße auf 16 Pixel fest.
|
|
|
text-decoration: none entfernt die für Verweise übliche
|
|
|
Unterstreichung.
|
|
|
*/
|
|
|
.btn {display: block; padding: 12px 14px;border: 2px solid #000;
|
|
|
background-color: #eee;color: #000;font-size: 16px;
|
|
|
text-decoration: none;}
|
|
|
|
|
|
/*
|
|
|
Gestaltung einer Schaltfläche, wenn sich der Mauszeiger
|
|
|
darüber befindet.
|
|
|
Die Hintergrundfarbe wird etwas dunkler. Dadurch erhält
|
|
|
der Benutzer eine optische Rückmeldung.
|
|
|
*/
|
|
|
.btn:hover {background-color: #ddd;}
|
|
|
|
|
|
/*
|
|
|
Trennlinie zwischen den Programmen zur Datenerfassung
|
|
|
und Datenbearbeitung und den Auswertungsprogrammen.
|
|
|
margin legt oben und unten einen Außenabstand von
|
|
|
6 Pixeln fest. Links und rechts gibt es keinen
|
|
|
zusätzlichen Außenabstand.
|
|
|
border: none entfernt die Standarddarstellung des
|
|
|
hr-Elements.
|
|
|
border-top erzeugt stattdessen eine schwarze,
|
|
|
durchgezogene obere Linie mit einer Stärke von
|
|
|
2 Pixeln.
|
|
|
*/
|
|
|
.sep {margin: 6px 0;border: none;border-top: 2px solid #000;}
|
|
7.3 Rechnungen eingeben |
|
7.3.1 Aufgabe |
|
Nun das Programm "hinter" dieser Schaltfläche: |
|

|
|
Nach Betätigen der Schaltfläche soll folgende Maske erscheinen: |
|

|
|
Die Maske soll folgende Funktionalitäten erfassen, bzw. ermöglichen: |
|
- Eingabe der Anzahl an Rechnungspositionen ("Maske anpassen"). Drücken von "Maske anpassen" soll dann die Anzahl einzugebender Positionen ändern und zu folgender Anzeige führen:
|
|

|
|
- Wahl des Kunden, dem die Rechnung gilt. Das Eingabefenster greift auf die Kundendatei zu, zeigt mit Hilfe eines Dropdown-Menüs Name und Kundennummer an und erlaubt die Übernahme der Daten.
|
|

|
|
- Eingabe des Rechnungsdatums
|
|

|
|
|
|
|
Klickt man auf ein Artikelfeld werden wiederum über ein Dropdown-Menü die Artikel der Datei (Relation) Artikel angezeigt und es kann ausgewählt werden. |
|

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

|
|
die Daten in die Datenbank eingetragen. Danach soll die neue Rechnung z.B. so angezeigt werden: |
|

|
|
|
|
Betätigen von "Zurück" führt wieder zur Leitseite. |
|
Eingabe einer Rechnung mit http://localhost/KuReAr/KuReArRechnungen.php: |
|

|
|
Anzeigen der neuen Rechnung mit KuReAr_6_show.php |
|

|
|
Gesamtausgabe aller vier Relationen (Tabellen) mit KuReAr_5.php |
|

|
|
Insgesamt werden also zusätzlich zu KuReArStart (in htdocs) folgende Programme für die Bearbeitung von Rechnungen im Verzeichnis /htdocs/KuReAr angelegt: |
|
- KuReArRechnungen.php für die Eingabe einer neuen Rechnung
- KuReArRechnungen.css
- KuReAr_6_show.php für das Anzeigen der Rechnung nach erfolgter Eingabe einer neuen Rechnung
- KuReAr_6_show.css
- KuReAr_5.php für testweise Ausgabe aller vier Relationen in einer Tabelle nach erfolgter Eingabe einer neuen Rechnung
- KuReAr_5.css
|
|
Nun die Programme. |
|
7.3.2 KuReArRechnungen.php |
|
|
<?php
|
|
|
/*
|
|
|
1. VERBINDUNGSDATEN ZUR DATENBANK
|
|
|
Der DSN (Data Source Name) beschreibt, wie PDO die
|
|
|
Verbindung zur MySQL-Datenbank herstellen soll:
|
|
|
- mysql bezeichnet den zu verwendenden PDO-Treiber.
|
|
|
- host=localhost bedeutet, dass der Datenbankserver auf
|
|
|
demselben Rechner wie das PHP-Programm läuft.
|
|
|
- dbname=KuReAr gibt die Bezeichnung der Datenbank an.
|
|
|
- charset=utf8mb4 legt den Zeichensatz der Verbindung fest.
|
|
|
*/
|
|
|
$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";
|
|
|
|
|
|
/*
|
|
|
Benutzername und Passwort für den Datenbankzugriff.
|
|
|
Bei einer lokalen XAMPP-Testinstallation wird häufig
|
|
|
der Benutzer root ohne Passwort verwendet. In einer
|
|
|
öffentlich erreichbaren Anwendung wäre dies ungeeignet.
|
|
|
*/
|
|
|
$user = "root";
|
|
|
$pass = "";
|
|
|
|
|
|
/*
|
|
|
2. VOREINSTELLUNGEN FÜR DIE EINGABEMASKE
|
|
|
Beim ersten Aufruf des Programms werden standardmäßig
|
|
|
drei Eingabezeilen für Rechnungspositionen angezeigt.
|
|
|
*/
|
|
|
$defaultPosCount = 3;
|
|
|
$posCount = $defaultPosCount;
|
|
|
|
|
|
/*
|
|
|
Variablen für Rückmeldungen an den Benutzer:
|
|
|
- $message enthält den Meldungstext.
|
|
|
- $messageClass enthält die zugehörige CSS-Klasse,
|
|
|
beispielsweise ok oder err.
|
|
|
*/
|
|
|
$message = "";
|
|
|
$messageClass = "";
|
|
|
|
|
|
/*
|
|
|
Die Arrays werden vorsorglich initialisiert.
|
|
|
Falls bereits der Verbindungsaufbau fehlschlägt, können
|
|
|
die Variablen dadurch später trotzdem ohne PHP-Warnung
|
|
|
verwendet werden.
|
|
|
*/
|
|
|
$kunden = [];
|
|
|
$artikel = [];
|
|
|
|
|
|
/*
|
|
|
3. HILFSFUNKTION FÜR DIE SICHERE HTML-AUSGABE
|
|
|
htmlspecialchars() vgl. Anhang.
|
|
|
ENT_QUOTES berücksichtigt sowohl doppelte als auch
|
|
|
einfache Anführungszeichen.
|
|
|
*/
|
|
|
function h(string $s): string
|
|
|
{
|
|
|
return htmlspecialchars($s, ENT_QUOTES, "UTF-8");
|
|
|
}
|
|
|
try {
|
|
|
|
|
|
/*
|
|
|
4. VERBINDUNG ZUR DATENBANK HERSTELLEN
|
|
|
new PDO() erzeugt ein Objekt der Klasse PDO. Die
|
|
|
Variable $pdo verweist auf dieses Verbindungsobjekt.
|
|
|
PDO::ERRMODE_EXCEPTION legt fest, dass PDO bei einem
|
|
|
Datenbankfehler eine PDOException auslöst. Diese kann
|
|
|
im catch-Abschnitt zentral behandelt werden.
|
|
|
*/
|
|
|
$pdo = new PDO($dsn,$user,$pass,
|
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
|
|
|
|
/*
|
|
|
5. STAMMDATEN LADEN
|
|
|
Für die Auswahllisten der Eingabemaske werden die
|
|
|
Kunden- und Artikeldaten aus der Datenbank gelesen.
|
|
|
query() kann hier verwendet werden, weil die
|
|
|
SQL-Anweisungen keine Werte aus Benutzereingaben
|
|
|
enthalten.
|
|
|
*/
|
|
|
|
|
|
// Kunden nach Name und Vorname sortiert laden.
|
|
|
$kunden = $pdo->query("SELECT KuNr, Name, Vorname FROM Kunden
|
|
|
ORDER BY Name, Vorname")->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
|
|
// Artikel nach ihrer Beschreibung sortiert laden.
|
|
|
$artikel = $pdo->query(
|
|
|
"SELECT ArtNr, Beschreibung, Preis FROM Artikel
|
|
|
ORDER BY Beschreibung")->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
|
|
/*
|
|
|
6. ANZAHL DER POSITIONSZEILEN FESTLEGEN
|
|
|
Falls das Formular einen Wert für posCount übermittelt
|
|
|
hat, wird dieser übernommen.
|
|
|
Die Anzahl wird auf den Bereich von 1 bis 20 begrenzt.
|
|
|
Die serverseitige Begrenzung ist auch dann wirksam,
|
|
|
wenn jemand die HTML-Vorgaben im Browser umgeht.
|
|
|
*/
|
|
|
if (isset($_POST["posCount"])) {
|
|
|
$posCount = (int)$_POST["posCount"];
|
|
|
if ($posCount < 1) {$posCount = 1;}
|
|
|
if ($posCount > 20) {$posCount = 20;}
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
7. GEWÜNSCHTE AKTION FESTSTELLEN
|
|
|
Das Formular kann zwei Aktionen auslösen:
|
|
|
- resize: Anzahl der Eingabezeilen ändern
|
|
|
- save: Rechnung in der Datenbank speichern
|
|
|
Der Null-Koaleszenz-Operator ?? liefert eine leere
|
|
|
Zeichenkette, falls action nicht übermittelt wurde.
|
|
|
*/
|
|
|
$action = $_POST["action"] ?? "";
|
|
|
|
|
|
/*
|
|
|
8. FALL 1: RECHNUNG SPEICHERN
|
|
|
*/
|
|
|
if ($action === "save") {
|
|
|
|
|
|
/*
|
|
|
Kopfdaten aus dem Formular übernehmen.
|
|
|
Formulardaten treffen grundsätzlich als
|
|
|
Zeichenketten ein. Die Kundennummer wird daher
|
|
|
ausdrücklich in eine ganze Zahl umgewandelt.
|
|
|
*/
|
|
|
$KuNr = (int)($_POST["KuNr"] ?? 0);
|
|
|
$ReDatum = trim($_POST["ReDatum"] ?? "");
|
|
|
|
|
|
/*
|
|
|
Prüfen, ob die erforderlichen Kopfdaten vorhanden
|
|
|
sind.
|
|
|
*/
|
|
|
if ($KuNr <= 0 || $ReDatum === "") {
|
|
|
throw new Exception(
|
|
|
"Kunde und Rechnungsdatum müssen angegeben werden."
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
Das Format des Datums prüfen.
|
|
|
createFromFormat() versucht, aus der Eingabe ein
|
|
|
Datum im Format Jahr-Monat-Tag zu erzeugen.
|
|
|
*/
|
|
|
$datum = DateTime::createFromFormat("Y-m-d",$ReDatum);
|
|
|
|
|
|
if ($datum === false ||
|
|
|
$datum->format("Y-m-d") !== $ReDatum) {
|
|
|
throw new Exception("Das Rechnungsdatum ist ungültig.");
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
9. POSITIONEN AUS DEM FORMULAR EINSAMMELN
|
|
|
In $positions werden alle vollständig ausgefüllten
|
|
|
Rechnungspositionen gesammelt.
|
|
|
Vollständig leere Zeilen werden ignoriert. Ist nur
|
|
|
eines der beiden Felder ausgefüllt, wird dagegen
|
|
|
eine Fehlermeldung erzeugt.
|
|
|
*/
|
|
|
$positions = [];
|
|
|
for ($i = 1; $i <= $posCount; $i++) {
|
|
|
$ArtNr = (int)($_POST["ArtNr_$i"] ?? 0);
|
|
|
$Anzahl = (int)($_POST["Anzahl_$i"] ?? 0);
|
|
|
|
|
|
/*
|
|
|
Eine vollständig leere Positionszeile wird übersprungen.
|
|
|
*/
|
|
|
if ($ArtNr === 0 && $Anzahl === 0) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
Eine teilweise oder ungültig ausgefüllte
|
|
|
Positionszeile wird nicht stillschweigend ignoriert.
|
|
|
*/
|
|
|
if ($ArtNr <= 0 || $Anzahl <= 0) {throw new Exception(
|
|
|
"Position $i ist nicht vollständig "
|
|
|
. "oder nicht gültig ausgefüllt.");
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
Die Positionsnummern werden lückenlos neu vergeben.
|
|
|
Leere Zeilen verursachen dadurchkeine Lücken in der Nummerierung.
|
|
|
*/
|
|
|
$positions[] = ["PosNr" => count($positions) + 1,
|
|
|
"ArtNr" => $ArtNr, "Anzahl" => $Anzahl];
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
Eine Rechnung muss mindestens eine gültige
|
|
|
Rechnungsposition besitzen.
|
|
|
*/
|
|
|
if (count($positions) === 0) {throw new Exception(
|
|
|
"Mindestens eine Rechnungsposition "
|
|
|
. "muss ausgefüllt sein.");
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
10. TRANSAKTION STARTEN
|
|
|
Rechnungskopf und Rechnungspositionen gehören zusammen.
|
|
|
Deshalb werden alle zugehörigen Daten innerhalb einer
|
|
|
Transaktion gespeichert.Entweder werden sämtliche Daten
|
|
|
übernommen oder im Fehlerfall vollständig zurückgenommen.
|
|
|
*/
|
|
|
$pdo->beginTransaction();
|
|
|
|
|
|
/*
|
|
|
11. NEUE RECHNUNGSNUMMER ERMITTELN
|
|
|
Zunächst wird die größte bisher vorhandene
|
|
|
Rechnungsnummer ermittelt und anschließend um
|
|
|
eins erhöht.
|
|
|
Konkret:
|
|
|
Die SQL-Anweisung ermittelt mit MAX(ReNr) die größte vorhandene
|
|
|
Rechnungsnummer. Ist die Relation noch leer, ersetzt IFNULL() den
|
|
|
Wert NULL durch 0. Anschließend wird der Wert um 1 erhöht und mit
|
|
|
der Bezeichnung NeueNr zurückgegeben. fetch(PDO::FETCH_ASSOC) liest
|
|
|
das Ergebnis als assoziatives Array. Der Wert mit dem Schlüssel
|
|
|
"NeueNr" wird danach in eine ganze Zahl umgewandelt und in $NeueReNr
|
|
|
gespeichert.
|
|
|
*/
|
|
|
$stmt = $pdo->query("SELECT IFNULL(MAX(ReNr), 0) + 1 AS NeueNr
|
|
|
FROM RechKoepfe");
|
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
$NeueReNr = (int)$row["NeueNr"];
|
|
|
|
|
|
/*
|
|
|
12. RECHNUNGSKOPF SPEICHERN
|
|
|
Die Fragezeichen sind Positionsplatzhalter eines
|
|
|
Prepared Statements.
|
|
|
Beim Aufruf von execute() müssen die Werte in
|
|
|
derselben Reihenfolge angegeben werden wie die
|
|
|
zugehörigen Platzhalter.
|
|
|
*/
|
|
|
$stmtHead = $pdo->prepare("INSERT INTO RechKoepfe
|
|
|
(ReNr, KuNr, ReDatum) VALUES (?, ?, ?)");
|
|
|
|
|
|
$stmtHead->execute([$NeueReNr,$KuNr,$ReDatum]);
|
|
|
|
|
|
/*
|
|
|
13. RECHNUNGSPOSITIONEN SPEICHERN
|
|
|
Das Prepared Statement wird nur einmal vorbereitet.
|
|
|
Anschließend kann es für jede Rechnungsposition
|
|
|
erneut ausgeführt werden.
|
|
|
*/
|
|
|
$stmtPos = $pdo->prepare("INSERT INTO RechPos
|
|
|
(ReNr, PosNr, ArtNr, Anzahl) VALUES(?,?, ?, ?)");
|
|
|
foreach ($positions as $p)
|
|
|
{$stmtPos->execute([$NeueReNr,$p["PosNr"],$p["ArtNr"],
|
|
|
$p["Anzahl"]]);
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
14. TRANSAKTION ABSCHLIESSEN
|
|
|
Da alle Datenbankoperationen erfolgreich waren,
|
|
|
werden die Änderungen mit commit() dauerhaft übernommen.
|
|
|
*/
|
|
|
$pdo->commit();
|
|
|
|
|
|
/*
|
|
|
15. ZUR ANZEIGESEITE WEITERLEITEN
|
|
|
Die Rechnungsnummer wird als URL-Parameter an die
|
|
|
Anzeigeseite übergeben.
|
|
|
urlencode() sorgt dafür, dass der übergebene Wert
|
|
|
für die Verwendung innerhalb einer URL geeignet ist.
|
|
|
exit beendet das aktuelle PHP-Programm unmittelbar
|
|
|
nach dem Versenden des Location-Headers.
|
|
|
*/
|
|
|
header("Location: KuReAr_6_show.php?ReNr="
|
|
|
. urlencode((string)$NeueReNr));
|
|
|
exit;
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
16. FALL 2: EINGABEMASKE ANPASSEN
|
|
|
In diesem Fall werden keine Daten in der Datenbank
|
|
|
gespeichert. Es wird lediglich die Anzahl der
|
|
|
angezeigten Positionszeilen geändert.
|
|
|
*/
|
|
|
if ($action === "resize") {$message = "Maske angepasst: "
|
|
|
. $posCount . "Position(en).";
|
|
|
$messageClass = "ok";
|
|
|
}
|
|
|
} catch (Exception $e) {
|
|
|
|
|
|
/*
|
|
|
17. FEHLERBEHANDLUNG
|
|
|
Falls während einer laufenden Transaktion ein Fehler
|
|
|
aufgetreten ist, werden alle bis dahin vorgenommenen
|
|
|
Änderungen zurückgenommen.
|
|
|
Konkret: Zunächst prüft isset($pdo), ob die Variable $pdo
|
|
|
vorhanden ist und nicht null enthält. Anschließend prüft
|
|
|
$pdo->inTransaction(), ob eine Transaktion aktiv ist.
|
|
|
Nur wenn beide Bedingungen erfüllt sind, setzt $pdo->rollBack()
|
|
|
die Transaktion zurück. Durch die Kurzschlussauswertung des
|
|
|
UND-Operators && wird inTransaction() nur aufgerufen, wenn
|
|
|
$pdo tatsächlich vorhanden ist.
|
|
|
*/
|
|
|
if (isset($pdo) && $pdo->inTransaction()) {$pdo->rollBack();}
|
|
|
|
|
|
/*
|
|
|
Der Meldungstext wird zunächst unverändert gespeichert.
|
|
|
Die Umwandlung in eine sichere HTML-Ausgabe erfolgt
|
|
|
erst an der Ausgabestelle mit h().
|
|
|
*/
|
|
|
$message = "Fehler: ". $e->getMessage();
|
|
|
$messageClass = "err";
|
|
|
}
|
|
|
?>
|
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>KuReAr - Rechnung erfassen</title>
|
|
|
<link rel="stylesheet" href="KuReArRechnungen.css" >
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
<div class="box">
|
|
|
<div class="caption">
|
|
|
KuReAr - Eingabe einer Rechnung mit mehreren Positionen
|
|
|
</div>
|
|
|
<h2>Rechnung erfassen</h2>
|
|
|
<?php if ($message !== ""): ?>
|
|
|
|
|
|
<!--
|
|
|
Eine vorhandene Rückmeldung wird ausgegeben.
|
|
|
h($messageClass) schützt die Bezeichnung der CSS-Klasse.
|
|
|
h($message) schützt den eigentlichen Meldungstext.
|
|
|
-->
|
|
|
<div class="msg <?= h($messageClass) ?>"><?= h($message) ?>
|
|
|
</div>
|
|
|
<?php endif; ?>
|
|
|
|
|
|
<!--
|
|
|
FORMULAR 1: ANZAHL DER POSITIONEN ÄNDERN
|
|
|
Dieses Formular verändert nur die Anzahl der sichtbaren
|
|
|
Positionszeilen. Es speichert noch keine Rechnung.
|
|
|
-->
|
|
|
<form method="post" class="row">
|
|
|
<label for="posCount">Anzahl Positionen:</label>
|
|
|
<input type="number" id="posCount" name="posCount" min="1"
|
|
|
max="20" value="<?= (int)$posCount ?>">
|
|
|
<button type="submit" name="action" value="resize">
|
|
|
Maske anpassen
|
|
|
</button>
|
|
|
|
|
|
<!--
|
|
|
Bereits eingegebene Kopfdaten werden als verborgene
|
|
|
Formularfelder mitübertragen. Dadurch gehen diese
|
|
|
Werte beim Anpassen der Maske nicht verloren.
|
|
|
-->
|
|
|
<input type="hidden" name="KuNr"
|
|
|
value="<?= h($_POST["KuNr"] ?? "") ?>">
|
|
|
<input type="hidden" name="ReDatum"
|
|
|
value="<?= h($_POST["ReDatum"] ?? "") ?>">
|
|
|
<?php
|
|
|
|
|
|
/*
|
|
|
Auch die bereits eingegebenen Positionsdaten werden
|
|
|
in verborgenen Formularfeldern mitübertragen.
|
|
|
*/
|
|
|
for ($i = 1; $i <= $posCount; $i++) {
|
|
|
$a = h($_POST["ArtNr_$i"] ?? "");
|
|
|
$n = h($_POST["Anzahl_$i"] ?? "");
|
|
|
echo "<input type=\"hidden\" " . "name=\"ArtNr_$i\" "
|
|
|
. "value=\"$a\">";
|
|
|
echo "<input type=\"hidden\" "
|
|
|
. "name=\"Anzahl_$i\" " . "value=\"$n\">";
|
|
|
}
|
|
|
?>
|
|
|
</form>
|
|
|
|
|
|
<!--
|
|
|
FORMULAR 2: RECHNUNG SPEICHERN
|
|
|
Dieses Formular übermittelt die Kopfdaten und alle
|
|
|
Rechnungspositionen zur Speicherung.
|
|
|
-->
|
|
|
<form method="post">
|
|
|
|
|
|
<!--
|
|
|
Die aktuelle Anzahl der Positionszeilen wird
|
|
|
ebenfalls an das PHP-Programm übermittelt.
|
|
|
-->
|
|
|
<input type="hidden" name="posCount" value="<?= (int)$posCount ?>">
|
|
|
<div class="row">
|
|
|
<label for="KuNr">Kunde:</label><br>
|
|
|
|
|
|
<!--
|
|
|
Die Einträge der Auswahlliste werden aus den
|
|
|
zuvor geladenen Kundendaten erzeugt.
|
|
|
-->
|
|
|
<select id="KuNr" name="KuNr" required>
|
|
|
<option value="">
|
|
|
-- bitte wählen --
|
|
|
</option>
|
|
|
|
|
|
<?php foreach ($kunden as $k): ?>
|
|
|
|
|
|
<?php
|
|
|
$val = (int)$k["KuNr"];
|
|
|
|
|
|
/*
|
|
|
Eine bereits vorgenommene Kundenauswahl
|
|
|
wird beim erneuten Anzeigen des
|
|
|
Formulars beibehalten.
|
|
|
*/
|
|
|
$selectedKuNr = (int)($_POST["KuNr"] ?? 0);
|
|
|
?>
|
|
|
|
|
|
<option value="<?= $val ?>"
|
|
|
<?= $selectedKuNr === $val ? "selected" : "" ?>>
|
|
|
<?= h($k["Name"] . ", " . $k["Vorname"]
|
|
|
. " (KuNr " . $k["KuNr"] . ")" ) ?>
|
|
|
</option>
|
|
|
<?php endforeach; ?>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="row">
|
|
|
<label for="ReDatum">Rechnungsdatum:</label><br>
|
|
|
|
|
|
<!--
|
|
|
Das Eingabefeld type="date" unterstützt die
|
|
|
Eingabe eines Datums. Eine bereits übermittelte
|
|
|
Eingabe bleibt erhalten.
|
|
|
-->
|
|
|
<input type="date" id="ReDatum" name="ReDatum"
|
|
|
required value="<?= h($_POST["ReDatum"] ?? "") ?>">
|
|
|
</div>
|
|
|
<div class="row">
|
|
|
<table class="rel">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>PosNr</th>
|
|
|
<th>Artikel</th>
|
|
|
<th>Anzahl</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<?php
|
|
|
for ($i = 1;$i <= $posCount;$i++ ): ?>
|
|
|
<?php
|
|
|
|
|
|
/*
|
|
|
Bereits übermittelte Werte werden übernommen, damit sie bei
|
|
|
einer erneuten Anzeige nicht verloren gehen.
|
|
|
*/
|
|
|
$artSel =$_POST["ArtNr_$i"] ?? "";
|
|
|
$anzVal = $_POST["Anzahl_$i"] ?? "";
|
|
|
?>
|
|
|
<tr>
|
|
|
|
|
|
<!--
|
|
|
Laufende Nummer der Eingabezeile. Beim Speichern werden die
|
|
|
tatsächlich verwendeten Positionen lückenlos nummeriert.
|
|
|
-->
|
|
|
<td><?= $i ?></td>
|
|
|
<td>
|
|
|
|
|
|
<!--
|
|
|
Auswahlliste für den Artikel. Der Wert 0 kennzeichnet eine
|
|
|
unbenutzte Positionszeile.
|
|
|
-->
|
|
|
<select name="ArtNr_<?= $i ?>">
|
|
|
<option value="0"> -- leer --</option>
|
|
|
<?php foreach ($artikel as $a): ?>
|
|
|
<?php
|
|
|
$val = (int)$a["ArtNr"];
|
|
|
|
|
|
/*
|
|
|
Eine bereits getroffene Artikelauswahl wird wieder markiert.
|
|
|
*/
|
|
|
$selected = (string)$artSel !== "" && (int)$artSel === $val;
|
|
|
?>
|
|
|
<option value="<?= $val ?>" <?= $selected ? "selected" : "" ?>>
|
|
|
<?= h($a["Beschreibung"] . " (ArtNr " . $a["ArtNr"] . ", "
|
|
|
. $a["Preis"] . " EUR)") ?>
|
|
|
</option>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
</select>
|
|
|
</td>
|
|
|
<td>
|
|
|
|
|
|
<!--
|
|
|
Eingabe der Artikelmenge.
|
|
|
min="0" erlaubt den Wert 0 für eine unbenutzte Positionszeile.
|
|
|
Eine tatsächlich verwendete Position muss serverseitig eine
|
|
|
Anzahl größer als 0 besitzen.
|
|
|
-->
|
|
|
<input type="number" name="Anzahl_<?= $i ?>" min="0"
|
|
|
value="<?= h((string)$anzVal) ?>">
|
|
|
</td>
|
|
|
</tr>
|
|
|
<?php endfor; ?>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
<div class="actions">
|
|
|
|
|
|
<!-- Speichert die eingegebene Rechnung. -->
|
|
|
<button type="submit" name="action" value="save">
|
|
|
Rechnung speichern
|
|
|
</button>
|
|
|
|
|
|
<!--
|
|
|
Dieser Schalter sendet das Formular nicht ab,
|
|
|
sondern ruft die Startseite auf.
|
|
|
-->
|
|
|
<button type="button" onclick="location.href='/KuReArStart.php'">
|
|
|
Zurück
|
|
|
</button>
|
|
|
</div>
|
|
|
</form>
|
|
|
</div>
|
|
|
</body>
|
|
|
</html>
|
|
7.3.3 KuReArRechnungen ohne Kommentare |
|
Wegen dem Programmumfang und der zahlreichen Kommentare hier noch eine Fassung ohne Kommentare. |
|
|
<?php
|
|
|
|
|
|
$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";
|
|
|
$user = "root";
|
|
|
$pass = "";
|
|
|
|
|
|
$defaultPosCount = 3;
|
|
|
$posCount = $defaultPosCount;
|
|
|
$message = "";
|
|
|
$messageClass = "";
|
|
|
$kunden = [];
|
|
|
$artikel = [];
|
|
|
|
|
|
function h(string $s): string
|
|
|
{
|
|
|
return htmlspecialchars($s, ENT_QUOTES, "UTF-8");
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
$pdo = new PDO($dsn, $user, $pass,
|
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
|
|
|
|
$kunden = $pdo->query(
|
|
|
"SELECT KuNr, Name, Vorname FROM Kunden ORDER BY Name, Vorname"
|
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
|
|
$artikel = $pdo->query(
|
|
|
"SELECT ArtNr, Beschreibung, Preis FROM Artikel ORDER BY Beschreibung"
|
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
|
|
if (isset($_POST["posCount"])) {
|
|
|
$posCount = (int)$_POST["posCount"];
|
|
|
|
|
|
if ($posCount < 1) {
|
|
|
$posCount = 1;
|
|
|
}
|
|
|
if ($posCount > 20) {
|
|
|
$posCount = 20;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
$action = $_POST["action"] ?? "";
|
|
|
|
|
|
if ($action === "save") {
|
|
|
$KuNr = (int)($_POST["KuNr"] ?? 0);
|
|
|
$ReDatum = trim($_POST["ReDatum"] ?? "");
|
|
|
|
|
|
if ($KuNr <= 0 || $ReDatum === "") {
|
|
|
throw new Exception(
|
|
|
"Kunde und Rechnungsdatum müssen angegeben werden."
|
|
|
);
|
|
|
}
|
|
|
|
|
|
$datum = DateTime::createFromFormat("Y-m-d", $ReDatum);
|
|
|
|
|
|
if ($datum === false || $datum->format("Y-m-d") !== $ReDatum) {
|
|
|
throw new Exception("Das Rechnungsdatum ist ungültig.");
|
|
|
}
|
|
|
|
|
|
$positions = [];
|
|
|
|
|
|
for ($i = 1; $i <= $posCount; $i++) {
|
|
|
$ArtNr = (int)($_POST["ArtNr_$i"] ?? 0);
|
|
|
$Anzahl = (int)($_POST["Anzahl_$i"] ?? 0);
|
|
|
|
|
|
if ($ArtNr === 0 && $Anzahl === 0) {
|
|
|
continue;
|
|
|
}
|
|
|
if ($ArtNr <= 0 || $Anzahl <= 0) {
|
|
|
throw new Exception("Position $i ist nicht vollständig "
|
|
|
. "oder nicht gültig ausgefüllt."
|
|
|
);
|
|
|
}
|
|
|
|
|
|
$positions[] = [
|
|
|
"PosNr" => count($positions) + 1,
|
|
|
"ArtNr" => $ArtNr,
|
|
|
"Anzahl" => $Anzahl];
|
|
|
}
|
|
|
|
|
|
if (count($positions) === 0) {
|
|
|
throw new Exception(
|
|
|
"Mindestens eine Rechnungsposition muss ausgefüllt sein."
|
|
|
);
|
|
|
}
|
|
|
|
|
|
$pdo->beginTransaction();
|
|
|
|
|
|
$stmt = $pdo->query(
|
|
|
"SELECT IFNULL(MAX(ReNr), 0) + 1 AS NeueNr FROM RechKoepfe");
|
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
$NeueReNr = (int)$row["NeueNr"];
|
|
|
|
|
|
$stmtHead = $pdo->prepare("INSERT INTO RechKoepfe "
|
|
|
. "(ReNr, KuNr, ReDatum) VALUES (?, ?, ?)");
|
|
|
$stmtHead->execute([$NeueReNr, $KuNr, $ReDatum]);
|
|
|
|
|
|
$stmtPos = $pdo->prepare("INSERT INTO RechPos "
|
|
|
. "(ReNr, PosNr, ArtNr, Anzahl) VALUES (?, ?, ?, ?)");
|
|
|
|
|
|
foreach ($positions as $p) {
|
|
|
$stmtPos->execute(
|
|
|
[$NeueReNr, $p["PosNr"], $p["ArtNr"], $p["Anzahl"]]);
|
|
|
}
|
|
|
|
|
|
$pdo->commit();
|
|
|
header("Location: KuReAr_6_show.php?ReNr="
|
|
|
. urlencode((string)$NeueReNr));
|
|
|
exit;
|
|
|
}
|
|
|
|
|
|
if ($action === "resize") {
|
|
|
$message = "Maske angepasst: " . $posCount . " Position(en).";
|
|
|
$messageClass = "ok";
|
|
|
}
|
|
|
} catch (Exception $e) {
|
|
|
if (isset($pdo) && $pdo->inTransaction()) {
|
|
|
$pdo->rollBack();
|
|
|
}
|
|
|
$message = "Fehler: " . $e->getMessage();
|
|
|
$messageClass = "err";
|
|
|
}
|
|
|
?>
|
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>KuReAr - Rechnung erfassen</title>
|
|
|
<link rel="stylesheet" href="KuReArRechnungen.css">
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="box">
|
|
|
<div class="caption">
|
|
|
KuReAr - Eingabe einer Rechnung mit mehreren Positionen
|
|
|
</div>
|
|
|
<h2>Rechnung erfassen</h2>
|
|
|
|
|
|
<?php if ($message !== ""): ?>
|
|
|
<div class="msg <?= h($messageClass) ?>">
|
|
|
<?= h($message) ?>
|
|
|
</div>
|
|
|
<?php endif; ?>
|
|
|
|
|
|
<form method="post" class="row">
|
|
|
<label for="posCount">Anzahl Positionen:</label>
|
|
|
<input type="number" id="posCount" name="posCount" min="1"
|
|
|
max="20" value="<?= (int)$posCount ?>">
|
|
|
<button type="submit" name="action" value="resize">Maske anpassen</button>
|
|
|
<input type="hidden" name="KuNr"
|
|
|
value="<?= h($_POST["KuNr"] ?? "") ?>">
|
|
|
<input type="hidden" name="ReDatum"
|
|
|
value="<?= h($_POST["ReDatum"] ?? "") ?>">
|
|
|
|
|
|
<?php
|
|
|
for ($i = 1; $i <= $posCount; $i++) {
|
|
|
$a = h($_POST["ArtNr_$i"] ?? "");
|
|
|
$n = h($_POST["Anzahl_$i"] ?? "");
|
|
|
echo "<input type=\"hidden\" name=\"ArtNr_$i\" value=\"$a\">";
|
|
|
echo "<input type=\"hidden\" name=\"Anzahl_$i\" value=\"$n\">";
|
|
|
}
|
|
|
?>
|
|
|
</form>
|
|
|
|
|
|
<form method="post">
|
|
|
<input type="hidden" name="posCount" value="<?= (int)$posCount ?>">
|
|
|
|
|
|
<div class="row">
|
|
|
<label for="KuNr">Kunde:</label><br>
|
|
|
<select id="KuNr" name="KuNr" required>
|
|
|
<option value="">-- bitte wählen --</option>
|
|
|
<?php foreach ($kunden as $k):
|
|
|
$val = (int)$k["KuNr"];
|
|
|
$selectedKuNr = (int)($_POST["KuNr"] ?? 0);
|
|
|
?>
|
|
|
<option value="<?= $val ?>"
|
|
|
<?= $selectedKuNr === $val ? "selected" : "" ?>>
|
|
|
<?= h($k["Name"] . ", " . $k["Vorname"]
|
|
|
. " (KuNr " . $k["KuNr"] . ")") ?>
|
|
|
</option>
|
|
|
<?php endforeach; ?>
|
|
|
</select>
|
|
|
</div>
|
|
|
|
|
|
<div class="row">
|
|
|
<label for="ReDatum">Rechnungsdatum:</label><br>
|
|
|
<input type="date" id="ReDatum" name="ReDatum" required
|
|
|
value="<?= h($_POST["ReDatum"] ?? "") ?>">
|
|
|
</div>
|
|
|
|
|
|
<div class="row">
|
|
|
<table class="rel">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>PosNr</th>
|
|
|
<th>Artikel</th>
|
|
|
<th>Anzahl</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<?php for ($i = 1; $i <= $posCount; $i++):
|
|
|
$artSel = $_POST["ArtNr_$i"] ?? "";
|
|
|
$anzVal = $_POST["Anzahl_$i"] ?? "";
|
|
|
?>
|
|
|
<tr>
|
|
|
<td><?= $i ?></td>
|
|
|
<td>
|
|
|
<select name="ArtNr_<?= $i ?>">
|
|
|
<option value="0">-- leer --</option>
|
|
|
<?php foreach ($artikel as $a):
|
|
|
$val = (int)$a["ArtNr"];
|
|
|
$selected = (string)$artSel !== "" && (int)$artSel === $val;
|
|
|
?>
|
|
|
<option value="<?= $val ?>" <?= $selected ? "selected" : "" ?>>
|
|
|
<?= h($a["Beschreibung"] . " (ArtNr " . $a["ArtNr"]
|
|
|
. ", " . $a["Preis"] . " EUR)") ?>
|
|
|
</option>
|
|
|
<?php endforeach; ?>
|
|
|
</select>
|
|
|
</td>
|
|
|
<td>
|
|
|
<input type="number" name="Anzahl_<?= $i ?>" min="0"
|
|
|
value="<?= h((string)$anzVal) ?>">
|
|
|
</td>
|
|
|
</tr>
|
|
|
<?php endfor; ?>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<div class="actions">
|
|
|
<button type="submit" name="action" value="save">
|
|
|
Rechnung speichern
|
|
|
</button>
|
|
|
<button type="button" onclick="location.href='/KuReArStart.php'">
|
|
|
Zurück
|
|
|
</button>
|
|
|
</div>
|
|
|
</form>
|
|
|
</div>
|
|
|
</body>
|
|
|
</html>
|
|
7.3.4 KuReArRechnungen.css |
|
Hier die CSS-Datei für obiges Programm. Es ist recht ausführlich und - für Einsteiger - mit vielen Erläuterungen versehen. Wie immer gilt: Die Erläuterungen beziehen sich auf die nachfolgenden Anweisungen. |
|
|
/*
|
|
|
Diese Regel gilt für alle HTML-Elemente (Universalselektor).
|
|
|
box-sizing: border-box bewirkt, dass festgelegte Breiten
|
|
|
bereits die Innenabstände und Rahmen enthalten.
|
|
|
*/
|
|
|
* {box-sizing: border-box;}
|
|
|
|
|
|
/*
|
|
|
Grundlegende Gestaltung des sichtbaren Seitenbereichs.
|
|
|
margin: 0 entfernt den vom Browser standardmäßig
|
|
|
vorgegebenen Außenabstand.
|
|
|
padding: 20px erzeugt einen Abstand zwischen dem Rand
|
|
|
des Browserfensters und dem Inhalt der Webseite.
|
|
|
Als Schriftart wird Arial verwendet, ersatzweise eine andere
|
|
|
serifenlose Schriftart.
|
|
|
*/
|
|
|
body {margin: 0; padding: 20px; font-family: Arial, sans-serif;}
|
|
|
|
|
|
/*
|
|
|
Gestaltung der Überschrift zweiter Ordnung.
|
|
|
text-align: center zentriert den Text innerhalb des
|
|
|
Überschriftenbereichs.
|
|
|
margin legt die Außenabstände fest:
|
|
|
- oben: 10 Pixel
|
|
|
- rechts: 0
|
|
|
- unten: 14 Pixel
|
|
|
- links: 0
|
|
|
*/
|
|
|
h2 {margin: 10px 0 14px; text-align: center;}
|
|
|
|
|
|
/*
|
|
|
Äußerer Bereich der Eingabemaske.
|
|
|
width: 100% bewirkt, dass die Box grundsätzlich die
|
|
|
verfügbare Breite einnimmt.
|
|
|
max-width: 920px begrenzt ihre Breite jedoch auf
|
|
|
höchstens 920 Pixel. In einem schmalen Browserfenster kann
|
|
|
die Box dadurch kleiner werden. In einem breiteren Fenster
|
|
|
bleibt sie auf 920 Pixel begrenzt.
|
|
|
margin: 0 auto erzeugt oben und unten keinen zusätzlichen
|
|
|
Außenabstand. Der Wert auto für links und rechts zentriert
|
|
|
die Box horizontal.
|
|
|
*/
|
|
|
.box {width: 100%; max-width: 920px; margin: 0 auto;}
|
|
|
|
|
|
/*
|
|
|
Gestaltung der Beschriftung oberhalb der Eingabemaske.
|
|
|
color: #c00 legt einen kräftigen Rotton als Schriftfarbe
|
|
|
fest.
|
|
|
font-weight: 700 stellt den Text fett dar.
|
|
|
margin legt nur unterhalb der Beschriftung einen
|
|
|
Außenabstand von 8 Pixeln fest.
|
|
|
*/
|
|
|
.caption {margin: 0 0 8px; color: #c00; font-weight: 700;}
|
|
|
|
|
|
/*
|
|
|
Grundlegende Gestaltung der Meldungsbereiche.
|
|
|
margin erzeugt oberhalb und unterhalb der Meldung einen
|
|
|
Außenabstand von 10 Pixeln.
|
|
|
padding erzeugt auf allen Seiten einen Innenabstand von
|
|
|
10 Pixeln.
|
|
|
border erzeugt einen schwarzen, durchgezogenen Rahmen
|
|
|
mit einer Stärke von 2 Pixeln.
|
|
|
background-color legt Weiß als Hintergrundfarbe fest.
|
|
|
*/
|
|
|
.msg {margin: 10px 0;padding: 10px;border: 2px solid #000;
|
|
|
background-color: #fff;}
|
|
|
|
|
|
/*
|
|
|
Gestaltung einer Erfolgsmeldung.
|
|
|
Ein HTML-Element mit den beiden Klassen msg und ok erhält
|
|
|
eine dunkelgrüne Schriftfarbe.
|
|
|
Beispiel:
|
|
|
<div class="msg ok">Rechnung wurde gespeichert.</div>
|
|
|
*/
|
|
|
.msg.ok {color: #060;}
|
|
|
|
|
|
/*
|
|
|
Gestaltung einer Fehlermeldung.
|
|
|
Ein HTML-Element mit den beiden Klassen msg und err erhält
|
|
|
eine dunkelrote Schriftfarbe.
|
|
|
Beispiel:
|
|
|
<div class="msg err">Fehler beim Speichern.</div>
|
|
|
*/
|
|
|
.msg.err {color: #900;}
|
|
|
|
|
|
/*
|
|
|
Gestaltung aller Beschriftungen von Formularfeldern.
|
|
|
font-weight: 700 stellt die Beschriftungen fett dar.
|
|
|
*/
|
|
|
label {font-weight: 700;}
|
|
|
|
|
|
/*
|
|
|
Abstand zwischen den einzelnen Bereichen der Eingabemaske.
|
|
|
Oberhalb und unterhalb jedes Bereichs wird ein
|
|
|
Außenabstand von 10 Pixeln erzeugt.
|
|
|
*/
|
|
|
.row {margin: 10px 0;}
|
|
|
|
|
|
/*
|
|
|
Gestaltung der Tabelle mit den Rechnungspositionen.
|
|
|
border-collapse: collapse führt die Rahmen benachbarter
|
|
|
Zellen zu einer gemeinsamen Linie zusammen.
|
|
|
width: 100% bewirkt, dass die Tabelle die gesamte
|
|
|
verfügbare Breite einnimmt.
|
|
|
border erzeugt einen schwarzen Außenrahmen mit einer
|
|
|
Stärke von 3 Pixeln.
|
|
|
font-size legt die Schriftgröße auf 15 Pixel fest.
|
|
|
*/
|
|
|
table.rel {width: 100%; border: 3px solid #000;
|
|
|
border-collapse: collapse; font-size: 15px;}
|
|
|
|
|
|
/*
|
|
|
Gemeinsame Gestaltung der Kopf- und Datenzellen.
|
|
|
border erzeugt um jede Zelle einen schwarzen,
|
|
|
durchgezogenen Rahmen mit einer Stärke von 2 Pixeln.
|
|
|
padding legt den Innenabstand fest:
|
|
|
- oben und unten: 6 Pixel
|
|
|
- links und rechts: 10 Pixel
|
|
|
text-align: left richtet den Inhalt linksbündig aus.
|
|
|
background-color legt Weiß als Hintergrundfarbe fest.
|
|
|
*/
|
|
|
Zur Erinnerung: table.rel ist ein zusammengesetzer Selektor,
|
|
|
der alle HTML-Elemente auswählt, die eine table-Element sind
|
|
|
und gleichzeitg die CSS-Klasse rel besitzen.
|
|
|
table.rel th,
|
|
|
table.rel td {padding: 6px 10px; border: 2px solid #000;
|
|
|
background-color: #fff;text-align: left;}
|
|
|
|
|
|
/*
|
|
|
Die Beschriftungen der Spaltenüberschriften werden fett
|
|
|
dargestellt.
|
|
|
*/
|
|
|
table.rel th {font-weight: 700;}
|
|
|
|
|
|
/*
|
|
|
Bereich mit den Schaltflächen unterhalb des Formulars.
|
|
|
margin-top erzeugt oberhalb des Bereichs einen
|
|
|
Außenabstand von 12 Pixeln.
|
|
|
display: flex aktiviert das Flexbox-Verfahren. Die
|
|
|
Schaltflächen werden dadurch nebeneinander angeordnet.
|
|
|
flex-wrap: wrap erlaubt einen Zeilenumbruch, falls auf
|
|
|
einem kleinen Bildschirm nicht genügend Platz vorhanden ist.
|
|
|
gap legt zwischen den Schaltflächen einen Abstand von
|
|
|
10 Pixeln fest.
|
|
|
*/
|
|
|
.actions {display: flex; flex-wrap: wrap; gap: 10px;
|
|
|
margin-top: 12px;}
|
|
|
|
|
|
/*
|
|
|
Gestaltung aller Eingabefelder vom Typ number.
|
|
|
Die Eingabefelder für die Anzahl der Positionen und für
|
|
|
die Artikelmengen erhalten eine Breite von 90 Pixeln.
|
|
|
*/
|
|
|
input[type="number"] {width: 90px;}
|
|
|
|
|
|
/*
|
|
|
Gestaltung der Auswahllisten.
|
|
|
width: 100% bewirkt, dass eine Auswahlliste grundsätzlich
|
|
|
die verfügbare Breite einnimmt.
|
|
|
max-width: 100% verhindert, dass sie über den umgebenden
|
|
|
Bereich hinausragt.
|
|
|
*/
|
|
|
select {width: 100%; max-width: 100%;}
|
|
|
|
|
|
/*
|
|
|
Grundlegende Gestaltung aller Schaltflächen.
|
|
|
padding legt den Innenabstand fest:
|
|
|
- oben und unten: 6 Pixel
|
|
|
- links und rechts: 10 Pixel
|
|
|
border erzeugt einen schwarzen, durchgezogenen Rahmen
|
|
|
mit einer Stärke von 2 Pixeln.
|
|
|
background-color legt einen hellgrauen Hintergrund fest.
|
|
|
cursor: pointer zeigt beim Berühren der Schaltfläche mit
|
|
|
dem Mauszeiger einen Handzeiger an.
|
|
|
*/
|
|
|
button {padding: 6px 10px; border: 2px solid #000;
|
|
|
background-color: #eee; cursor: pointer;}
|
|
|
|
|
|
/*
|
|
|
Gestaltung einer Schaltfläche, wenn sich der Mauszeiger
|
|
|
darüber befindet.
|
|
|
Die etwas dunklere Hintergrundfarbe gibt dem Benutzer
|
|
|
eine optische Rückmeldung.
|
|
|
*/
|
|
|
button:hover {background-color: #ddd;}
|
|
|
|
|
|
/*
|
|
|
Auf kleinen Bildschirmen kann die Tabelle breiter als
|
|
|
das Browserfenster werden. Dieser Medienausdruck wird
|
|
|
wirksam, sobald das Browserfenster höchstens 700 Pixel
|
|
|
breit ist.
|
|
|
Die äußere Box darf dann horizontal gescrollt werden.
|
|
|
Dadurch bleiben alle Spalten der Tabelle erreichbar.
|
|
|
*/
|
|
|
@media (max-width: 700px) {.box {overflow-x: auto;}
|
|
|
table.rel {min-width: 620px;}
|
|
|
}
|
|
7.3.5 Programmbeschreibung |
|
Das Programm dient dazu, eine neue Rechnung mit mehreren Positionen in der Datenbank KuReAr zu erfassen. Dabei werden sowohl der Rechnungskopf mit Kunde und Rechnungsdatum als auch die zugehörigen Rechnungspositionen mit Artikel und Anzahl gespeichert. |
|
Der Zugriff auf die Datenbank erfolgt über PDO (PHP Data Objects). Zu Beginn werden die Verbindungsparameter DSN, Benutzername und Passwort festgelegt. Anschließend wird eine Verbindung zur Datenbank hergestellt. Die Variable $posCount steuert die Anzahl der Eingabezeilen für die Rechnungspositionen. Standardmäßig werden drei Positionszeilen angezeigt. Der Benutzer kann diese Anzahl über ein Formular verändern. |
|
Nach dem Verbindungsaufbau werden zunächst die benötigten Stammdaten geladen: |
|
- Kunden aus der Relation Kunden
- Artikel aus der Relation Artikel
|
|
Diese Daten werden später in Auswahlfeldern (Dropdown-Menüs) angezeigt. Das Programm verwendet zwei HTML-Formulare: |
|
- ein Formular zur Anpassung der Anzahl der Positionszeilen
- ein Formular zur Eingabe und Speicherung der Rechnung
|
|
Beim Absenden eines Formulars wird mithilfe der Variablen $action unterschieden, welche Aktion ausgeführt werden soll: |
|
- "resize": Die Anzahl der Positionszeilen wird angepasst.
- "save": Die eingegebenen Rechnungsdaten werden geprüft und gespeichert.
|
|
Speichern der Rechnung |
|
Beim Speichern werden zunächst die Eingabedaten geprüft. Es muss ein Kunde ausgewählt und ein Rechnungsdatum angegeben worden sein. Anschließend werden die eingegebenen Positionsdaten verarbeitet. Vollständig leere Positionszeilen werden ignoriert. Übernommen werden nur Positionen mit einer gültigen Artikelnummer und einer gültigen Anzahl. Die Positionsnummern (PosNr) werden automatisch fortlaufend vergeben. |
|
Das Programm prüft außerdem, ob mindestens eine gültige Rechnungsposition vorhanden ist. Andernfalls wird eine Fehlermeldung ausgegeben. |
|
Für die neue Rechnung wird eine neue Rechnungsnummer (ReNr) ermittelt. Dazu dient folgende SQL-Abfrage: |
|
|
SELECT IFNULL(MAX(ReNr), 0) + 1
|
|
MAX(ReNr) ermittelt die bisher höchste Rechnungsnummer. Falls noch keine Rechnung vorhanden ist, liefert IFNULL() stattdessen den Wert 0. Durch die Addition von 1 entsteht die neue Rechnungsnummer. Dieses Verfahren eignet sich für das lokale Lernsystem. Bei einer Anwendung mit mehreren gleichzeitig arbeitenden Benutzern wäre dagegen eine automatisch erzeugte Rechnungsnummer, beispielsweise mit AUTO_INCREMENT, richtig. |
|
Transaktion |
|
Das Speichern erfolgt innerhalb einer Transaktion. Zunächst wird der Rechnungskopf in die Relation RechKoepfe eingefügt. Danach werden die zugehörigen Rechnungspositionen in der Relation RechPos gespeichert. Die Transaktion stellt sicher, dass entweder der Rechnungskopf und sämtliche Positionen vollständig gespeichert werden oder im Fehlerfall keine dieser Änderungen dauerhaft übernommen wird. Bei erfolgreicher Ausführung wird die Transaktion mit commit() abgeschlossen. Tritt beim Speichern ein Fehler auf, werden die bereits ausgeführten Änderungen mit rollBack() zurückgenommen. |
|
Weiterleitung und Fehlerbehandlung |
|
Nach erfolgreichem Speichern wird der Benutzer automatisch zur Anzeige der neu angelegten Rechnung weitergeleitet: |
|
|
header(
|
|
|
"Location: KuReAr_6_show.php?ReNr=" . $ReNr
|
|
|
);
|
|
Die Rechnungsnummer wird dabei als URL-Parameter an KuReAr_6_show.php übergeben. Dieses Programm lädt den Rechnungskopf und die zugehörigen Rechnungspositionen aus der Datenbank und zeigt sie an. |
|
Tritt ein Fehler auf, wird eine entsprechende Fehlermeldung ausgegeben. Falls bereits eine Transaktion begonnen wurde, wird sie mit rollBack() zurückgesetzt. |
|
Benutzeroberfläche |
|
Die Eingabe erfolgt über eine übersichtlich aufgebaute HTML-Seite: |
|
- Auswahl des Kunden über ein Dropdown-Menü
- Eingabe des Rechnungsdatums
- Relation zur Eingabe mehrerer Rechnungspositionen
- Schaltfläche "Maske anpassen" zur Veränderung der Anzahl der Positionszeilen
- Schaltfläche "Rechnung speichern" zum Starten des Speichervorgangs
- Schaltfläche "Zurück" für die Rückkehr zur Startseite
- Ausgabe von Fehlermeldungen oberhalb des Formulars
|
|
Nach erfolgreichem Speichern wird die neue Rechnung auf einer eigenen Seite angezeigt. |
|
Sicherheitsaspekte |
|
Die Datenbankzugriffe erfolgen mit Prepared Statements. Dabei werden die SQL-Anweisungen und die zu verarbeitenden Werte getrennt an das Datenbanksystem übergeben. Dies schützt insbesondere vor SQL-Injection. |
|
Die Funktion htmlspecialchars() sorgt für eine sichere Ausgabe von Daten innerhalb der HTML-Seite. Sonderzeichen in HTML werden in entsprechende HTML-Zeichenreferenzen umgewandelt. Dadurch wird verhindert, dass ausgegebene Daten als unerwünschter HTML- oder JavaScript-Code interpretiert werden. Die Prüfung und Typumwandlung der Eingabewerte stellt zusätzlich sicher, dass nur geeignete Daten verarbeitet werden. Vgl. auch den Anhang. |
|
Zusammenfassung |
|
Das Programm zeigt den typischen Ablauf einer Datenbankanwendung mit PHP und PDO: |
|
- Laden von Stammdaten
- dynamische Anpassung eines Formulars
- Prüfung von Benutzereingaben
- Verarbeitung mehrerer zusammengehöriger Datensätze
- Verwendung von Prepared Statements
- Einsatz einer Transaktion
- strukturierte Fehlerbehandlung
- Weiterleitung zur Anzeige der gespeicherten Daten
|
|
Nun die weiteren Programme. |
|
7.4 Verbindung zur Datenbank |
|
7.4.1 KuReAr_5.php |
|
|
<?php
|
|
|
|
|
|
/*
|
|
|
1. VERBINDUNGSDATEN ZUR DATENBANK
|
|
|
Der DSN (Data Source Name) beschreibt die Verbindung:
|
|
|
- mysql bezeichnet den verwendeten PDO-Treiber.
|
|
|
- host=localhost bedeutet, dass der Datenbankserver auf
|
|
|
demselben Rechner läuft.
|
|
|
- dbname=KuReAr bezeichnet die verwendete Datenbank.
|
|
|
- charset=utf8mb4 legt den Zeichensatz der Verbindung fest.
|
|
|
*/
|
|
|
$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";
|
|
|
$user = "root";
|
|
|
$pass = "";
|
|
|
|
|
|
//Prüfen der Eingabe
|
|
|
function h(mixed $value): string
|
|
|
{
|
|
|
return htmlspecialchars((string)$value,ENT_QUOTES,"UTF-8");
|
|
|
}
|
|
|
|
|
|
//Start des try/catch - Abschnitts
|
|
|
try {
|
|
|
|
|
|
/*
|
|
|
3. VERBINDUNG ZUR DATENBANK HERSTELLEN
|
|
|
new PDO() erzeugt ein Objekt der Klasse PDO. Über dieses
|
|
|
Objekt werden anschließend die SQL-Anweisungen ausgeführt.
|
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION bewirkt,
|
|
|
dass PDO bei einem Datenbankfehler eine PDOException
|
|
|
auslöst. Diese wird im catch-Abschnitt behandelt.
|
|
|
*/
|
|
|
$pdo = new PDO($dsn,$user,$pass,
|
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
|
|
|
|
/*
|
|
|
4. DATEN AUS DEN VIER RELATIONEN ABFRAGEN
|
|
|
Die SELECT-Anweisung verbindet folgende Relationen:
|
|
|
- Kunden
|
|
|
- RechKoepfe
|
|
|
- RechPos
|
|
|
- Artikel
|
|
|
Dabei werden für jede Rechnungsposition Daten über den
|
|
|
Kunden, den Rechnungskopf, die Rechnungsposition und den
|
|
|
zugehörigen Artikel ausgegeben.
|
|
|
Die Aliasnamen verkürzen die Schreibweise:
|
|
|
- k steht für Kunden
|
|
|
- rk steht für RechKoepfe
|
|
|
- rp steht für RechPos
|
|
|
- a steht für Artikel
|
|
|
k.Name AS KuName gibt der ausgewählten Spalte Name im
|
|
|
Abfrageergebnis den eindeutigen Namen KuName.
|
|
|
DATE_FORMAT() formatiert das Rechnungsdatum in dem in
|
|
|
Deutschland üblichen Format Tag.Monat.Jahr.
|
|
|
Zu den Inner Joins siehe auch den Anhang, sowie grundsätzlich und
|
|
|
ausführlich, die Texte zu Relationalen Datenbanken auf diesen
|
|
|
Webseiten
|
|
|
*/
|
|
|
$sql = "SELECT k.KuNr, k.Name AS KuName, rk.ReNr,
|
|
|
DATE_FORMAT(rk.ReDatum, '%d.%m.%Y') AS RechnDatum,
|
|
|
rp.PosNr, a.ArtNr, a.Beschreibung, a.Preis, rp.Anzahl
|
|
|
FROM RechKoepfe AS rk
|
|
|
INNER JOIN Kunden AS k ON rk.KuNr = k.KuNr
|
|
|
INNER JOIN RechPos AS rp ON rk.ReNr = rp.ReNr
|
|
|
INNER JOIN Artikel AS a ON rp.ArtNr = a.ArtNr
|
|
|
ORDER BY rk.ReNr, rp.PosNr";
|
|
|
|
|
|
/*
|
|
|
5. SQL-ANWEISUNG AUSFÜHREN
|
|
|
query() führt die SELECT-Anweisung aus.
|
|
|
Da die SQL-Anweisung keine Benutzereingaben enthält, ist
|
|
|
hier kein Prepared Statement erforderlich.
|
|
|
fetchAll(PDO::FETCH_ASSOC) liest alle Ergebniszeilen und
|
|
|
liefert sie als Array. Jede Zeile ist dabei ein assoziatives
|
|
|
Array, dessen Schlüssel den ausgewählten Spaltennamen entsprechen.
|
|
|
*/
|
|
|
$rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
|
|
} catch (PDOException $e) {
|
|
|
|
|
|
/*
|
|
|
6. FEHLERBEHANDLUNG
|
|
|
Falls beim Aufbau der Verbindung oder beim Ausführen der
|
|
|
SQL-Anweisung ein Datenbankfehler auftritt, wird das
|
|
|
Programm mit die() beendet.
|
|
|
h() schützt die Fehlermeldung bei der Ausgabe im Browser.
|
|
|
*/
|
|
|
die("DB-Fehler: " . h($e->getMessage()));
|
|
|
}
|
|
|
?>
|
|
|
|
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<!--
|
|
|
UTF-8 ermöglicht unter anderem die korrekte Darstellung
|
|
|
von Umlauten und Sonderzeichen.
|
|
|
-->
|
|
|
<meta charset="utf-8">
|
|
|
<title>Gesamte Datenbank KuReAr</title>
|
|
|
<link rel="stylesheet" href="KuReAr_5.css">
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="box">
|
|
|
|
|
|
<!--
|
|
|
Zusätzliche Beschriftung oberhalb der Überschrift.
|
|
|
-->
|
|
|
<div class="caption">Und jetzt alle vier Relationen:</div>
|
|
|
<h2>Die gesamte Datenbank Kunden - Rechnungen - Artikel</h2>
|
|
|
|
|
|
<!--
|
|
|
Die Ergebnismenge der SQL-Abfrage wird als HTML-Tabelle
|
|
|
ausgegeben.
|
|
|
Jede Ergebniszeile entspricht einer Rechnungsposition.
|
|
|
Die Angaben zum Kunden und zum Rechnungskopf wiederholen
|
|
|
sich deshalb, wenn eine Rechnung mehrere Positionen
|
|
|
enthält. Dies wurde hier akzeptiert.
|
|
|
Start der Tabellendarstellung:
|
|
|
-->
|
|
|
<table class="rel">
|
|
|
<thead>
|
|
|
|
|
|
<!-- Überschriften der Tabellenspalten -->
|
|
|
<tr>
|
|
|
<th>KuNr</th>
|
|
|
<th>KuName</th>
|
|
|
<th>ReNr</th>
|
|
|
<th>RechnDatum</th>
|
|
|
<th>PosNr</th>
|
|
|
<th>ArtNr</th>
|
|
|
<th>Beschreibung</th>
|
|
|
<th>Preis</th>
|
|
|
<th>Anzahl</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
|
|
|
<!--Prüfen, ob Anzahl der Datensätze gleich Null-->
|
|
|
<?php if (count($rows) === 0): ?>
|
|
|
|
|
|
<!--
|
|
|
Dieser Fall tritt ein, wenn die Abfrage keine
|
|
|
Datensätze geliefert hat.
|
|
|
colspan="9", weil der Text über die ganze Tabellenbreite geht
|
|
|
-->
|
|
|
<tr>
|
|
|
<td colspan="9">Keine Daten vorhanden.</td>
|
|
|
</tr>
|
|
|
|
|
|
<!--else von obigem if und Schleifenbeginn über die Eergebnisse-->
|
|
|
<?php else: ?>
|
|
|
<?php foreach ($rows as $r): ?>
|
|
|
|
|
|
<!--
|
|
|
Für jede Ergebniszeile wird eine Zeile der
|
|
|
HTML-Tabelle erzeugt.
|
|
|
Alle Werte werden vor der Ausgabe mit h()
|
|
|
geschützt.
|
|
|
-->
|
|
|
<tr>
|
|
|
<td><?= h($r["KuNr"]) ?></td>
|
|
|
<td><?= h($r["KuName"]) ?></td>
|
|
|
<td><?= h($r["ReNr"]) ?></td>
|
|
|
<td><?= h($r["RechnDatum"]) ?></td>
|
|
|
<td><?= h($r["PosNr"]) ?></td>
|
|
|
<td><?= h($r["ArtNr"]) ?></td>
|
|
|
<td><?= h($r["Beschreibung"]) ?></td>
|
|
|
<td><?= h($r["Preis"]) ?></td>
|
|
|
<td><?= h($r["Anzahl"]) ?></td>
|
|
|
</tr>
|
|
|
<?php endforeach; ?>
|
|
|
<?php endif; ?>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</body>
|
|
|
</html>
|
|
7.4.2 Anmerkungen zum Programm |
|
- Die mehrfachen Aufrufe von htmlspecialchars() (vgl. Anhang) wurden durch die Hilfsfunktion h() ersetzt.
- Die Abfrage verwendet einen sog. Inner Join. Damit erscheinen nur Kunden mit Rechnungen und nur Rechnungen mit vorhandenen Positionen und zugeordneten Artikeln. Vgl. Anhang.
- Dies ist eine Demonstration der Verknüpfung der vier Relationen zu Demonstrationszwecken, weshalb die redundante Darstellung bei den Rechnungspositionen akzeptiert wurde.
|
|
7.4.3 KuReAr_5.css |
|
|
/*
|
|
|
Diese Regel gilt für alle HTML-Elemente (Universalselektor *).
|
|
|
box-sizing: border-box bewirkt, dass festgelegte Breiten
|
|
|
bereits die Innenabstände und Rahmen enthalten.
|
|
|
Dadurch wird insbesondere verhindert, dass die äußere Box
|
|
|
durch ihr padding breiter wird als vorgesehen.
|
|
|
*/
|
|
|
* {box-sizing: border-box;}
|
|
|
|
|
|
/*
|
|
|
1. GESTALTUNG DER GESAMTEN SEITE
|
|
|
Als bevorzugte Schriftart wird Arial verwendet. Falls Arial
|
|
|
nicht verfügbar ist, versucht der Browser Helvetica und
|
|
|
anschließend eine andere serifenlose Schriftart zu verwenden.
|
|
|
background-color legt einen sehr hellen Grauton als Hintergrundfarbe
|
|
|
der gesamten Webseite fest.
|
|
|
margin: 0 und padding: 0 entfernen die vom Browser standardmäßig
|
|
|
vorgesehenen Außen- und Innenabstände.
|
|
|
*/
|
|
|
body {margin: 0; padding: 0; background-color: #f5f5f5;
|
|
|
font-family: Arial, Helvetica, sans-serif;}
|
|
|
|
|
|
/*
|
|
|
2. GEMEINSAMER CONTAINER FÜR DIE AUSGABE
|
|
|
width: 100% bewirkt, dass die Box in einem kleinen
|
|
|
Browserfenster die verfügbare Breite einnimmt.
|
|
|
max-width: 940px begrenzt die Gesamtbreite auf 940 Pixel.
|
|
|
Dieser Wert setzt sich gedanklich aus der ursprünglich
|
|
|
festgelegten Breite von 900 Pixeln und den Innenabständen von
|
|
|
jeweils 20 Pixeln zusammen.
|
|
|
margin: 20px auto erzeugt oben und unten einen Außenabstand
|
|
|
von 20 Pixeln. Der Wert auto zentriert die Box horizontal.
|
|
|
padding erzeugt innerhalb der Box auf allen Seiten einen
|
|
|
Abstand von 20 Pixeln.
|
|
|
box-shadow erzeugt einen schwachen Schatten um die Box.
|
|
|
rgba() legt dabei Schwarz mit einer Deckkraft von 10 Prozent
|
|
|
fest.
|
|
|
overflow-x: auto erzeugt bei Bedarf eine horizontale
|
|
|
Bildlaufleiste. Dies ist hier wichtig, weil die Relation neun
|
|
|
Spalten besitzt.
|
|
|
*/
|
|
|
.box {width: 100%; max-width: 940px; margin: 20px auto;
|
|
|
padding: 20px; overflow-x: auto;background-color: #fff;
|
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
3. ROTER HINWEISTEXT OBERHALB DER ÜBERSCHRIFT
|
|
|
color legt Rot als Schriftfarbe fest.
|
|
|
font-weight: 700 stellt den Text fett dar.
|
|
|
margin-bottom erzeugt unterhalb des Hinweistextes einen
|
|
|
Außenabstand von 10 Pixeln.
|
|
|
*/
|
|
|
.caption {margin-bottom: 10px; color: red; font-weight: 700;}
|
|
|
|
|
|
/*
|
|
|
4. ÜBERSCHRIFT
|
|
|
text-align: center richtet die Überschrift horizontal zentriert aus.
|
|
|
margin-top: 0 entfernt den standardmäßigen oberen Außenabstand der
|
|
|
Überschrift.
|
|
|
*/
|
|
|
h2 {margin-top: 0; text-align: center;}
|
|
|
|
|
|
/*
|
|
|
5. RELATION AUSGEBEN / DARSTELLEN
|
|
|
width: 100% bewirkt, dass die Relation die gesamte Breite
|
|
|
ihres umgebenden Bereichs einnimmt.
|
|
|
border-collapse: collapse führt die Rahmen benachbarter
|
|
|
Zellen zu jeweils einer gemeinsamen Linie zusammen.
|
|
|
margin-top erzeugt oberhalb der Relation einen Außenabstand
|
|
|
von 15 Pixeln.
|
|
|
min-width verhindert, dass die neun Spalten auf kleinen
|
|
|
Bildschirmen zu stark zusammengedrückt werden. Bei Bedarf
|
|
|
kann innerhalb der Box horizontal gescrollt werden.
|
|
|
*/
|
|
|
.rel {width: 100%; min-width: 850px; margin-top: 15px;
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
/*
|
|
|
6. TABELLENKOPF
|
|
|
Der Tabellenkopf erhält eine hellgraue Hintergrundfarbe.
|
|
|
Dadurch lässt er sich optisch von den Datenzeilen unterscheiden.
|
|
|
*/
|
|
|
.rel thead {background-color: #e0e0e0;}
|
|
|
|
|
|
/*
|
|
|
7. KOPF- UND DATENZELLEN
|
|
|
border erzeugt um jede Zelle einen grauen, durchgezogenen
|
|
|
Rahmen mit einer Stärke von einem Pixel.
|
|
|
padding legt den Innenabstand fest:
|
|
|
- oben und unten: 6 Pixel
|
|
|
- links und rechts: 8 Pixel
|
|
|
text-align: left richtet die Inhalte zunächst grundsätzlich
|
|
|
linksbündig aus.
|
|
|
*/
|
|
|
.rel th, .rel td {padding: 6px 8px; border: 1px solid #999;
|
|
|
text-align: left;}
|
|
|
|
|
|
/*
|
|
|
8. RECHTSBÜNDIGE AUSRICHTUNG DER ZAHLEN
|
|
|
:nth-child() wählt eine Zelle anhand ihrer Position innerhalb der
|
|
|
Tabellenzeile aus.
|
|
|
Rechtsbündig ausgegeben werden:
|
|
|
- 1. Spalte: KuNr
|
|
|
- 3. Spalte: ReNr
|
|
|
- 5. Spalte: PosNr
|
|
|
- 6. Spalte: ArtNr
|
|
|
- 8. Spalte: Preis
|
|
|
- 9. Spalte: Anzahl
|
|
|
Die übrigen Spalten enthalten Texte oder ein Datum und
|
|
|
bleiben linksbündig.
|
|
|
*/
|
|
|
.rel td:nth-child(1),
|
|
|
.rel td:nth-child(3),
|
|
|
.rel td:nth-child(5),
|
|
|
.rel td:nth-child(6),
|
|
|
.rel td:nth-child(8),
|
|
|
.rel td:nth-child(9) {text-align: right;}
|
|
|
|
|
|
/*
|
|
|
9. HERVORHEBUNG EINER DATENZEILE
|
|
|
:hover wird wirksam, wenn sich der Mauszeiger über einer
|
|
|
Tabellenzeile befindet. Die Zeile erhält dann eine sehr helle blaue
|
|
|
Hintergrundfarbe. Dies erleichtert das Lesen umfangreicher
|
|
|
Ergebnismengen.
|
|
|
Durch tbody wird sichergestellt, dass dieser Effekt nur für
|
|
|
die Datenzeilen und nicht für den Tabellenkopf gilt.
|
|
|
*/
|
|
|
.rel tbody tr:hover {background-color: #f0f8ff;}
|
|
|
|
|
|
/*
|
|
|
10. ANPASSUNG AN KLEINE BILDSCHIRME
|
|
|
Sobald das Browserfenster höchstens 980 Pixel breit ist,
|
|
|
erhält die Box links und rechts einen kleinen Außenabstand.
|
|
|
Die Funktion calc() zieht insgesamt 20 Pixel von der
|
|
|
verfügbaren Fensterbreite ab.
|
|
|
*/
|
|
|
@media (max-width: 980px) {
|
|
|
.box {width: calc(100% - 20px); margin: 10px;}}
|
|
7.5 Anzeigen einer Rechnung |
|
7.5.1 KuReAr_6_show.php |
|
|
<?php
|
|
|
// Anzeige einer Rechnung mit Rechnungskopf und Rechnungspositionen
|
|
|
|
|
|
/*
|
|
|
1. VERBINDUNGSDATEN ZUR DATENBANK
|
|
|
Der DSN (Data Source Name) enthält:
|
|
|
- mysql: verwendeter PDO-Treiber
|
|
|
- host=localhost: Datenbankserver auf demselben Rechner
|
|
|
- dbname=KuReAr: Name der Datenbank
|
|
|
- charset=utf8mb4: Zeichensatz der Verbindung
|
|
|
*/
|
|
|
$dsn = "mysql:host=localhost;dbname=KuReAr;charset=utf8mb4";
|
|
|
$user = "root";
|
|
|
$pass = "";
|
|
|
|
|
|
/*
|
|
|
2. HILFSFUNKTION FÜR DIE HTML-AUSGABE
|
|
|
htmlspecialchars() wandelt Zeichen mit einer besonderen
|
|
|
Bedeutung in HTML in entsprechende HTML-Zeichenreferenzen um.
|
|
|
Dadurch werden die ausgegebenen Daten nicht als HTML-Code
|
|
|
interpretiert. Vgl. auch den Anhang.
|
|
|
Der Datentyp mixed erlaubt die Übergabe von Zeichenketten,
|
|
|
Zahlen und NULL-Werten. Innerhalb der Funktion wird der Wert
|
|
|
in eine Zeichenkette umgewandelt.
|
|
|
*/
|
|
|
function h(mixed $value): string
|
|
|
{
|
|
|
return htmlspecialchars((string)$value,ENT_QUOTES,"UTF-8");
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
3. RECHNUNGSNUMMER AUS DER URL ÜBERNEHMEN
|
|
|
Das Programm erwartet eine URL in dieser Form:
|
|
|
KuReAr_6_show.php?ReNr=1001
|
|
|
$_GET["ReNr"] enthält dann den Wert 1001.
|
|
|
Der Null-Koaleszenzoperator ?? liefert 0, falls der Parameter
|
|
|
ReNr nicht vorhanden ist. Durch (int) wird der übergebene Wert
|
|
|
in eine ganze Zahl umgewandelt.
|
|
|
*/
|
|
|
$ReNr = (int)($_GET["ReNr"] ?? 0);
|
|
|
|
|
|
// Eine Rechnungsnummer muss größer als 0 sein.
|
|
|
// Ist dies nicht der Fall, wird das Programm mit die() beendet.
|
|
|
if ($ReNr <= 0) {
|
|
|
die("Fehler: ReNr fehlt oder ist ungültig.");
|
|
|
}
|
|
|
try {
|
|
|
|
|
|
/*
|
|
|
4. VERBINDUNG ZUR DATENBANK HERSTELLEN
|
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION bewirkt,
|
|
|
dass PDO bei einem Datenbankfehler eine PDOException
|
|
|
auslöst.
|
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC legt
|
|
|
fest, dass gelesene Datensätze standardmäßig als
|
|
|
assoziative Arrays zurückgegeben werden.
|
|
|
*/
|
|
|
$pdo = new PDO($dsn,$user,$pass,[
|
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
|
|
);
|
|
|
|
|
|
/*
|
|
|
5. RECHNUNGSKOPF LADEN
|
|
|
Die Relation RechKoepfe enthält die allgemeinen Angaben
|
|
|
zur Rechnung:
|
|
|
- Rechnungsnummer
|
|
|
- Rechnungsdatum
|
|
|
- Kundennummer
|
|
|
Über die Kundennummer wird die Relation Kunden verbunden.
|
|
|
Dadurch können zusätzlich Name und Vorname des Kunden ausgegeben
|
|
|
werden. DATE_FORMAT() erzeugt aus dem gespeicherten Datum eine
|
|
|
Darstellung in der Form Tag.Monat.Jahr.
|
|
|
*/
|
|
|
$sqlHead = "SELECT rk.ReNr, DATE_FORMAT(rk.ReDatum, '%d.%m.%Y')
|
|
|
AS ReDatum, rk.KuNr, k.Name, k.Vorname
|
|
|
FROM RechKoepfe AS rk
|
|
|
INNER JOIN Kunden AS k
|
|
|
ON rk.KuNr = k.KuNr
|
|
|
WHERE rk.ReNr = :ReNr";
|
|
|
|
|
|
/*
|
|
|
prepare() bereitet die SQL-Anweisung vor.
|
|
|
Der benannte Platzhalter :ReNr wird erst bei execute()
|
|
|
mit der Rechnungsnummer verbunden. Dadurch wird die
|
|
|
Rechnungsnummer nicht unmittelbar in den SQL-Text
|
|
|
eingesetzt.
|
|
|
*/
|
|
|
$stmtHead = $pdo->prepare($sqlHead);
|
|
|
$stmtHead->execute(["ReNr" => $ReNr]);
|
|
|
|
|
|
/*
|
|
|
fetch() liest den gefundenen Rechnungskopf.
|
|
|
Wurde keine passende Rechnung gefunden, liefert fetch()
|
|
|
den booleschen Wert false.
|
|
|
*/
|
|
|
$head = $stmtHead->fetch();
|
|
|
|
|
|
/*
|
|
|
Das Programm wird beendet, wenn keine Rechnung mit der
|
|
|
angegebenen Rechnungsnummer vorhanden ist.
|
|
|
*/
|
|
|
if ($head === false) {die("Fehler: Rechnung nicht gefunden "
|
|
|
. "(ReNr " . h($ReNr) . ").");
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
6. RECHNUNGSPOSITIONEN LADEN
|
|
|
Die Relation RechPos enthält unter anderem:
|
|
|
- Positionsnummer
|
|
|
- Artikelnummer
|
|
|
- Anzahl
|
|
|
Über die Artikelnummer wird die Relation Artikel
|
|
|
verbunden. Dadurch stehen außerdem die Beschreibung
|
|
|
und der Preis des Artikels zur Verfügung.
|
|
|
Der Positionswert wird innerhalb der SQL-Anweisung berechnet:
|
|
|
Anzahl * Preis
|
|
|
Der Alias Positionswert gibt dem berechneten Ergebnis einen Namen.
|
|
|
Zum Inner-Join: Dieser verbindet die Rechnungspositionen mit den
|
|
|
zugehörigen Artikeldaten. Vgl. auch den Anhang. Die Join-Bedingung rp.ArtNr = a.ArtNr legt
|
|
|
fest, dass Datensätze mit derselben Artikelnummer zusammengeführt
|
|
|
werden. Die Aliase rp und a stehen für die Relationen RechPos und
|
|
|
Artikel. Die WHERE-Bedingung beschränkt das Ergebnis auf die
|
|
|
Positionen der durch :ReNr angegebenen Rechnung.
|
|
|
*/
|
|
|
$sqlPos = "SELECT rp.PosNr, rp.ArtNr, a.Beschreibung, a.Preis,
|
|
|
rp.Anzahl, (rp.Anzahl * a.Preis) AS Positionswert
|
|
|
FROM RechPos AS rp INNER JOIN Artikel AS a ON rp.ArtNr = a.ArtNr
|
|
|
WHERE rp.ReNr = :ReNr
|
|
|
ORDER BY rp.PosNr";
|
|
|
|
|
|
// Auch für diese Abfrage wird ein Prepared Statement verwendet.
|
|
|
$stmtPos = $pdo->prepare($sqlPos);
|
|
|
$stmtPos->execute(["ReNr" => $ReNr]);
|
|
|
|
|
|
// fetchAll() liest alle Rechnungspositionen und speichert sie in
|
|
|
// einem Array.
|
|
|
$pos = $stmtPos->fetchAll();
|
|
|
|
|
|
/*
|
|
|
7. RECHNUNGSSUMME BERECHNEN
|
|
|
Die Variable $summe erhält den Anfangswert 0. Die foreach-Schleife
|
|
|
durchläuft anschließend sämtliche Rechnungspositionen. Bei jeder
|
|
|
Position wird deren Positionswert zur Rechnungssumme addiert.
|
|
|
Die Typumwandlung (float) stellt sicher, dass mit einem
|
|
|
numerischen Wert gerechnet wird.
|
|
|
*/
|
|
|
$summe = 0.0;
|
|
|
foreach ($pos as $p) {
|
|
|
$summe += (float)$p["Positionswert"];
|
|
|
}
|
|
|
} catch (PDOException $e) {
|
|
|
|
|
|
/*
|
|
|
8. FEHLERBEHANDLUNG
|
|
|
Tritt beim Verbindungsaufbau oder beim Ausführen einer
|
|
|
SQL-Anweisung ein Datenbankfehler auf, wird das Programm
|
|
|
beendet.
|
|
|
Für das lokale Lernsystem wird hier die technische
|
|
|
Fehlermeldung ausgegeben. Auf einem öffentlichen
|
|
|
Webserver sollte der Benutzer nur eine allgemeine
|
|
|
Fehlermeldung erhalten.
|
|
|
*/
|
|
|
die("DB-Fehler: " . h($e->getMessage()));
|
|
|
}
|
|
|
?>
|
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>Rechnung anzeigen</title>
|
|
|
|
|
|
<!--
|
|
|
Die Gestaltung der Webseite wird aus einer CSS-Datei geladen.
|
|
|
-->
|
|
|
<link rel="stylesheet" href="KuReAr_6_show.css">
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="box">
|
|
|
|
|
|
<!--
|
|
|
Hinweis darauf, dass die Rechnung zuvor erfolgreich
|
|
|
gespeichert wurde.
|
|
|
-->
|
|
|
<div class="caption">Rechnung wurde gespeichert -
|
|
|
Anzeige der neuen Rechnung
|
|
|
</div>
|
|
|
|
|
|
<!--
|
|
|
Die Rechnungsnummer wird in die Überschrift eingesetzt.
|
|
|
-->
|
|
|
<h2>Rechnung <?= h($head["ReNr"]) ?></h2>
|
|
|
|
|
|
<!--
|
|
|
Ausgabe der allgemeinen Rechnungsdaten. Vgl. zur Syntax Kapitel 9.
|
|
|
-->
|
|
|
<div class="info">
|
|
|
<div><strong>ReNr:</strong><?= h($head["ReNr"]) ?>
|
|
|
</div>
|
|
|
<div><strong>ReDatum:</strong><?= h($head["ReDatum"]) ?>
|
|
|
</div>
|
|
|
<div><strong>KuNr:</strong><?= h($head["KuNr"]) ?></div>
|
|
|
<div><strong>Kunde:</strong>
|
|
|
<?= h($head["Name"] . ", " . $head["Vorname"]) ?></div>
|
|
|
<div><strong>Rechnungssumme:</strong>
|
|
|
<?= number_format($summe,2,",",".") ?>Euro</div>
|
|
|
</div>
|
|
|
|
|
|
<!--
|
|
|
Ausgabe der einzelnen Rechnungspositionen als HTML-Tabelle.
|
|
|
-->
|
|
|
<table class="rel">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>PosNr</th>
|
|
|
<th>ArtNr</th>
|
|
|
<th>Beschreibung</th>
|
|
|
<th>Preis</th>
|
|
|
<th>Anzahl</th>
|
|
|
<th>Positionswert</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
|
|
|
<!--Prüfen, ob Zahl der Positionen Null -->
|
|
|
<?php if (count($pos) === 0): ?>
|
|
|
|
|
|
<!--
|
|
|
Dieser Fall sollte bei einer vollständig gespeicherten Rechnung
|
|
|
normalerweise nicht auftreten.
|
|
|
-->
|
|
|
<tr>
|
|
|
<td colspan="6">Zu dieser Rechnung sind keine Positionen
|
|
|
vorhanden.</td>
|
|
|
</tr>
|
|
|
<?php else: ?>
|
|
|
<?php foreach ($pos as $p): ?>
|
|
|
|
|
|
<!--
|
|
|
Für jede Rechnungsposition wird eine Zeile der Tabelle erzeugt.
|
|
|
-->
|
|
|
<tr>
|
|
|
<td><?= h($p["PosNr"]) ?></td>
|
|
|
<td><?= h($p["ArtNr"]) ?></td>
|
|
|
<td><?= h($p["Beschreibung"]) ?></td>
|
|
|
<td><?= number_format((float)$p["Preis"],2,",",".") ?></td>
|
|
|
<td><?= h($p["Anzahl"]) ?></td>
|
|
|
<td><?= number_format((float)$p["Positionswert"],2,",",".") ?></td>
|
|
|
</tr>
|
|
|
<?php endforeach; ?>
|
|
|
<?php endif; ?>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
|
|
|
<!--
|
|
|
Verweise auf die Erfassung einer weiteren Rechnung und
|
|
|
auf die Gesamtausgabe der vier Relationen.
|
|
|
-->
|
|
|
<div class="actions">
|
|
|
<a class="btn" href="KuReArRechnungen.php">Neue Rechnung
|
|
|
erfassen</a>
|
|
|
<a class="btn" href="KuReAr_5.php">
|
|
|
Gesamtausgabe (alle vier Relationen)</a>
|
|
|
</div>
|
|
|
</div>
|
|
|
</body>
|
|
|
</html>
|
|
7.5.2 Programmlauf |
|
Nicht vergessen: Das Programm wird über KuRaArStart.php angesteuert: http://localhost/kurearStart.php. Siehe auch oben. |
|
Dann wird der Button Rechnungen eingeben angeklickt. |
|

|
|
Das führt zur Eingabemaske: |
|

|
|
Nach dem Speichern der Rechnung: |
|

|
|
Dann die Gesamtausgabe: |
|

|
|
7.5.3 Anmerkungen |
|
Die Funktion h() dient dazu, Werte sicher in einer HTML-Seite auszugeben: |
|
|
function h(mixed $value): string
|
|
|
{
|
|
|
return htmlspecialchars((string)$value, ENT_QUOTES, "UTF-8");
|
|
|
}
|
|
Die erste Zeile function h(mixed $value): string definiert eine Funktion mit dem Namen h. |
|
- function leitet die Funktionsdefinition ein.
- h ist der Name der Funktion. Der kurze Name steht hier sinngemäß für "HTML-sichere Ausgabe".
- $value ist der Parameter. Beim Aufruf wird der auszugebende Wert an die Funktion übergeben.
- mixed bedeutet, dass der übergebene Wert unterschiedliche Datentypen haben darf, beispielsweise string, int, float oder null.
- : string legt fest, dass die Funktion als Ergebnis immer eine Zeichenkette zurückgibt.
|
|
Die Typumwandlung (string)$value wandelt den übergebenen Wert zunächst in eine Zeichenkette um. Dadurch können auch Zahlen sicher an htmlspecialchars() übergeben werden. |
|
Die Funktion htmlspecialchars(...) wandelt Zeichen um, die in HTML eine besondere Bedeutung haben. Z.B. das kaufmännische Und, das Größer- und Kleiner-Zeichen sowie Anführungszeichen. Vgl. auch den Anhang. |
|
ENT_QUOTES bewirkt, dass sowohl doppelte als auch einfache Anführungszeichen umgewandelt werden. "UTF-8" gibt den verwendeten Zeichensatz an. Dadurch werden beispielsweise Umlaute wie ä, ö und ü korrekt verarbeitet. return gibt die umgewandelte Zeichenkette an die aufrufende Stelle zurück. Die Funktion kann in einer HTML-Ausgabe kurz und übersichtlich aufgerufen werden: |
|
|
<p>Kunde: <?= h($kunde["Name"]) ?></p>
|
|
Sie ist damit eine Hilfsfunktion für die sichere HTML-Ausgabe. Sie schützt allerdings nicht allgemein vor allen Angriffen: Für SQL-Anweisungen müssen weiterhin Prepared Statements verwendet werden. |
|
7.5.4 KuReAr_6_show.css |
|
|
/*
|
|
|
Mit dem Universal-Selektor * gilt die folgende Festlegung für alle
|
|
|
HTML-Elemente.
|
|
|
box-sizing: border-box bewirkt, dass festgelegte Breiten
|
|
|
bereits die Innenabstände und Rahmen enthalten.
|
|
|
Dadurch wird verhindert, dass ein Element durch padding
|
|
|
oder border breiter wird als vorgesehen.
|
|
|
*/
|
|
|
* {
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
1. GESTALTUNG DER GESAMTEN SEITE mit body
|
|
|
Als bevorzugte Schriftart wird Arial verwendet. Falls Arial
|
|
|
nicht verfügbar ist, verwendet der Browser eine andere serifenlose
|
|
|
Schriftart.
|
|
|
Die hellgraue Hintergrundfarbe hebt den weißen Inhaltsbereich
|
|
|
optisch von der Seite ab.
|
|
|
margin: 0 entfernt den standardmäßigen Außenabstand des Browsers.
|
|
|
*/
|
|
|
body {margin: 0; background-color: #f5f5f5;
|
|
|
font-family: Arial, sans-serif;
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
2. GEMEINSAMER CONTAINER FÜR DIE AUSGABE
|
|
|
- width: 100% ermöglicht die Anpassung an kleine Browserfenster.
|
|
|
- max-width: 920px begrenzt die Breite auf den ursprünglich
|
|
|
vorgesehenen Wert von 920 Pixeln.
|
|
|
- margin: 20px auto erzeugt oben und unten einen Abstand von
|
|
|
20 Pixeln und zentriert die Box horizontal.
|
|
|
- padding erzeugt innerhalb der Box einen Abstand von 20 Pixeln.
|
|
|
- overflow-x: auto blendet bei Bedarf eine horizontale
|
|
|
Bildlaufleiste ein. Dies verhindert, dass die Relation bei
|
|
|
kleinen Bildschirmbreiten über den Inhaltsbereich hinausragt.
|
|
|
*/
|
|
|
.box {width: 100%; max-width: 920px; margin: 20px auto;
|
|
|
padding: 20px; overflow-x: auto; background-color: #fff;
|
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
3. HINWEISTEXT
|
|
|
Der Hinweis oberhalb der Überschrift wird dunkelrot und fett
|
|
|
dargestellt.
|
|
|
margin erzeugt:
|
|
|
- oben einen Außenabstand von 10 Pixeln
|
|
|
- links und rechts keinen Außenabstand
|
|
|
- unten einen Außenabstand von 8 Pixeln
|
|
|
*/
|
|
|
.caption {margin: 10px 0 8px; color: #c00; font-weight: 700;}
|
|
|
|
|
|
/*
|
|
|
4. ÜBERSCHRIFT
|
|
|
Die Überschrift wird horizontal zentriert.
|
|
|
margin erzeugt oben einen Abstand von 6 Pixeln, links und
|
|
|
rechts keinen Abstand und unten einen Abstand von 12 Pixeln.
|
|
|
*/
|
|
|
h2 {margin: 6px 0 12px; text-align: center;}
|
|
|
|
|
|
/*
|
|
|
5. BEREICH MIT DEN ALLGEMEINEN RECHNUNGSDATEN
|
|
|
Der Bereich enthält Rechnungsnummer, Rechnungsdatum,
|
|
|
Kundennummer, Kundenname und Rechnungssumme.
|
|
|
border erzeugt einen schwarzen Rahmen mit einer Stärke von
|
|
|
2 Pixeln.
|
|
|
padding erzeugt innerhalb des Bereichs einen Abstand von 10 Pixeln.
|
|
|
margin erzeugt:
|
|
|
- oben einen Außenabstand von 10 Pixeln
|
|
|
- links und rechts keinen Außenabstand
|
|
|
- unten einen Außenabstand von 14 Pixeln
|
|
|
*/
|
|
|
.info {margin: 10px 0 14px; padding: 10px; border: 2px solid #000;
|
|
|
background-color: #fff;}
|
|
|
|
|
|
/*
|
|
|
Zwischen den einzelnen Angaben im Informationsbereich wird
|
|
|
ein kleiner vertikaler Abstand eingefügt.
|
|
|
Diese zusätzliche Regel verbessert die Lesbarkeit, ohne die
|
|
|
HTML-Struktur ändern zu müssen.
|
|
|
*/
|
|
|
.info div + div {margin-top: 4px;}
|
|
|
|
|
|
/*
|
|
|
6. RELATION MIT DEN RECHNUNGSPOSITIONEN
|
|
|
- border-collapse: collapse führt die Rahmen benachbarter
|
|
|
Zellen zu gemeinsamen Linien zusammen.
|
|
|
- width: 100% bewirkt, dass die Relation die gesamte verfügbare
|
|
|
Breite einnimmt.
|
|
|
- min-width verhindert, dass die sechs Spalten auf kleinen
|
|
|
Bildschirmen zu stark zusammengedrückt werden.
|
|
|
- border erzeugt einen schwarzen Außenrahmen mit einer Stärke
|
|
|
von 3 Pixeln.
|
|
|
- font-size legt eine Schriftgröße von 15 Pixeln fest.
|
|
|
*/
|
|
|
table.rel {width: 100%; min-width: 700px; border: 3px solid #000;
|
|
|
border-collapse: collapse;font-size: 15px;}
|
|
|
|
|
|
/*
|
|
|
7. KOPF- UND DATENZELLEN
|
|
|
Jede Zelle erhält einen schwarzen Rahmen mit einer Stärke
|
|
|
von 2 Pixeln.
|
|
|
padding legt den Innenabstand fest:
|
|
|
- oben und unten: 6 Pixel
|
|
|
- links und rechts: 10 Pixel
|
|
|
Die Inhalte werden zunächst linksbündig ausgerichtet.
|
|
|
*/
|
|
|
table.rel th,table.rel td {padding: 6px 10px;border: 2px solid #000;
|
|
|
background-color: #fff; text-align: left;}
|
|
|
|
|
|
/*
|
|
|
8. TABELLENKOPF
|
|
|
Die Überschriften werden fett dargestellt und erhalten eine
|
|
|
hellgraue Hintergrundfarbe. Dadurch hebt sich der Tabellenkopf von
|
|
|
den Datenzeilen ab.
|
|
|
*/
|
|
|
table.rel th {background-color: #e0e0e0;font-weight: 700;}
|
|
|
|
|
|
/*
|
|
|
9. RECHTSBÜNDIGE AUSRICHTUNG NUMERISCHER WERTE
|
|
|
Rechtsbündig ausgegeben werden:
|
|
|
- 1. Spalte: PosNr
|
|
|
- 2. Spalte: ArtNr
|
|
|
- 4. Spalte: Preis
|
|
|
- 5. Spalte: Anzahl
|
|
|
- 6. Spalte: Positionswert
|
|
|
Die Beschreibung in der 3. Spalte bleibt linksbündig.
|
|
|
*/
|
|
|
table.rel td:nth-child(1), table.rel td:nth-child(2),
|
|
|
table.rel td:nth-child(4), table.rel td:nth-child(5),
|
|
|
table.rel td:nth-child(6)
|
|
|
{
|
|
|
text-align: right;
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
10. HERVORHEBUNG EINER DATENZEILE
|
|
|
Wenn sich der Mauszeiger über einer Datenzeile befindet,
|
|
|
erhält diese eine sehr helle blaue Hintergrundfarbe.
|
|
|
Da die Hintergrundfarbe ursprünglich den einzelnen Zellen
|
|
|
zugewiesen wurde, muss der Hover-Effekt ebenfalls auf die
|
|
|
Zellen angewendet werden.
|
|
|
*/
|
|
|
table.rel tbody tr:hover td {background-color: #f0f8ff;}
|
|
|
|
|
|
/*
|
|
|
11. BEREICH MIT DEN VERWEISEN
|
|
|
display: flex ordnet die beiden Verweise nebeneinander an.
|
|
|
gap erzeugt einen Abstand von 10 Pixeln zwischen ihnen.
|
|
|
flex-wrap: wrap erlaubt einen Zeilenumbruch, wenn der
|
|
|
verfügbare Platz nicht ausreicht.
|
|
|
*/
|
|
|
.actions {display: flex; flex-wrap: wrap; gap: 10px;
|
|
|
margin-top: 12px;}
|
|
|
|
|
|
/*
|
|
|
12. GESTALTUNG DER VERWEISE ALS SCHALTFLÄCHEN
|
|
|
- display: inline-block ermöglicht die Verwendung von
|
|
|
Innenabständen und lässt den Verweis wie eine Schaltfläche
|
|
|
erscheinen.
|
|
|
- text-decoration: none entfernt die übliche Unterstreichung
|
|
|
eines Verweises.
|
|
|
- color legt Schwarz als Schriftfarbe fest.
|
|
|
*/
|
|
|
a.btn {display: inline-block; padding: 6px 10px;
|
|
|
border: 2px solid #000; background-color: #eee;
|
|
|
color: #000; text-decoration: none;}
|
|
|
|
|
|
/*
|
|
|
13. HERVORHEBUNG EINER SCHALTFLÄCHE
|
|
|
Wenn sich der Mauszeiger über einer Schaltfläche befindet,
|
|
|
wird deren Hintergrund etwas dunkler.
|
|
|
*/
|
|
|
a.btn:hover {background-color: #ddd;}
|
|
|
|
|
|
/*
|
|
|
14. BEDIENUNG MIT DER TASTATUR
|
|
|
- :focus-visible hebt einen Verweis hervor, wenn er über die
|
|
|
Tastatur ausgewählt wurde.
|
|
|
- outline liegt außerhalb des Rahmens und beeinträchtigt daher
|
|
|
nicht die Abmessungen der Schaltfläche.
|
|
|
*/
|
|
|
a.btn:focus-visible {outline: 3px solid #06c; outline-offset: 2px;}
|
|
|
|
|
|
/*
|
|
|
15. ANPASSUNG AN KLEINE BILDSCHIRME
|
|
|
Bei einer Fensterbreite von höchstens 960 Pixeln wird links
|
|
|
und rechts ein Außenabstand von jeweils 10 Pixeln berücksichtigt.
|
|
|
calc(100% - 20px) zieht diese beiden Abstände von der verfügbaren
|
|
|
Fensterbreite ab.
|
|
|
*/
|
|
|
@media (max-width: 960px) {
|
|
|
.box {width: calc(100% - 20px); margin: 10px; }
|
|
|
}
|
|
Die Programme für die übrigen Buttons sind in Vorbereitung. Hier im Folgenden sozusagen die Platzhalter. |
|
7.6 Sonstige Programme |
|
7.6.1 KuReArArtikel.php |
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>Artikel bearbeiten</title>
|
|
|
<link rel="stylesheet" href="KuReAr_5.css">
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="box">
|
|
|
<h2>Artikel bearbeiten</h2>
|
|
|
<p>
|
|
|
Das Programm zur Bearbeitung der Artikeldaten
|
|
|
befindet sich in Vorbereitung.
|
|
|
</p>
|
|
|
</div>
|
|
|
</body>
|
|
|
</html>
|
|
7.6.2 KuReArKunden.php |
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>Kunden bearbeiten</title>
|
|
|
<link rel="stylesheet" href="KuReAr_5.css">
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="box">
|
|
|
<h2>Kunden bearbeiten</h2>
|
|
|
<p>
|
|
|
Das Programm zur Bearbeitung der Kundendaten
|
|
|
befindet sich in Vorbereitung.
|
|
|
</p>
|
|
|
</div>
|
|
|
</body>
|
|
|
</html>
|
|
7.6.3 KuReArAusw1.php |
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>Liste der Rechnungen</title>
|
|
|
<link rel="stylesheet" href="KuReAr_5.css">
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="box">
|
|
|
<h2>Liste der Rechnungen</h2>
|
|
|
<p>
|
|
|
Das Programm zur Ausgabe der Rechnungsliste
|
|
|
befindet sich in Vorbereitung.
|
|
|
</p>
|
|
|
</div>
|
|
|
</body>
|
|
|
</html>
|
|
7.6.4 KuReArAusw2.php |
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>Liste der Kunden</title>
|
|
|
<link rel="stylesheet" href="KuReAr_5.css">
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="box">
|
|
|
<h2>Liste der Kunden</h2>
|
|
|
<p>
|
|
|
Das Programm zur Ausgabe der Kundenliste
|
|
|
befindet sich in Vorbereitung.
|
|
|
</p>
|
|
|
</div>
|
|
|
</body>
|
|
|
</html>
|
|
7.6.5 KuReArAusw3.php |
|
|
<!doctype html>
|
|
|
<html lang="de">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<title>Artikelbestand</title>
|
|
|
<link rel="stylesheet" href="KuReAr_5.css">
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="box">
|
|
|
<h2>Artikelbestand</h2>
|
|
|
<p>
|
|
|
Das Programm zur Ausgabe des Artikelbestands
|
|
|
befindet sich in Vorbereitung.
|
|
|
</p>
|
|
|
</div>
|
|
|
</body>
|
|
|
</html>
|
|
|
|
|
Die Zuordnung der drei Auswertungsprogramme ist damit: |
|
|
|
| Datei |
Vorgesehene Ausgabe |
| KuReArAusw1.php |
Liste der Rechnungen |
| KuReArAusw2.php |
Liste der Kunden |
| KuReArAusw3.php |
Artikelbestand |
| |
Die Dateien enthalten noch keinen PHP-Code. Die Dateiendung .php ist dennoch sinnvoll, weil die Platzhalter später um Datenbankabfragen ergänzt werden. Durch <div class="box"> wird außerdem bereits die Gestaltung aus KuReAr_5.css genutzt. |
|
11 Anhang |
|
Vertiefte Erläuterungen zu ausgewählten Konzepten bzw. Konstrukten oder Begriffen. |
|
|
|
11.1 Vom Web zur DB |
|
Von der Webseite zur Datenbank |
|
Es gibt im Wesentlichen zwei Ebenen, auf denen man "von den Masken einer Webseite" (HTML-Formulare, Listen, Detailseiten) auf eine relationale Datenbank wie KuReAr zugreift: |
|
- Serverseitig (klassisch, am häufigsten)
- Über eine API (modern, entkoppelt; Frontend ruft Endpunkte auf)
|
|
Darunter gibt es mehrere konkrete Varianten. |
|
1) Direkt serverseitig mit PHP (Form -> PHP -> DB) |
|
1.1 PHP + PDO |
|
Das Formular sendet Daten an ein PHP-Skript, das per PDO (Prepared Statements) mit MySQL kommuniziert. |
|
KuReAr-Beispiel (Kunden suchen): |
|
- Maske: Eingabefeld KuNr
- PHP: SELECT * FROM kunden WHERE KuNr = :id
|
|
Vorteile |
|
- Sehr sicher (Prepared Statements)
- Flexibel (MySQL, PostgreSQL, ...)
- Gute Fehlerbehandlung/Transaktionen
|
|
Typische Einsatzfälle in KuReAr |
|
- Kunden anzeigen/ändern
- Rechnungskopf speichern
- Rechnungspositionen speichern (Transaktion!)
|
|
1.2 PHP + MySQLi |
|
Ähnlich PDO, aber MySQL-spezifisch. Ebenfalls mit Prepared Statements möglich. |
|
Vorteile |
|
- Direkt für MySQL/MariaDB
- Schnell, simpel für kleine Projekte
|
|
Nachteil: Weniger portabel als PDO |
|
1.3 (Nicht empfohlen) "direkte" SQL-Strings aus Formulardaten |
|
Beispiel: |
|
|
SELECT * FROM kunden WHERE KuNr = $id
|
|
Das führt schnell zu SQL-Injection, wenn nicht sauber validiert/escaped wird. |
|
2) Zugriff über eine API-Schicht (Frontend <--> API <--> DB) |
|
2.1 PHP-API (REST) + Fetch im Browser |
|
Die Web-Maske (HTML/JS) ruft Endpunkte auf, z. B.: |
|
- GET /api/kunden?kunr=1003
- POST /api/rechnungen (Rechnungskopf)
- POST /api/rechnungen/{id}/positionen
|
|
Die API-Skripte greifen dann per PDO/MySQLi auf KuReAr zu. |
|
Vorteile |
|
- Frontend und DB-Logik sauber getrennt
- Mehrere Frontends möglich (Web, Mobile)
- Bessere Struktur für größere Projekte
|
|
Typisch für KuReAr |
|
- Kundenliste als JSON laden und in Tabelle rendern
- Artikel suchen (Autocomplete)
- Rechnung speichern über mehrere Calls oder als "ein Payload"
|
|
2.2 Node.js/Express-API (oder Python/Java) + MySQL-Driver |
|
Nicht PHP, sondern ein Backend-Dienst stellt die API bereit und nutzt einen MySQL-Treiber/ORM. |
|
Vorteile |
|
- Gute Architektur für Team/Skalierung
- Moderne Toolchains
|
|
Nachteil: Mehr Setup als reines PHP |
|
3) Mit einem ORM (Object-Relational Mapping) statt "SQL von Hand" |
|
Doctrine (PHP), Sequelize (Node), Hibernate (Java) usw. |
|
Dabei wird mit Klassen und Objekten gearbeitet. Das ORM übernimmt die Zuordnung zwischen Objekten und Datenbankrelationen und erzeugt die erforderlichen SQL-Befehle automatisch. |
|
Vorteile |
|
- Weniger SQL-Handarbeit
- Beziehungen (z. B. Kunden <--> Adressen über eine junction table wie kuadr/CustomerAddresses) sind komfortabler zu modellieren
|
|
Nachteile |
|
- Einarbeitung erforderlich; viele Abläufe erfolgen automatisch und sind für Einsteiger zunächst schwer nachvollziehbar.
- Für kleine Projekte wie KuReAr häufig zu aufwendig (Overkill).
|
|
4) Architektur-Muster, wie die Masken "an die DB" kommen |
|
4.1 Klassisch: Multi-Page / Post-Redirect-Get (PRG) |
|
- Formular POST -> PHP speichert in KuReAr -> Redirect -> Anzeige/Bestätigung
- Sehr robust, passt gut zu KuReAr-Übungen (Kunde anlegen, Rechnung speichern)
|
|
4.2 Single-Page-Anteile: AJAX/Fetch |
|
- Maske bleibt, lädt Daten dynamisch (z. B. Artikel-Suche, Kundenfilter)
- Speichern weiterhin über API/Endpoints
|
|
5) Was bei einer Datenbank wie KuReAr fast immer gebraucht wird (egal welche Variante) |
|
- Prepared Statements (PDO/MySQLi)
- Validierung (z. B. KuNr ist Integer, Datum gültig, Menge > 0)
- Transaktionen bei zusammengesetzten Speichervorgängen
KuReAr-Beispiel: Rechnung speichern = rechkoepfe + mehrere rechpos -> alles oder nichts.
- Saubere Modellierung von n:m
KuReAr-Beispiel: Kunden <--> Adressen über kuadr (Verbindungsrelation, junction table), ohne doppelte Attribute in der Ergebnisliste (JOIN statt Kopieren).
|
|
11.2 HTML Special Characters |
|
PHP-Funktion htmlspecialchars() |
|
Die PHP-Funktion htmlspecialchars() wandelt bestimmte Sonderzeichen in HTML-Entitäten um. Dadurch werden sie nicht als HTML-Code interpretiert, sondern als normaler Text angezeigt. Die Funktion wird hauptsächlich verwendet, um zu verhindern, dass HTML- oder JavaScript-Code über ein Formular eingeschleust wird. |
|
Eine typische Anwendung ist, wenn Benutzereingaben aus einem Formular erfasst und ins Programm bzw. in die Datenbank eingegeben werden. Beispiel: |
|
|
echo htmlspecialchars($_POST['name']);
|
|
Folgende Zeichen werden umgewandelt: |
|

|
|
Sinnvoll sind noch folgende Ergänzungen: |
|
|
htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
|
Der Parameter ENT_QUOTES wandelt auch einfache Anführungszeichen um und UTF-8 präzisiert die Zeichenkodierung. |
|
11.3 Prepared Statement |
|
Ein Prepared Statement gibt die Möglichkeit, SQL-Befehle abgesichert an eine Datenbank zu schicken. Die Grundidee: |
|
- Zuerst wird das SQL-Statement mit Platzhaltern vorbereitet.
- Danach werden die konkreten Werte getrennt übergeben.
|
|
So werden SQL-Injection-Angriffe verhindert. |
|
Beispiel |
|
In der Relation (Tabelle) kunden der Datenbank KuReAr wird ein Kunde über seine Kundennummer (KuNr) gesucht. Eine unsichere Variante dafür ist: |
|
|
$id = $_GET['id'];
|
|
|
$sql = "SELECT * FROM kunden WHERE KuNr = $id";
|
|
Damit kann ein Angreifer statt z.B. "1003" folgendes eingeben: |
|
|
1003 OR 1=1
|
|
Das würde im SQL-Befehl so aussehen: |
|
|
SELECT * FROM kunden WHERE KuNr = 1003 OR 1=1
|
|
Diese Anweisung liefert alle Kunden, denn 1=1 gilt immer. Solch ein Vorgehen wird SQL-Injection genannt. Das kann durch die Verwendung von Prepared Statements verhindert werden. Dabei unterscheidet die Anwendung zwischen der SQL-Struktur und den Werten (Attributsausprägungen) und es ist festgelegt, dass die eingegebenen Werte die Struktur (des Befehls) nicht mehr verändern können. |
|
Variante 1: Mit PDO |
|
- Schritt 1 - SQL vorbereiten (mit Platzhalter)
|
|
|
$stmt = $pdo->prepare(
|
|
|
"SELECT * FROM kunden WHERE KuNr = :id");
|
|
:id ist ein Platzhalter. |
|
- Schritt 2 - Wert einsetzen
|
|
|
$stmt->execute(['id' => $id]);
|
|
Erst jetzt wird der Wert gebunden. |
|
- Schritt 3 - Ergebnis holen
|
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
Die Datenbank bekommt also intern zwei Dinge: |
|
- Struktur: SELECT * FROM kunden WHERE KuNr = ?
- Wert (Attributsausprägung): 1003
Der Wert wird nie als Teil des SQL-Codes interpretiert, sondern nur als Datenwert. |
|
Variante 2: Mit MySQLi |
|
Hier gibt es zwei Schritte mehr: |
|
|
|
|
|
$stmt = $con->prepare(
|
|
|
"SELECT * FROM kunden WHERE KuNr = ?"
|
|
|
);
|
|
Das Fragezeichen ist der Platzhalter für den später einzutragenden Wert. |
|
|
|
|
|
$stmt->bind_param("i", $id);
|
|
Das "i" bedeutet: integer. Möglich wären auch s (string), d (double), b (blob) |
|
|
|
|
|
$stmt->execute();
|
|
Betrachten wir als Beispiel das Speichern einer neuen Rechnung in KuReAr: |
|
|
$stmt = $pdo->prepare(
|
|
|
"INSERT INTO rechkoepfe (RechNr, KuNr, Datum)
|
|
|
VALUES (:rnr, :knr, :datum)");
|
|
|
$stmt->execute([
|
|
|
'rnr'=> $rechNr,
|
|
|
'knr'=> $kuNr,
|
|
|
'datum' => $datum ]);
|
|
Es gilt also: Nie Benutzereingaben direkt in den SQL-Befehl einbauen - immer Prepared Statements verwenden. |
|
|
11.4 Exception auslösen |
|
Zum Begiff Exception (Ausnahme) Eine Exception ist ein Objekt, das einen außergewöhnlichen Zustand oder Fehler während der Programmausführung beschreibt. Es enthält unter anderem eine Fehlermeldung und Informationen darüber, wo und warum der Fehler aufgetreten ist. |
|
Die Option PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION legt fest, dass PDO bei Datenbankfehlern eine Exception auslöst. Betrachten wir das genauer: |
|
|
$pdo = new PDO(
|
|
|
"mysql:host=localhost;dbname=KuReAr;charset=utf8mb4",
|
|
|
"root",
|
|
|
"",
|
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
|
|
);
|
|
Was bedeutet das? |
|
PDO::ATTR_ERRMODE ist eine Konstante der Klasse PDO. Deshalb auch die Großbuchstaben. Diese werden in diesem Kontext für Konstanten verwendet. Sie steht für die Einstellung "Fehlerbehandlungsmodus". Damit wird festgelegt, wie PDO auf Fehler reagiert. |
|
PDO::ERRMODE_EXCEPTION ist ebenfalls eine Klassenkonstante. Sie bedeutet: Bei einem Fehler wird eine Exception (Ausnahme, Fehlermeldung) ausgelöst. |
|
Was passiert dadurch konkret? Wenn z. B. ein SQL-Fehler auftritt: |
|
|
$pdo->query("SELECT * FROM nicht_existierende_tabelle");
|
|
Dann geschieht folgendes: |
|
- Ohne ERRMODE_EXCEPTION liefert PDO nur einen Fehlercode und das Programm läuft weiter.
- Mit ERRMODE_EXCEPTION "wirft" PDO automatisch eine PDOException, der Code "springt" in den Catch-Block so dass das Programm kontrolliert auf den Fehler reagiert.
|
|
Beispiel: |
|
|
try {
|
|
|
$pdo->query("SELECT * FROM nicht_existierende_tabelle");
|
|
|
} catch (PDOException $e) {
|
|
|
echo "Fehler: " . $e->getMessage();
|
|
|
}
|
|
11.5 Variable $con |
|
$con ist eine Variable, die auf ein Objekt (eine Instanz) der Klasse mysqli verweist. Dieses Objekt entsteht durch den Aufruf von new mysqli(...). Es verwaltet die Verbindung zum MySQL-Server und stellt Methoden sowie Eigenschaften bereit, mit denen PHP-Programme Datenbankoperationen durchführen können. Über die Variable $con kann auf diese Methoden und Eigenschaften zugegriffen werden. |
|
Etwas technischer formuliert: $con verweist auf ein Objekt (eine Instanz) der Klasse mysqli. Dieses Objekt entsteht durch den Aufruf des Konstruktors new mysqli(...) und kapselt die Verbindung zum MySQL-Server einschließlich der dafür erforderlichen Informationen und Funktionen. |
|
Wichtige Eigenschaften |
|
|
$con->connect_error
|
|
Häufig verwendete Methoden |
|
- $con->prepare()
- $con->query()
- $con->commit()
- $con->begin_transaction()
- $con->rollback()
- $con->close()
|
|
Beispiel |
|
|
$con = new mysqli("localhost", "root", "", "KuReAr");
|
|
Dabei geschieht Folgendes: |
|
- new erzeugt ein neues Objekt der Klasse mysqli.
- Der Konstruktor baut die Verbindung zum angegebenen MySQL-Server auf und authentifiziert den Benutzer.
- Das Objekt enthält den aktuellen Zustand dieser Verbindung sowie die zugehörigen Methoden und Eigenschaften.
- Die Variable $con speichert einen Verweis auf dieses Objekt und ermöglicht dadurch den Zugriff auf dessen Methoden und Eigenschaften.
|
|
11.6 Array von Arrays |
|
Mehrdimensionales Array, Ein Array von assoziativen Arrays |
|
Die äußere Struktur sieht recht vertraut aus: |
|
|
$positionen = [ ... ];
|
|
Das ist ein normales PHP-Array. Es enthält 3 Elemente. Die Indizes sind automatisch numerisch: 0, 1, 2 |
|
Es gibt aber noch die innere Struktur: Jedes Element ist wiederum ein assoziatives Array: |
|
|
["posnr" => 1, "artnr" => 100, "anzahl" => 2]
|
|
Das bedeutet: |
|
- Schlüssel = String ("posnr", "artnr", "anzahl")
- Werte = Integer
|
|
Der Datentyp ist array<int, array<string, int>>. Das heißt: |
|
- Äußeres Array: numerisch indiziert
- Inneres Array: String-Schlüssel -> Integer-Werte
|
|
Das ist hier sinnvoll,weil eine Liste von Rechnungspositionen modelliert wird. Jede Position hat Attribute wie: |
|
- Positionsnummer
- Artikelnummer
- Anzahl
|
|
Das entspricht konzeptionell einer kleinen "Tabelle im Speicher". |
|
Zugriff im Code |
|
|
foreach ($positionen as $p) {
|
|
|
echo $p["artnr"];
|
|
|
}
|
|
$p ist jeweils ein assoziatives Array. |
|
11.7 Klasse mysqli |
|
Die Klasse mysqli stellt zahlreiche Eigenschaften (Properties) und Methoden zur Verfügung. Über die Eigenschaften können Informationen über den Zustand der Datenbankverbindung oder über das Ergebnis der zuletzt ausgeführten SQL-Anweisung abgefragt werden. |
|
In der Praxis werden jedoch nur wenige Eigenschaften regelmäßig benötigt. Die wichtigsten werden im Folgenden vorgestellt. |
|
Eigenschaften beim Verbindungsaufbau |
|
Nach dem Aufbau einer Datenbankverbindung kann geprüft werden, ob dabei ein Fehler aufgetreten ist. |
|
connect_errno liefert die Fehlernummer eines Verbindungsfehlers: |
|
|
if ($con->connect_errno) {
|
|
|
echo $con->connect_errno;
|
|
|
}
|
|
kann folgende Ergebnisse zeitigen: |
|
- 0: Die Verbindung wurde erfolgreich aufgebaut.
- Ungleich 0: Es ist ein Fehler beim Verbindungsaufbau aufgetreten.
|
|
connect_error liefert den zugehörigen Fehlertext. |
|
|
echo $con->connect_error;
|
|
Diese Eigenschaft wird häufig verwendet, um die Ursache eines Verbindungsfehlers anzuzeigen. |
|
Eigenschaften nach SQL-Anweisungen |
|
Nach der Ausführung einer SQL-Anweisung können weitere Eigenschaften ausgewertet werden. |
|
- errno liefert die Fehlernummer der zuletzt ausgeführten SQL-Anweisung mit echo $con->errno;
- error liefert den Fehlertext der zuletzt ausgeführten SQL-Anweisung mit echo $con->error;
|
|
Diese beiden Eigenschaften sind beim Testen und Debuggen von Programmen hilfreich. |
|
Eigenschaften für Änderungen an der Datenbank |
|
affected_rows gibt an, wie viele Datensätze durch eine INSERT-, UPDATE- oder DELETE-Anweisung betroffen waren. Abfrage mit echo $con->affected_rows; Damit kann beispielsweise überprüft werden, ob eine Änderung tatsächlich durchgeführt wurde. |
|
insert_id liefert die automatisch vergebene AUTO_INCREMENT-Nummer des zuletzt eingefügten Datensatzes. Abfrage mit echo $con->insert_id; Diese Eigenschaft ist besonders wichtig, wenn zunächst ein Datensatz in einer übergeordneten Relation (z. B. Rechnungsköpfe, InvoiceHeaders) gespeichert und anschließend dazugehörige Datensätze (z. B. Rechnungspositionen, InvoiceItems) eingefügt werden sollen. |
|
Weitere Eigenschaften |
|
Die Klasse mysqli besitzt darüber hinaus zahlreiche weitere Eigenschaften, beispielsweise: |
|
- server_info - Version des MySQL-Servers
- host_info - Informationen über den Server bzw. Host
- protocol_version - verwendete Protokollversion
- thread_id - Kennung des aktuellen Server-Threads
|
|
Diese Eigenschaften werden vor allem für Verwaltungs- und Diagnosezwecke verwendet und spielen in den meisten Anwendungsprogrammen nur eine untergeordnete Rolle. |
|
Zusammenfassung |
|
Für die meisten PHP-Anwendungen reichen bereits wenige Attribute der Klasse mysqli aus: |
|
|
|
| Eigenschaft |
Bedeutung |
| connect_error |
Fehlermeldung beim Verbindungsaufbau |
| connect_errno |
Fehlernummer beim Verbindungsaufbau |
| error |
Fehlermeldung der zuletzt ausgeführten SQL-Anweisung |
| errno |
Fehlernummer der zuletzt ausgeführten SQL-Anweisung |
| affected_rows |
Anzahl der geänderten Datensätze |
| insert_id |
AUTO_INCREMENT-Wert des zuletzt eingefügten Datensatzes |
| |
|
|
11.8 Referenzieren |
|
Sehr oft taucht in Texten zu PHP, insbesondere zu PHP-Objekten, das Wort referenzieren auf. Z.B. so: |
|
new PDO() erzeugt ein Objekt der Klasse PDO. Die Variable referenziert dieses Verbindungsobjekt. |
|
Da kann leicht der Eindruck entstehen, dass die Variable das Objekt enthält. Dem ist aber nicht so. Bei PHP-Objekten enthält die Variable einen sogenannten Objektbezeichner, über den auf das Objekt zugegriffen wird. Deshalb wird hier und in ähnlichen Situationen die folgende Formulierung gewählt: |
|
new PDO() erzeugt ein Objekt der Klasse PDO. Die Variable $pdo verweist auf dieses Verbindungsobjekt. |
|
11.9 isset() |
|
Weil in den obigen Beispielen viele Formulare vorkommen und deshalb auch die Funktion isset(), hier eine Erläuterung dazu. |
|
Die PHP-Funktion isset() prüft, ob eine Variable vorhanden ist und nicht den Wert null enthält. Die allgemeine Schreibweise lautet: isset($variable) |
|
Die Funktion liefert einen booleschen Wert: |
|
- true - die Variable ist vorhanden und enthält nicht null
- false - die Variable ist nicht vorhanden oder enthält null
|
|
Einfaches Beispiel |
|
|
$name = "Meier";
|
|
|
if (isset($name)) {
|
|
|
echo "Die Variable ist vorhanden.";
|
|
|
}
|
|
Da $name vorhanden ist und den Wert "Meier" enthält, liefert isset($name) den Wert true. |
|
Variable mit dem Wert null |
|
|
$name = null;
|
|
|
if (isset($name)) {
|
|
|
echo "Die Variable ist vorhanden.";
|
|
|
}
|
|
Obwohl die Variable angelegt wurde, liefert isset($name) den Wert false, weil sie null enthält. |
|
Nicht vorhandene Variable |
|
|
if (isset($ort)) {
|
|
|
echo "Die Variable ist vorhanden.";
|
|
|
}
|
|
Wenn $ort vorher nicht angelegt wurde, liefert isset($ort) ebenfalls false. Dabei erzeugt isset() keine Warnung wegen einer nicht vorhandenen Variablen. |
|
Verwendung bei Formulardaten |
|
isset() wird häufig verwendet, um zu prüfen, ob ein bestimmtes Formularfeld übermittelt wurde: |
|
|
if (isset($_POST["posCount"])) {
|
|
|
$posCount = (int)$_POST["posCount"];
|
|
|
}
|
|
Das bedeutet: Wenn im Array $_POST ein Element mit dem Schlüssel "posCount" vorhanden ist und dessen Wert nicht null ist, wird der übermittelte Wert in eine ganze Zahl umgewandelt und der Variablen $posCount zugewiesen. |
|
Ohne die Prüfung könnte der Zugriff |
|
|
$_POST["posCount"]
|
|
eine Warnung erzeugen, wenn das Formularfeld nicht übermittelt wurde. |
|
Verwendung bei einem PDO-Objekt |
|
Im Programm KundenEintragenMySQLi.php steht: |
|
|
if (isset($pdo) && $pdo->inTransaction()) {
|
|
|
$pdo->rollBack();
|
|
|
}
|
|
Hier prüft isset($pdo), ob die Variable $pdo vorhanden ist und nicht null enthält. Diese Prüfung ist erforderlich, weil bereits der Verbindungsaufbau fehlschlagen könnte: |
|
|
$pdo = new PDO(...);
|
|
Wenn dabei eine Ausnahme ausgelöst wird, wurde $pdo möglicherweise noch kein PDO-Objekt zugewiesen. Deshalb darf |
|
|
$pdo->inTransaction()
|
|
nur aufgerufen werden, wenn $pdo tatsächlich vorhanden ist. Durch die Kurzschlussauswertung des UND-Operators && wird die zweite Bedingung nur geprüft, wenn isset($pdo) den Wert true liefert. |
|
Prüfung mehrerer Variablen |
|
isset() kann mehrere Variablen gleichzeitig prüfen: |
|
|
if (isset($name, $vorname, $ort)) {
|
|
|
echo "Alle Variablen sind vorhanden.";
|
|
|
}
|
|
Das Ergebnis ist nur dann true, wenn alle drei Variablen vorhanden sind und keine davon null enthält. |
|
Somit gilt: |
|
isset() prüft, ob eine Variable oder ein Element eines Arrays vorhanden ist und nicht den Wert null enthält. Die Funktion liefert in diesem Fall true, andernfalls false. Sie wird häufig eingesetzt, bevor auf möglicherweise nicht vorhandene Formulardaten zugegriffen wird. |
|
11.10 Der Null-Koaleszenzoperator |
|
Der Null-Koaleszenzoperator ?? prüft, ob der Ausdruck auf seiner linken Seite vorhanden ist und nicht den Wert null hat. Die allgemeine Form lautet: $linkerAusdruck ?? $ersatzwert. Das Ergebnis ist: |
|
- der linke Ausdruck, wenn er vorhanden und nicht null ist
- andernfalls der rechte Ausdruck als Ersatzwert
|
|
Beispiel: |
|
|
$name = $_POST["name"] ?? "";
|
|
Das bedeutet: Wenn im Array $_POST das Element mit dem Schlüssel "name" vorhanden ist und nicht null enthält, wird dessen Wert verwendet. Andernfalls wird die leere Zeichenkette "" verwendet. |
|
Gleichbedeutende Schreibweise mit isset() |
|
Die Anweisung $name = $_POST["name"] ?? ""; entspricht weitgehend: |
|
|
if (isset($_POST["name"])) {
|
|
|
$name = $_POST["name"];
|
|
|
} else {
|
|
|
$name = "";
|
|
|
}
|
|
Sie kann auch mit dem Bedingungsoperator geschrieben werden: |
|
|
$name = isset($_POST["name"]) ? $_POST["name"] : "";
|
|
Der Null-Koaleszenzoperator ist somit eine kurze Schreibweise für eine Prüfung mit isset(). |
|
Nicht mit einer Prüfung auf "leer" verwechseln |
|
Der Operator ?? prüft nicht, ob ein Wert leer ist. Er prüft nur, ob der linke Ausdruck vorhanden und nicht null ist. Diese Werte werden daher übernommen: |
|
|
0
|
|
|
""
|
|
|
"0"
|
|
|
false
|
|
|
[]
|
|
Verwendung bei Formulardaten |
|
In obigen Programmen findet sich beispielsweise: |
|
|
$action = $_POST["action"] ?? "";
|
|
Beim ersten Aufruf der Seite wurde noch kein Formular abgesendet. Das Element $_POST["action"] ist dann möglicherweise nicht vorhanden. Durch ?? erhält $action trotzdem einen definierten Wert: "" Nach dem Absenden kann $_POST["action"] beispielsweise save enthalten. Dann wird dieser Wert übernommen: $action = "save"; |
|
Kurzschlussauswertung |
|
Der Operator ?? arbeitet mit einer Kurzschlussauswertung. Der rechte Ausdruck wird nur ausgewertet, wenn der linke Ausdruck nicht vorhanden ist oder null ergibt. |
|
|
$wert = $vorhanden ?? ersatzwertErmitteln();
|
|
Wenn $vorhanden einen Wert ungleich null enthält, wird die Funktion ersatzwertErmitteln() nicht aufgerufen. |
|
11.11 Joins |
|
Die obigen INNER JOIN-Anweisungen sorgen manchmal für Verwirrung. Deshalb hier eine kurze Erinnerung an die Joins der relationalen Theorie. |
|
Im obigen Beispiel stellen die Inner Joins die Beziehungen zwischen diesen Relationen her: |
|
|
FROM RechKoepfe AS rk
|
|
|
INNER JOIN Kunden AS k ON rk.KuNr = k.KuNr
|
|
|
INNER JOIN RechPos AS rp ON rk.ReNr = rp.ReNr
|
|
|
INNER JOIN Artikel AS a ON rp.ArtNr = a.ArtNr
|
|
|
|
|
Rechnungsköpfe mit Kunden verbinden |
|
|
INNER JOIN Kunden AS k ON rk.KuNr = k.KuNr
|
|
In RechKoepfe steht die Kundennummer. Der Kundenname steht dagegen in Kunden. Deshalb werden beide Relationen über KuNr verbunden. Dadurch können diese Angaben gemeinsam ausgegeben werden: |
|
|
k.KuNr
|
|
|
k.Name AS KuName
|
|
|
rk.ReNr
|
|
Rechnungsköpfe mit Rechnungspositionen verbinden |
|
|
INNER JOIN RechPos AS rp ON rk.ReNr = rp.ReNr
|
|
Ein Rechnungskopf enthält die allgemeinen Rechnungsdaten. Die einzelnen Positionen stehen in RechPos. Beide Relationen werden über die Rechnungsnummer verbunden. Dadurch stehen zu jeder Rechnung die zugehörigen Positionen zur Verfügung: |
|
|
rk.ReNr
|
|
|
rp.PosNr
|
|
|
rp.Anzahl
|
|
Rechnungspositionen mit Artikeln verbinden |
|
|
INNER JOIN Artikel AS a ON rp.ArtNr = a.ArtNr
|
|
In RechPos steht die Artikelnummer. Die Beschreibung und der Preis stehen dagegen in Artikel. Deshalb werden beide Relationen über ArtNr verbunden. Dadurch können diese Werte ausgegeben werden: |
|
|
a.ArtNr
|
|
|
a.Beschreibung
|
|
|
a.Preis
|
|
Muss es ausdrücklich INNER JOIN heißen? |
|
Das Wort INNER kann weggelassen werden. Diese beiden Schreibweisen bedeuten dasselbe: |
|
|
INNER JOIN Kunden AS k ON rk.KuNr = k.KuNr
|
|
|
JOIN Kunden AS k ON rk.KuNr = k.KuNr
|
|
JOIN allein wird standardmäßig als INNER JOIN behandelt: D.h. wird JOIN ohne weiteren Zusatz verwendet, ist damit standardmäßig ein INNER JOIN gemeint. Beide Schreibweisen sind daher gleichwertig. Die ausführliche Bezeichnung INNER JOIN macht jedoch deutlicher, dass nur Datensätze mit übereinstimmenden Verknüpfungswerten in das Ergebnis aufgenommen werden. |
|
Warum gerade ein INNER JOIN? |
|
Ein INNER JOIN liefert nur Datensätze, für die auf beiden Seiten der Verknüpfung passende Werte vorhanden sind. Eine Rechnungsposition wird also nur ausgegeben, wenn |
|
- zur Rechnung ein Kunde vorhanden ist,
- zur Rechnung mindestens eine Rechnungsposition vorhanden ist und
- zur Rechnungsposition ein Artikel vorhanden ist.
|
|
Das ist bei korrekt eingerichteten Fremdschlüsselbeziehungen normalerweise erwünscht. |
|
Wann wäre ein LEFT JOIN sinnvoller? |
|
Wenn auch Rechnungen ohne Rechnungspositionen ausgegeben werden sollen, müsste die Verbindung zu RechPos mit LEFT JOIN erfolgen: |
|
|
LEFT JOIN RechPos AS rp ON rk.ReNr = rp.ReNr
|
|
Soll außerdem eine Rechnungsposition auch dann erscheinen, wenn der zugehörige Artikel fehlt, müsste auch die nächste Verbindung ein LEFT JOIN sein: |
|
|
LEFT JOIN Artikel AS a ON rp.ArtNr = a.ArtNr
|
|
Eine mögliche Fassung wäre: |
|
|
SELECT k.KuNr, k.Name AS KuName, rk.ReNr,
|
|
|
DATE_FORMAT(rk.ReDatum, '%d.%m.%Y') AS RechnDatum,
|
|
|
rp.PosNr, a.ArtNr, a.Beschreibung, a.Preis, rp.Anzahl
|
|
|
FROM RechKoepfe AS rk
|
|
|
INNER JOIN Kunden AS k ON rk.KuNr = k.KuNr
|
|
|
LEFT JOIN RechPos AS rp ON rk.ReNr = rp.ReNr
|
|
|
LEFT JOIN Artikel AS a ON rp.ArtNr = a.ArtNr
|
|
|
ORDER BY rk.ReNr, rp.PosNr
|
|
Für eine Liste vollständiger Rechnungen ist die ursprüngliche Fassung mit drei INNER JOIN-Verknüpfungen aber sachlich richtig. |
|
Es gibt auch noch den Right Join, der entsprechend funktioniert. |
|
|
|