Herrlicher PHP-Rant

Da hat jemand einen äußerst umfangreichen PHP-Rant verfasst.

PHP: a fractal of bad design

Falls ein Leser also mit PHP arbeitet: Lesen! Und zwar nicht unbedingt deswegen, um nachher PHP abzuschwören (okay, das wäre aber auch keine schlechte Idee), sondern um die Stolpersteine besser zu wissen und in Zukunft besser zu umfahren.

Der Autor lässt hierbei wirklich nichts grade stehen und geht alles an: von Parser über die Exceptions bis hin zur Security. Es schadet übrigens natürlich nicht, diesen Beitrag dan auch selbst kritisch zu lesen (nicht alles, was PHP seiner Meinung nach schlecht/nicht kann, ist auch wirklich so schlimm), aber es sollte mindestens sensibel machen.

Und nebenbei kann man über so manches „Was haben sich die PHP-Entwickler dabei nur gedacht?“ entweder laut lachen oder weinen — je nach dem.

{DEVELOPERS SHAME DAY}

Cem hat kürzlich den Developers Shame Day ins Leben gerufen.

Als Stichtag würde ich den 3.11.2010 vorschlagen. Ich stelle mir vor, dass an diesem Tag alle Entwickler, die ein Blog oder eine Seite betreiben, ein kleines Stück Code präsentieren, dass aus heutiger (oder vielleicht auch damaliger) Sicht total hirnverbrannt ist. Ein Stück Code, dass uns selbst die Schamröte ins Gesicht steigen lässt. Dabei ist egal, ob es sich um PHP, JavaScript, CSS, HTML, Java, C oder sonst etwas handelt. Es muss nur von euch sein und es darf nicht verändert werden (umeventuell doch als total verrückter Hund dazustehen ). Ein kleiner erläuternder Text sollte natürlich auch nicht fehlen.

Und das ist mein Beitrag, wohl einer meiner ersten Gehversuche in PHP. Der Quellcode entstammt einer Datei namens „functions.inc.php“ [sic!]. Und es scheint so ;), also wäre es eine größere Anwendung gewesen, ich habe jedoch echt keine Ahnung mehr welche — vielleicht die erste PHP-Homepage in Eigenentwicklung? Die Datei hatte sich irgendwie auf meine „Informatik-Diskette“ [sic!] (Oberstufenkurs) verirrt, von der ich tatsächlich noch ein Abbild hatte. Der Rest der Programmiersünden muss entweder (m)einen Backup-Aufräumaktionen oder einem vor einigen Jahren aufgetreten Backupmediumfehler zum Opfen gefallen sein. Zu meiner Verteidigung kann ich wohl nur sagen hoffen, dass dies einer erste Spielversion war und nie „produktiv“ wurde.

Listing

Bitte in voller Länge genießen. Jede Zeile ist ein Genuss. *ankoppfass*

Unverändert, nur Benutzername/Passwort habe ich ausgeixt.

Datei: functions.inc.php (von 2002 oder 2003)

<?php
//conf.inc.php
//please don't change this file manually - go to your admin-area to change settings!!!
$listname="Meine Linkliste";
$listname2="Downloads";
$linkwidth="100%";
$addpagewidth="50%";
$catorder="name";
$incatsort="Hits";
$perpage="5";
$max_search="10";
$adminpw="a";
$max_desc_leng="500";
$html="ON";
$language="german.lang";
$timeformat="1";
 @include("global/$language"); // language-file
 @include("../global/$language"); // language-file
//connect.inc.php
// DON'T CHANGE THIS FILE MANUALLY - EDIT ONLY VIA ADMIN-AREA
$server="localhost";
$user="xxx";
$pass="xxx";
$mydb="xxx";
$db_prefix="xl_";
$db_prefix2="dl_";
function JPDiv ($a,$b) {
  $i=0;
  $j=0;
  while($j==0) {
    if(($a/($b*($i+1)))>1) {
      $i++;
    } else {
      $j = 1;
    }
  }
return $i;
}
function JPDatumZeit ($a) {
  $JPTag = array("Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag");
  $JPMonat = array("Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember");
  $JPDatumTag = $JPTag[date("w",$a)];
  $JPDatumMonat = $JPMonat[date("n",$a)-1];
  $JPDatumJahr = date("Y",$a);
  $JPZeit = date("H:i:s",$a);
  $b = $JPDatumTag . ", " . date("j") . ". " . $JPDatumMonat . " " . $JPDatumJahr . " [" . $JPZeit . "]";
return $b;
}
function JPDatumZeit2 ($a) {
  $JPDatumJahr = date("Y",$a);
  $JPZeit = date("H:i:s",$a);
  $b = date("j") . "." . date("w",$a)+1 . "." . $JPDatumJahr . " [" . $JPZeit . "]";
return $b;
}
class my_zugriff{
//Variablen für Zugangsdaten
var $user="xxx";		//Benutzername für den MySQL-Zugang
var $password="xxx";		//Passwort
var $host="localhost";	//Name (IP-Adr.) des Rechners mit MySQL
var $dbname="xxx"; 	//Name der Datenbank
//Weitere Variablen
var $db_verbindung=false; //Speichert die Verbindungskennung
var $sql_result=false; //Speichert die Kennung eines ausgewerteten SQL-Befehls
//Konstruktor definieren
function my_zugriff(){
   //Funktion verbinden wird bei Aufruf der Klasse ausgeführt
	 $this->verbinden();
}
//Falls keine Verbindung besteht,
//Verbindung aufbauen und Datenbank als Standard definieren
function verbinden(){
if ($this->db_verbindung==false){
   $this->db_verbindung = @mysql_connect($this->host, $this->user, $this->password);
   if(empty($this->db_verbindung)){
      $this->fehler("Beim Verbinden");
   }
   $auswahl = @mysql_select_db($this->dbname, $this->db_verbindung);
   if(empty ($auswahl)){
      $this->fehler("Beim Ausw&auml;hlen der DB");
   }
   return $this->db_verbindung;
}
}
//Gibt Fehlermeldung aus und beendet das Skript
function fehler($fehlerpunkt){
    echo $fehlerpunkt . " ist ein Fehler aufgetreten!<br>";
		echo mysql_error() . "<br>"; //Fehlerbezeichnung
		echo mysql_errno();	 	 		//Fehlernummer
		echo "</body></html>";		//Html-Tags schließen
		exit;
}
//SQL-Befehl ausführen
function sql_befehl($sql){
$this->sql_result = @mysql_query($sql, $this->db_verbindung);
if (empty($this->sql_result)){
	 $this->fehler("Beim Senden der Abfrage");
}
return $this->sql_result;
}
//Falls zuvor ein SQL-Befehl ausgeführt wurde,
//wird hier das Array mit den Datensätzen ausgegeben
function sql_daten(){
if(!empty($this->sql_result)){
		$sql_array=@mysql_fetch_array($this->sql_result);
		return $sql_array;
}else{
		$this->fehler("Beim Ausgeben der Datens&auml;tze");
}
}
//eig. Fkt
function sql_num_rows($sql){
$this->sql_befehl($sql);
$rows = MYSQL_NUM_ROWS($this->sql_result);
return $rows;
}
}
$db=new my_zugriff();
$db->sql_befehl("SELECT elem_string FROM elem_global WHERE elem_name='JPmyVersion'");
$myVersion_now = $db->sql_daten();
 $JPmyVersion = $myVersion_now[0];
?>

Selbstreflexion

  • Struktur: globale Variablen
  • Struktur/Aufbau: als Script noch okay, aber als Include? Oweh..
  • Includes mittendrin
  • Variabel- und Funktionsnamen multilingual
  • Inhalt der Funktionen..
  • kein gängiger Codestyle (ja, der Blog zeigt’s richtig an)
  • Inline-SQL(!)

Kurzum: What the hell…?

Weitere via Google oder Twitter oder Facebook.

Best practices in papayaCMS 5 – oder: Sinnvolle Aufteilung der Modulklassen.

Vor wenigen Tagen wurde papayaCMS 5.0 veröffentlicht. Da eventuell der ein oder andere mit dem System nun herumexperimentiert, wird man als Entwickler auch schnell Module entwickeln wollen. Vor allem bei größeren Modulen (bestehend aus vielen Ausgabe-Klassen) empfiehlt sich eine weit granulärere Aufteilung als zwingend vorgeschrieben ist.

Ich erspare mir die Erklärung der Basispapayasystems, dafür möge man bitte die entsprechenden Dokumentionen (PDF) konsultieren. Nur so viel sei gesagt: Um die Daten von der Datenbank auf den Bildschirm zu bekommen, braucht man grundsätzlich nur eine Ausgabeklasse (Seite, Box, usw.) und ein entsprechendes XSL-Template. Aber, aber..

Ineffiziente Strukturierung der Klassen
Ineffiziente Strukturierung der Klassen

Prinzipiell gilt: MVC-Kenner werden einen sehr leichten Einstieg haben, die anderen sollten bei Fragen zunächst diese Thematik verinnerlichen.

Damit eine Klasse Zugriff auf die Datenbank hat, muss es von base_db (sys_base_db.php) abgeleitet werden. Vor allem bei kleinen Seitenmodulen passiert es häufig, dass der Entwickler die Kurzversion nimmt: Generalisierung der Datenbankklasse, 1-2 Queries und XML-Ausgabe. Spätestens, wenn man die gleiche Funktionalität – etwa „last blog post titles“ – nicht nur als Seiten- sondern auch als Boxmodul benötigt, entsteht doppelte Code. Doppelter und redundanter Code. Schlecht. Ein guter Ansatz, um die Zuständigkeiten zu prüfen und zu ermitteln, ob Operationen sich nicht generalisieren und delegieren lassen können. Und ja, wir kommen dem Ziel MVC damit praktisch sehr nahe.

Alleine aus diesem praktische Grunde (und selbstverständlich ist die Verwendung von Delegation grundsätzlich in Modulsystem zu befürworten) empfiehlt sich der Einsatz einer Datenbasisklasse. Diese Klasse erweitert die o.g. Datenbankklasse, die Ausgabemodule instanziieren nur noch diese Basisklasse. Und für unsere Freunde der notorischen Delegationsverweigerer: Mit „Seitenklasse erweitert Datenbankklasse“ kommt ihr nicht weiter… und das gilt selbstverständlich auch für alle anderen Arten von Modulen. Tatsächlich ist hier Delegation unbedingt notwendig.

Ein weiterer Vorteil ist die bessere Wartbarkeit und Übersichtlichkeit (vgl. MVC). Selbstständlich werden auch schreibene Operationen in dieser Klasse implementiert. Der Einsatz von Interfaces oder mehrerer Datenbasisklassen steht jedem frei. Man sollte aber nicht mit Kanonen auf Spatzen schießen, ein komplexes System wird durch mehr Komplexität nicht wirklich einfacher.

Beispiel mit Kompetenzen: Delegation
Beispiel mit Kompetenzen: Delegation

Die weitere Aufgabe des Ausgabemoduls (vgl. Controller) ist natürlich auch die Ausgabe.. ja, kein Witz 😉 Auch hier gibt es eine ähnliche Konstellation wie bei den Datenbankabfragen, aber in der umgekehrten Sicht: Es gibt in größeren System häufig gleiche oder ähnliche Ausgaben. Auch hier kann die Faulheit einen schnell dazu verleiten, den XML-Code im Controller selbst zu schreiben – ist ja praktisch. Aber auch hier gilt: Spätestens wenn man selber [beim Refactoring] anfängt, und klasseninterne Helpermethoden zur Sub-XML-Generierung zu bauen oder gleichen XML-Code an mehr als 1-2 Stellen verwendet.. richtig: Delegation! Hier empfiehlt sich der Einsatz einer Ausgabebasisklasse. Jene Klasse hat im Wesentlichen nur eine Aufgabe: Für einen oder mehrere Parameter (idR eine Datenarraystruktur) ein geschäftsklassenabhängiges XML zu bauen. Es gibt weder eine Verbindung zur Datenbank noch zu anderen Ausgabemodulen.

Ebenfalls für diese Klassen gilt: Eine weitere strukturelle Aufteilung der Ausgabebasisklassen kann, muss aber nicht vorteilhaft sein.

Selbstverständlich lassen sich beide Klasse auch in Nicht-Ausgabemodulen verwenden, etwa einem Connectormodul. Das ist vor allem für Module interessant, die bestimmte Daten und/oder XML (aus welchen Gründen auch immer) weitergeben müssen.

Tipp 1: Vor allem bei mehreren Seiten- oder Boxmodulen kann es durchaus sinnvoll sein, eine gemeinsame Oberklasse (base_myplugin_content) zu erstellen. Hier lassen sich auch direkt die notwendigen Basisklassen korrekt instanziieren.

Komplexes Beispiel mit Oberklassen für Module
Komplexes Beispiel mit Oberklassen für Module

Tipp 2: Die Verwendung des Singleton-Pattern steht einem frei: In der Regel hat es aber wenig Sinn, die Datenbasisklasse mehrfach zu erstellen (kann vorkommen, wenn Boxen in Seiten des gleichen Modules auftauchen). Daher: getInstance().

In der Zusammenfassung:

  • Trennung der Zuständigkeiten und Verwendung von Delegation, aber: Generalisierung geht auch
  • Verwendung von Datenbasisklassen, bspw. base_myplugin_data extends base_db
  • Verwendung von Ausgabebasisklassen, bspw. base_myplugin_output extends base_object
  • delegierte Verwendung der o.g. Klassen in Seiten- und Boxmodulen

Database Exporter: Database Structure and Naming Analyzer

Mit dem Database Exporter 1.0 kann man die Struktur und die Namensgebung seiner Datenbank (mitsamt aller Tabellen und Feldern) analysieren.

Dabei reicht es, die Zugangsdaten (Host, Benutzername, Password) anzugeben, und man kann eine komplette Datenbank (hier: DATABASENAME) in ein Verzeichnis (hier: xml) zu exportieren.

$dbexporter->exportDbStructureToXML('DATABASENAME', 'xml');

Während des Schemaexports wird auch die Struktur analysiert und nach einigen Gesichtspunkten bewertet:

  • Fremdschlüsselabhängigkeiten werden automatisch gefunden, wenn Fremdschlüssel einmalig heißen; d.h. beispielsweise in der Tabelle postings heißt es user_id und nicht poster_id, wenn damit eigentlich auf users.user_id verwiesen wird (id wie in ROR wird nicht verwendet)
  • gefundene Abhängigkeiten werden zudem mit ihren Typen verglichen, wobei davon ausgegangen wird, dass der Typ im Hauptschlüssel (Primary Key) der richtige ist

PHP/zlib: Wie ermittelt man die unkomprimierte Dateigröße einer .gz-Datei?

Problem: Mit $fh = gzopen($fileName, 'r'); erhält man einen Filepointer auf die Datei, aber für gzread() benötigt man neben dem Filepointer auch eine Leseinheit (und in den meisten Fällen dürfte das der Inhalt sein). Allerdings kommen wir mit filesize($fileName) nicht weiter, denn damit erhalten wir nur die Größe der komprimierten Datei zurück.

Antwort: Nach einem Beitrag von zaotong auf php.net/gzread wird die Originaldatengröße in den letzten 4 Bytes der Datei gespeichert; alles was man machen muss, ist also die letzten 4 Bytes auslesen und als Buffergröße nutzen.

Snippet:

$fileName = 'compressed.gz';
// normal öffnen, windows-binärmode
$fh = fopen($fileName, "rb");
// springe zum 4. vorletzen byte
fseek($fh, -4, SEEK_END);
//lese die nächsten 4 bytes (also die letzten)
$readBuf = fread($fh, 4);
// binärdaten lesen, ist integer (4 Byte)
$gzSize = end(unpack("V", $readBuf));
// pointer wieder schliesen
fclose($fh);
// nun entkomprimieren, windows-binärmode
$fh = gzopen($fileName, "rb");
// nun können wir angeben, wieviel wirkliche daten wir lesen wollen
$content = gzread($fh, $gzSize);
// und brav wieder schließen
gzclose($fh);