javascript:void((function(){for(i=0,d=document.getElementsByTagName("div");i<d.length;i++)d[i].style.width=="410px"&&(d[i].style.width="810px")})())
Um in den Code-Blöcken Zeilennummern zu sehen, mach dasselbe Spiel mit diesem Code:
javascript:void((function(){var a=1,b="#888",c="br",d="code",e="span";function g(c,d){c.insertBefore(document.createElement(e).appendChild(document.createTextNode((""+(1000+a++)). substr(1)+"| ")).parentNode,d).style.color=b;}function h(a,b){return a.nodeName.toLowerCase()==b;}function i(a){if(h(a,c))g(a.parentNode,a.nextSibling);else if(a.nodeType==1)for(var b=a.firstChild;b;b=b.nextSibling)i(b);}for(var j=0,f=document.getElementsByTagName(d);j<f.length;j++){var k=f[j];a=1;if(h(k.parentNode,d)){g(k,k.firstChild);i(k);}}})());
Die in PHP eingebauten Klassen sind schon oft hilfreich. Ja, sogar meistens. Aber manchmal reichen sie nicht aus und man braucht zusätzliche oder leicht veränderte Funktionalität.
Als OOP-Fans bedienen wir uns der Vererbung und fügen die gewünschten Features in der abgeleiteten Klasse hinzu. Das klappt oft prima. Ja, sogar meistens. Aber manchmal, nur manchmal, ist PHP ein Drecksack und verhält sich total unlogisch, weil irgendwelche verkorksten Interna anders funktionieren als man denkt, ja sogar anders als sie sollten.
Nein, ich will nicht nur Frust ablassen, sondern auch eine mögliche Lösung vorstellen, aber dazu später mehr. Erstmal kommt ein Beispiel:
Für ein bestimmtes Projekt brauchen wir präzise Zeitangaben mit Millisekunden. Die entsprechende Ableitung von DateTime namens PreciseDateTime haben wir bereits fertig und klappt soweit. Beim Addieren und Subtrahieren von Zeiten sind wir aber auf DateInterval beschränkt und diese Klasse arbeitet wiederum ohne Millisekunden-Anteil. Da das bisher alles so schön geklappt hat, schreiben wir auch hier eine abgeleitete Klasse PreciseDateInterval.
Wie die Elternklasse DateInterval selbst nimmt der Konstruktor einen auf den ersten Blick merkwürdig anmutenden String entgegen, in dem die Zeitspanne definiert wird: P8Y7M6DT5H43M21S
Das steht für Period: 8 Years 7 Months 6 Days Time part: 5 Hours 43 Minutes 21 Seconds. Die Reihenfolge ist zwar festgelegt, aber alle Bestandteile außer dem P sind optional. Wenn aber eine Zeitskomponente angegeben wird, muss das T davor stehen. Nur so kann man 3 Monate (P3M) von 3 Minuten (PT3M) unterscheiden.
Da wir ja mit Millisekunden arbeiten wollen, fügen wir noch das X ein, um z. B. 4 Minuten 3 Sekunden und 210 Millisekunden angeben zu können: PT4M3S210X
Also ran an die Arbeit und schnell was zusammengehackt, was schon recht gut aussieht. (Bitte die Quellcode-Kommentare lesen!)
PHP-Code:
class PreciseDateInterval extends DateInterval {
public $ms = 0;
public function __construct ($pIntervalSpec) {
// pIntervalSpec fängt mit einem P an, dann kommen die optionalen Bestandteile
// in genau dieser Reihenfolge:
// ..Y Anzahl Jahre
// ..M Anzahl Monate
// ..W Anzahl Wochen
// ..D Anzahl Tage (wenn W angegeben ist wird D einfach ignoriert)
// T Trennzeichen - ist Pflicht, wenn eine Zeitkomponente vorhanden ist
// ..H Anzahl Stunden
// ..M Anzahl Minuten
// ..S Anzahl Sekunden
// ..X Anzahl Millisekunden (haben wir hinzuerfunden)
// Das ganze muss jetzt aufgedröselt werden, denn der Elternkonstruktor würde über
// den X-Teil meckern aber wir brauchen ihn, um die zusätzliche eigene Eigenschaft $ms
// zu setzen.
// _______________________________
// /5 ______ \
// _____ _____ _____ _____ / _____ _____ _____ |9____ \ \
// Subpatterns: /1 \ /2 \ /3 \ /4 \/ /6 \ /7 \ /8 \ |/10 \ \ \
$pattern = "P(\\d+Y)?(\\d+M)?(\\d+W)?(\\d+D)(T(\\d+H)?(\\d+M)?(\\d+S)?((\\d+)X)?)?";
// neue intervalSpec für den Elternkonstruktor zusammenbauen
$spec = "P";
if (preg_match("<^" . $pattern . "$>", $pIntervalSpec, $matches)) {
foreach ($matches as $i => $match) {
if ($i == 5) {
// die 5 umfasst alle folgenden, also auch den X-Teil,
// daher nehmen wir nur das T mit und grasen 6 bis 8 einzeln ab
$spec .= "T";
}
else if ($i > 0 && $i < 9) {
// den gesamten Ausdruck (0) und den X-Teil (9 und 10) ausschließen
// die anderen (außer der 5) werden rüberkopiert
$spec .= $match;
}
else if ($i == 10) {
// wir greifen uns den X-Teil, aber dazu kommen wir später
// $this->ms = (int) $match;
}
}
}
else {
// Regex hat nicht gematcht => pIntervalSpec ungültig
// soll sich der Elternkonstruktor damit rumschlagen
$spec = $pIntervalSpec;
}
parent::__construct($spec);
}
public function format ($pFormat) {
// hier passiert die Magie, dass neben den eingebauten Tokens für die Ausgabe
// %Y, %M, %D, %H, %I, %S, %R,
// %y, %m, %d, %h, %i, %s, %r und %a
// jetzt auch %X unterstützt wird, um an die Millisekunden ranzukommen
$result = "";
$isLiteral = false;
foreach (explode("X", $pFormat) as $k => $v) {
$vt = rtrim($v, "%");
$percents = strlen($v) - strlen($vt);
if ($k) {
$result .= $isLiteral ? "X" : str_pad($this->ms, 3, 0, STR_PAD_LEFT);
}
$result .= parent::format($vt) . str_repeat("%", floor($percents / 2));
$isLiteral = !($percents % 2);
}
return $result;
}
}
PHP-Code:
header("Content-Type: text/plain; charset=utf-8");
print_r(new PreciseDateInterval("P8Y7M6DT5H43M21S987X"));
Code:
PreciseDateInterval Object ( [ms] => 0 [y] => 8 [m] => 7 [d] => 6 [h] => 5 [i] => 43 [s] => 21 [invert] => 0 [days] => )
Fatal error: PreciseDateInterval::__construct(): Unknown property (ms) in ... on line 41
DateInterval ist eine von diesen Klassen, mit denen man keine anständige Vererbung hinbekommt, weil intern das OOP nur trickreich vorgegaukelt wird, die Klasse aber gar kein OOP kann. Der gleiche Betrug wie beim Zitronenfalter.
Wie lösen wir das Problem? Mit Delegation. Dabei speichern wir eine Instanz in einer privaten Eigenschaft und reichen Methodenaufrufe an diese durch. Das kann man durch die magische __call-Methode natürlich stark vereinfachen, anstatt für jede Methode eine eigene zu schreiben, die diese aufruft. Für Eigenschaften gilt dasselbe: Mittels __get und __set delegieren wir das an die gespeicherte Instanz.
PHP-Code:
class PreciseDateInterval {
private $interval = null;
public $ms = 0;
public function __construct ($pIntervalSpec) {
$pattern = "P(\\d+Y)?(\\d+M)?(\\d+W)?(\\d+D)(T(\\d+H)?(\\d+M)?(\\d+S)?((\\d+)X)?)?";
$spec = "P";
if (preg_match("<^" . $pattern . "$>", $pIntervalSpec, $matches)) {
foreach ($matches as $i => $match) {
if ($i == 5) {
$spec .= "T";
}
else if ($i > 0 && $i < 9) {
$spec .= $match;
}
else if ($i == 10) {
$this->ms = (int) $match;
}
}
}
else {
$spec = $pIntervalSpec;
}
// bis hierher alles wie gehabt, aber statt des Elternkonstruktors erzeugen und
// speichern wir eine Instanz
$this->interval = new DateInterval($spec);
}
// mit magischen Gettern und Settern kann man dann alle abgerufenen Eigenschaften an die
// gespeicherte Instanz durchreichen. __call brauchen wir nicht, da die einzige Methode
// format() ist und wir die selbst umimplementieren müssen.
public function __get ($pName) {
return $this->interval->$pName;
}
public function __set ($pName, $pValue) {
$this->interval->$pName = $pValue;
}
public function format ($pFormat) {
$result = "";
$isLiteral = false;
foreach (explode("X", $pFormat) as $k => $v) {
$vt = rtrim($v, "%");
$percents = strlen($v) - strlen($vt);
if ($k) $result .= $isLiteral ? "X" : str_pad($this->ms, 3, 0, STR_PAD_LEFT);
$result .= $this->interval->format($vt) . str_repeat("%", floor($percents / 2));
$isLiteral = !($percents % 2);
}
return $result;
}
}
PHP-Code:
header("Content-Type: text/plain; charset=utf-8");
$pdi = new PreciseDateInterval("P8Y7M6DT5H43M21S987X");
$pdi->m = 1;
echo $pdi->s, ",", $pdi->ms, "\n";
echo $pdi->format("%M:%S.%X");
Code:
21,987 01:21.987
Während man in anderen Fällen durchaus trotzdem von der Delegat-Klasse erben kann, um auch bei Typehinting die Anforderungen an die Methodensignatur zu erfüllen, geht das in diesem speziellen Falle nicht. Sobald wir angeben, dass wir von DateInterval erben, können wir wieder nicht mehr auf die eigenen Membervariablen zugreifen.
Geht es aber z. B. darum, eine Ableitung von DomElement zu schreiben, um das Verhalten von firstChild, nextSibling u. s. w. zu modifizieren (was auch nur über Delegation möglich ist), dann können wir trotzdem von DomElement erben und dank DomDocument::registerNodeClass können wir das DomDocument anweisen, immer unsere Klasse zu verwenden.