Prof. Dr.-Ing. Oliver Radfelder
Informatik / Wirtschaftsinformatik
Hochschule Bremerhaven

Kurz und Knapp: Php

Logge Dich auf hopper und dann in Deinem Docker-Container ein. Gehe in Dein Webverzeichnis /var/www/html/docker-ACCOUNT-web/ und öffne den Editor vim mit dem Dateinamen miniapp.php.

$ vim miniapp.php
und gib langsam und mit korrekter Formatierung den folgenden ersten Programmcode ein:
<?php
echo("Hello PHP-World");
?>

Dann gehe mit dem Browser auf https://informatik.hs-bremerhaven.de/docker-ACCOUNT-web/miniapp.php

Für das Überprüfen im Web sollten während der Testphase auch immer Fehlermeldungen angeschaltet sein:

<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

echo("Moin");

Variable sind in PHP zunächst nicht mit einem Typen versehen. Sie können auf Zeichenketten, Zahlen oder Objekte verweisen. Innerhalb von Zeichenketten lässt sich auf Variableninhalte direkt zugreifen. Zeichenketten werden mit dem Punkt-Operator verkettet:

<?php
// dies ist ein Kommentar
// nun eine Variable - mit $ im Namen:
$name = "Johannes Flattermann";
// Zugriff auf den Inhalt
echo("Hello $name, ");
// mit Integer-Variablen lässt sich wie
// üblich rechnen:
$zahla = 5;
$zahlb = 7;
echo("$zahla * $zahlb ergibt: ".($zahla*$zahlb));
?>

Schleifen gibt es, wie üblich, sowohl als while-Schleifen ...

<?php
$i = 0;
while($i < 10) {
  echo("$i, ");
  $i++;
}
?>

... als auch als for-Schleifen.

<?php
for($i=0; $i < 10; $i++) {
  echo("$i, ");
}
?>

Und auch Bedingungen sind wie gewohnt:

<?php
$zahl = 8;
if($zahl >= 6) {
  echo("groesser gleich 6");
} else {
  echo("kleiner 6");
}
?>

Arrays werden durch [..] erzeugt. Zugegriffen wird mit name[$index].

<?php
$tage = ["montag", "dienstag", "mittwoch"];
echo("der 3. Tag: ".$tage[2]);
?>

Wie so oft, gehören Arrays und Schleifen zueinander. Die Größe eines Arrays liefert die eingebaute Funktion count(...).

<?php
$tage = ["montag", "dienstag", "mittwoch"];
$tage[] = "donnerstag";

for ($i=0; $i < count($tage); $i++ ) {
  echo($tage[$i]." ");
}
// oder mit foreach
foreach($tage as $tag){
  echo($tag);
}
?>

Assoziative Arrays spielen in PHP eine besondere Rolle. Mit ihnen wird ein Wert zu einem Schlüssel assoziiert, so dass über den Schlüssel sehr effizient wieder auf den Wert zugegriffen werden kann.

<?php
$tage =      ["mo" => "montag", 
              "di" => "dienstag", 
              "mi" => "mittwoch"];

$tage["do"] = "donnerstag";
echo($tage["di"]);
unset($tage["di"]);

foreach($tage as $key => $value) {
   echo("$key: $value\n");
}
?>

Das übliche Lesen und Schreiben über Stdin und Stdout:

<?php
$f = fopen( 'php://stdin', 'r' );
while(! feof($f)) {
  echo(fgets($f));
}

Mit assoziativen Arrays werden auch Werte aus Formularen im Web übergeben. GET-Argumente werden als Schlüsselwert-Paar an die URL hinter einem Fragezeichen angehängt und in der Variablen $_GET verfügbar gemacht:

<?php
// https://informatik.hs-bremerhaven.de/docker-ACCOUNT-web/miniapp.php?name=abcdef
header('Content-Type: text/plain'); // nicht absolut notwendig

$name = $_GET["name"];
echo("Sie sind $name ?");
?>

Der default Content-Type ist text/html. Wollen wir ihn ändern, setzen wir ihn mit der header-Funktion.

Oft werden Eingaben im Web aber aus Formularen heraus vermittels POST übertragen. Ein Formular in einer einfachen HTML-Datei eingabe.html könnte so aussehen:

<!doctype html>
<html>
<body>
  <form action="miniapp.php" method="post" >
    Name:  <input type="text" name="name" />
    Alter: <input type="text" name="alter" />
    <input type="Submit" value="Absenden" />
  </form>
</body>
</html>

Der entsprechende Verarbeitungscode auf dem Server liest dann die Werte aus dem assoziativen POST-Array:

<?php
// Rufen Sie 
// https://informatik.hs-bremerhaven.de/ACCOUNT/eingabe.html
// auf, füllen Sie das Formular aus und senden Sie es ab ...
$name = $_POST["name"];
$alter = $_POST["alter"];
echo("Hallo $name, Sie sind also $alter Jahre alt?");
?>

Um mit curl zu Test- und Validierungszwecken auch Post-Anfragen an den Webserver zu senden, nutzen Sie für die einzelnen Parameter -F.

# von hopper aus
curl -F "name=paul" -F "alter=18" https://informatik.hs-bremerhaven.de/docker-demo-web/demo.php

Der Zugriff auf Dateien funktioniert am einfachsten mit den Funktionen file(...), die ein Array zurückliefert, file_get_contents(...), die den ganzen Inhalt als String einliest und file_put_contents(...), die einen String schreibt, bzw. anhängt. Unsere Umgebung ist so eingerichtet, dass in das Verzeichnis private unter Eurem Webverzeichnis auch der User www-data, unter dem der Webserver läuft, schreiben darf.

Sollte das Verzeichnis private nicht existieren, lege es wie folgt an:

(umask 007; mkdir /var/www/html/$USER-web/private/)

Sollte es existieren, aber nicht schreibbar sein, passe die Dateirechte entsprechend an:

chown :www-data /var/www/html/$USER-web/private/
chmod g+w /var/www/html/$USER-web/private/
<?php
// Rufe 
// https://informatik.hs-bremerhaven.de/ACCOUNT/eingabe.html
// auf, fülle das Formular aus und sende es ab ...
$name = $_POST["name"];
$alter = $_POST["alter"];
file_put_contents("private/namen.txt", $name." ".$alter."\n", FILE_APPEND);
$eintraege=file("private/namen.txt");
for($i=0;$i < count($eintraege); $i++){
   echo($i.": ".$eintraege[$i]."<br>");
}
?>

Da nun eine Datei sich mittels file(...) in ein Array lesen lässt, brauchen wir nur noch die Möglichkeit, eine Zeile in Tokens zu zerlegen:

<?php
$line="das:ist:eine:zeichenkette";
$arr=explode(":",$line);
var_dump($arr);
$str=implode(":",$arr);
echo($str);
?>

Das Gegenstück zu explode ist implode und erwartet einen Trenner und ein Array.

Für kleine Experimente ist die Funktion var_dump recht gut geeignet. Sie gibt den Inhalt einer Variablen auf der Standardausgabe aus. Im Übrigen muss ein PHP-Script nicht im Web aufgerufen werden. Auf der Kommandozeile kann dem Interpreter ebenfalls ein Script mitgegeben werden:

php script.php

Das Einzige, dessen es nun noch bedarf, um eine vollständige Webanwendung zu erstellen, ist das Umgehen mit Sessions. Auch Sessions sind assoziative Arrays, in denen sich allerdings über Requests hinweg Werte halten lassen. Dazu muss jeweils einmal die Funktion session_start() in jedem Script aufgerufen werden. Passend dazu gibt es noch eine Funktion session_destroy.

<?php
session_start();
if (! isset($_SESSION['number'])) {
  $_SESSION['number']=0;
}

$_SESSION['number']++;
echo($_SESSION['number']." Zugriffe in dieser Session\n");
// test with :
// curl -c /tmp/$USER.jar -b /tmp/$USER.jar https://informatik.hs-bremerhaven.de/docker-demo-web/miniapp.php?[1-10]
?>

Zur besseren Organisation des Codes kann man sich auch eigene Funktionen schreiben:

<?php
function isValid($name, $alter) {
  if (strlen($name) > 0 && $alter >= 18 ) return TRUE;
  else return FALSE;
}

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

if (isValid($name, $alter)) {
  echo("Das ist in Ordnung...");
} else {
  echo("Das wird nichts...");
}
?>

Und das ergibt natürlich erst so richtig Sinn, wenn Funktionen in Dateien ausgelagert werden, die dann in mehreren anderen Scripten includiert werden. Schreibe die Funktionsdefinition von isValid in eine Datei mit dem Namen funktionen.inc.php.

<?php
include("funktionen.inc.php");

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

if (isValid($name, $alter)) {
  echo("Das ist in Ordnung...");
} else {
  echo("Das wird nichts...");
}
?>

Zur Sicherheit kann man die Bibliotheks-Dateien in dem Verzeichnis private unterbringen, damit sie nicht direkt aufgerufen werden können. Allerdings sollten sowieso nur Funktionsdefinitionen enthalten sein.

Es existiert mit composer ein recht ausgefeilten Dependency Manager für externe Bibliotheken. Wer ernsthaft Software mit php entwickeln will, wird sich damit auseinandersetzen müssen.

Bisweilen braucht man noch - jede Scriptausführung ist ein eigener Prozess - einen Mechanismus zum exklusiven Locking. Die Funktion fopen(...) öffnet eine Datei und erzeugt sie gegebenenfalls. Mit der Funktion flock(...) und dem zweiten Parameter LOCK_EX wird ein exklusiver Lock auf diese Datei gelegt. Zum Ende des Scripts wird dieser Lock automatisch wieder freigegeben - was aber auch bewusst wieder mit flock und dem zweiten Argument LOCK_UN erzielt werden kann.

<?php
$lockfile = fopen("lock","c");
$lock = flock($lockfile, LOCK_EX);
usleep(1000000); //microseconds
flock($lockfile, LOCK_UN);
fclose($lockfile);
echo("ok");
?>

Im Grunde ähnelt flock dem bekannten atomaren Zugriff mit mkdir. Allerdings ist hier der entscheidende Vorteil, dass flock selbständig wartet, bis es den Lock bekommt. Ein kleines Experiment macht den Wert deutlich: Wir erzeugen eine Datei und benutzen sie in einer For-Schleife, um einen Lock zu holen, den Inhalt einer anderen Datei (counter.txt) zu lesen, hochzuzählen und zurückzuschreiben.

<?php
$fp=fopen("/dev/shm/counterlock.txt","a+");
for($i=0;$i<1000;++$i){
  flock($fp,LOCK_EX);
  $content=file_get_contents("/dev/shm/counter.txt");
  $num=intval($content);
  echo($num."\n");
  $num++;
  file_put_contents("/dev/shm/counter.txt","".$num);
  flock($fp,LOCK_UN);
}
fclose($fp);

Wenn wir nun zunächst 0 in die Datei counter.txt schreiben und dann 10 Prozesse mit dem Script starten, erwarten wir am Ende die Zahl 10000 in der Datei. Das funktioniert auch verlässlich. Lassen wir jedoch die beiden flock-Aufrufe weg, ist das Ergebnis nicht vorhersehbar, weil das Schreiben und Lesen nicht zusammen atomar erfolgt.

Die Funktion flock ist leider nicht Posix-kompatibel. Allerdings ist die Funktion über Programmiersprachen hinweg unter Linux und BSD verfügbar. Insbesondere gibt es auch das Kommando flock, das mit dem flock-Aufruf von php kompatibel ist. Hier wird dann der Unterschied zwischen Prozessaufruf und Funktionsaufruf einer Betriebssystem-Funktionalität besonders deutlich.

Was nicht fehlen darf, ist ein Dateiupload...

<!doctype html>
<html>
<body>
<form action="phpupload.php" method="post" enctype="multipart/form-data">
    Datei auswaehlen:
    <input type="file" name="diedatei" >
    <input type="submit" value="hochladen" name="submit">
</form>
</body>
</html>
Auf Serverseite liegen nach einem Upload die Informationen in $_FILES als assoziativem Array vor. Für einen sicheren Dateiupload wären manche Zeilen mehr zu schreiben: die Dateigröße zu prüfen, der Datentyp, der Dateiname etc. Allerdings sollte ohne spezielle Verarbeitung - gegebenenfalls in einem gesonderten Prozess - eh keineswegs eine Datei wieder ausgeliefert werden oder gar als php-Datei verarbeitet werden.

<?php
  $basename=basename($_FILES["diedatei"]["name"]);
  $filename=str_replace("/","_", $basename); // minimum ...
  move_uploaded_file($_FILES["diedatei"]["tmp_name"], "private/phpupload/".$basename);
?>

Wer das ausführlicher diskutiert haben will, schaue mal bei wiki.selfhtml.org.

Und auch hier ist curl das kleine Wunderkind für Automatisierung und Tests:

curl -F "diedatei=@rabbit.png" https://informatik.hs-bremerhaven.de/docker-demo-web/phpupload.php

Wenn man mal etwas in php an einen auf einem anderen Server laufenden Dienst schicken möchte, und das Ergebnis soll an den Client zurückgeschickt werden, hilft es, zu wissen, dass sowohl echo als auch stream_get_contents Binärdaten verarbeiten können, womit sich wie in diesem Fall mittels LaTeX, ncat und php ein kleiner Dokumentendienst realisieren lässt. Alternativ zu dem praktischen stream_get_contents kann hier auch mit $line = fgets($client) gearbeitet werden, um sich String-Zeilen geben zu lassen.

<?php
header("Content-type: image/png");
$client = stream_socket_client("tcp://localhost:11000", 
                               $errno, $errorMessage);
fwrite($client,$_GET['doc']."\n");
fwrite($client,"\\\\renewcommand{\\\\slogan}{".$_GET['val']."}\n");

echo(stream_get_contents($client));
fclose($client);
?>
Auf dem anderen Rechner (oder dem selben), dem Server, läuft dann ein kleiner Dienst mit ncat:
ncat -l -k -e ./worker.sh  11000
Dieser wiederum ruft, wenn von dem Web-Server aus Kontakt aufgenommen wird, das Script worker.sh auf:
#!/bin/bash 
dir=$(mktemp)
cd "$dir" || exit
read -r commandline
read -r content
echo "$content" > data
name=$commandline
pdflatex -interaction=nonstopmode -halt-on-error $name.tex >>log 2>>&1
pdftocairo -r 150 -png -singlefile $name.pdf
cat $name.png
cd /tmp
rm -rf "$dir"

Falls Du einmal auf Deine Mariadb-Datenbank zugreifen willst, sorge dafür, dass in dem private-Ordner in dem Webverzeichnis eine Datei dbconnection.inc.php liegt, die nur von dem www-data-User und Dir lesbar ist. mariadb und mysql sind hier als synonym zu betrachten. Der Datenbanktreiber heißt in jedem Fall mysqli.

<?php
$host       = "mysql-server";
$user       = "USERNAME";
$password   = "PASSWORD";
$database   = "USERNAME_db";
Die konkreten Daten entnimm der Datei .my.cnf in Deinem Homeverzeichnis. Dann kannst Du eine PHP-Datei direkt in dem Webverzeichnis ablegen, die wiederum diese private Datei importiert und eine Verbindung zum Datenbankserver öffnet.
<?php
include("private/dbconnection.inc.php");

$conn = mysqli_connect($host, $user, $password, $database);
if (!$conn) {
    die("Connection failed: " . mysqli_connect_error());
}

$sql = "select * from demo";
$result = mysqli_query($conn, $sql);
while($row = mysqli_fetch_assoc($result)) {
  echo($row['demo_id']." ".$row['name']."\n");
}

mysqli_close($conn);

Andere Funktionen für das Arbeiten mit mariadb-Datenbanken, die nachzuschlagen lohnen, sind mysqli_insert_id, mysqli_affected_rows und mysqli_fetch_all.

Eine Tabelle in Ihrer Datenbank können Sie einfach auf hopper mit dem Kommandozeilentool mysql oder mariadberstellen. Legen Sie dazu eine Textdatei mit dem Namen createdemodb.sql an:
DROP TABLE IF EXISTS demo;
CREATE TABLE demo (
  demo_id INT NOT NULL AUTO_INCREMENT,
  name VARCHAR(100),
  PRIMARY KEY (demo_id));

INSERT INTO demo (name) VALUES
  ('peter'),('paul'),('mary');
Rufe dazu mariadb wie folgt auf:
mariadb < createdemodb.sql
Prüfe gegebenenfalls mit mariadb und einem kurzen Aufruf, ob die Datenbank korrekt angelegt wurde:
mariadb -s -e "SELECT * FROM demo;"

Neu seit php 8.2 ist mysqli_execute_query(), womit sich prepared Statements elegant formulieren lassen. In der als String formulierten SQL-Anfrage werden Fragezeichen als Platzhalter angegeben, die mit den Werten aus dem im zweiten Argument übergebenen Array ersetzt werden. Diese Form schützt insbesondere vor SQL-Injection Angriffen. Als Ergebnis wird bei einer Anfrage (select) ein Cursor zurückgegeben, mit dem sich mit einer typischen while-Schleife das Ergebnis zeilenweise durchlaufen lässt. In jeder Zeile wird über einen assoziativen Zugriff auf die Werte der Spalte zugegriffen.

<?php
$result = mysqli_execute_query($conn, "select * from demo where id=? or name=?", [1,"Peter"]);
while($row = mysqli_fetch_assoc($result)) {
  echo($row['demo_id']." ".$row['name']."\n");
}

Bilder lassen sich auf unterschiedliche Arten erzeugen. Hier die eher klassische Variante mit der GD-Bibliothek. Für das Nutzen einer Schrift können Sie die ttf-Font Datei in das Arbeitsverzeichnis kopieren.

<?php
header("content-type: image/png");
$txt="Hello Wörld";
if(isset($_GET['text']))
  $txt=$_GET['text'];

$im=imagecreatetruecolor(200,200);
$background_color=imagecolorallocate($im,200,200,255);
imagefilledrectangle($im,0,0,200,200,$background_color);
$text_color=imagecolorallocate($im,233,0,0);

imagettftext($im,20,0,25,40,$text_color,"./font.ttf",$txt);
imagepng($im);
imagedestroy($im);

Redis als Session-Store

Für eine skalierbare Umgebung, bei der mehrere Apache-Instanzen hinter einem Loadbalancer wie haproxy gemeinsam eine Webanwendung bedienen, reicht der Standard-Session-Store im Dateisystem nicht mehr aus.

In der jeweiligen php.ini unter /etc/php/VERSION/apache2/ lässt sich für Experimente die eigene redis-Instanz konfigurieren:


# session.save_handler = file # auskommentieren
session.save_handler = redis
session.save_path = "tcp://localhost:6379?auth=DeinRedisMariadbPasswort"

Nach Stop und Start des apache wird dann redis als Session-Store genutzt, was sich gut mit redis-cli monitor prüfen lässt. Denkt daran, dass die php.ini nach dem Booten nachts wieder in dem Ursprungszustand ist. Ihr müsst also selbst dafür sorgen, dass sie beim Starten angepasst wird.

Sonstiges

Wie so oft benötigen wir bisweilen ein Instrument zur Zeitmessung:

<?php
$start=microtime(TRUE);
$result="";
for($i=0;$i<1000000;++$i){
  $result.="x";
}
$ende=microtime(TRUE);
printf("took:%.3F secs\n",($ende-$start));

Filesystem-Operationen, die regelmäßig gebraucht werden:

<?php
if(!is_dir("tempdir")){
  mkdir("tempdir");
}

if(!file_exists("tempdir/temp.txt")){
  touch("tempdir/temp.csv");
}

//csv file handling
$file = fopen("tempdir/temp.csv","w");
fputcsv($file,[1,'feldmit"doublequote']);
fputcsv($file,[2,'feldmit""doublequote']);
fclose($file);

$file = fopen("tempdir/temp.csv","r");
while(! feof($file)){
  print_r(fgetcsv($file));
}
fclose($file);


copy("tempdir/temp.csv","result.csv");


//to delete a file
unlink("tempdir/temp.csv");
rmdir("tempdir");
$fn="file-".date(DATE_ISO8601);

printf("%10s %10d %10d\n","result.csv",filesize("result.csv"),filemtime("result.csv"));

In diesen Zeiten bedarf es regelmäßig der Konvertierung von und nach JSON:

<?php
$json = '{"a":1,"b":2,"c":3,"d":4,"e":5}';

$phpvar=json_decode($json, true);
var_dump($phpvar);
echo(json_encode($phpvar));

Weiterführendes

Datenschutz / Impressum