Schlagwort: PHP

Das Active-Record Prinzip

27. Mai 2010

Das Active-Record Prinzip
Im Rahmen des Kursus objektorientierte Programmierung mit PHP und MySQL wurde in den vergangenen Tagen über die verschiedenen Formen der Objektrelationalen Abbildung (ORM) diskutiert. Das hier am häufigsten gewählte Prinzip ist nach wie vor Active-Record. Viele Frameworks wie bspw. CakePHP, PHP on TRAX oder Doctrine nutzen diese Form des ORM. Grund genug, sich einmal genauer anzusehen, was sich hinter Active-Record verbirgt, und welche Möglichkeiten PHP bietet, dieses Prinzip umzusetzen.

Active-Record basiert auf der Annahme, dass es ein Domänen-Modell gibt, bei dem zu jeder Klasse des Objektmodells eine korrespondierende Datenbanktabelle existiert. Jede Spalte der Tabelle soll dabei als eine Eigenschaft/Attribut des Domänenobjektes abgebildet werden. Das Domänenobjekt wird zudem um die folgende Methoden erweitert: Erstellen eines Datenbankeintrages aus den Werten des Objektes (INSERT), Erstellen von Objekten auf Basis einer Datenreihe (SELECT), Aktualisieren eines Datensatzes (UPDATE) und Löschen eines Datensatzes (DELETE) . Zusammengefasst werden diese Methoden unter dem Begriff CRUD-Operationen (create, read, update und delete).
Ein Active-Record Objekt enthält demnach sowohl die Geschäftslogik des Domänenobjekts als auch die Persistierungslogik (Datenbankoperationen). Active-Record eignet sich immer dann besonders gut, wenn die Domänenlogik nicht zu komplex ist und wenn ein isomorphes Schema vorliegt, d.h. die Active-Record Objekte tatsächlichen Datenbanktabellen entsprechen. Der Objektentwurf ist damit eng mit dem Datenbankentwurf verbunden, “einseitige” Änderungen können bei der (Weiter-)Entwicklung daher leicht zu Problemen führen.

Relationen bei Active-Record

Zwischen den einzelnen Objekten können Beziehungen (Relationen) bestehen. Die Beziehung zwischen zwei Active-Record-Objekten steht in direktem Zusammenhang mit der Beziehung zwischen den Datenbanktabellen, die diese Domänenobjekte repräsentieren. Aus Sicht der Datenbank gibt es drei Beziehungstypen: eins-zu-viele (engl.: one-to-many, 1:n), eins-zu-eins (engl.: one-to-one, 1:1) und viele-zu-viele (engl.: many-to-many, n:m). Die meisten Umsetzungen von Active-Record unterteilen dabei wie folgt:

  • BELONGS_TO (gehört): Wenn die Beziehung zwischen den Tabellen A und B eins-zu-viele ist, dann gehört B A (z.B. Nachtricht gehört User).
  • HAS_MANY (hat viele): Wenn die Beziehung zwischen der Tabelle A und B eins-zu-viele ist, dann hat A viele B (z.B. User hat viele Nachricht(en)).
  • HAS_ONE (hat ein): Wenn A höchstens ein B hat (z.B. User hat höchstens ein Profil)
  • HAS_AND_BELONGS_TO_MANY oder MANY_MANY (viele viele): Dies entspricht der viele-zu-viele-Beziehung (n:m-Beziehung) bei Datenbanken. Es wird eine zusätzliche Verbindungstabelle benötigt, um die viele-zu-viele Beziehung auf jeweils eine eins-zu-viele-Beziehungen herunterzubrechen. (z.B. die Kontaktbeziehung zwischen User(n))

Die Implementierung dieser Beziehungen in Active-Record kann recht problematisch sein. Wenn man sich eine n:m Relation als SQL Statement anschaut, dann hat man es immer mit einem komplexeren JOIN zu tun. Ein solches komplexeres Statement hat den Vorteil, dass man über die so zusammengesetzen Spalten leicht filtern kann, und den Nachteil, dass man jede Menge an redundanten Daten erhält – die zudem auch noch den entsprechenden Domänenobjekten zugeordnet werden müssten.
In solchen Fällen wäre die Teilung des Statements in mehrere SQL Abfragen effizienter, die Zuordnung zu den Objekt ist leichter, redundante Daten (weil Spalten bei 1:n immer aufgefüllt werden) gäbe es nicht. Welche der beiden Ansätze der richtige ist, lässt sich nicht beantworten. Grundsätzlich gilt, je weniger Anfragen an die Datenbank, desto weniger Last, aber komplexe Anfragen kosten mehr Zeit als einfache. Einige Frameworks gehen soweit, dass sogar beide Varianten angeboten werden (bspw. Yii – Web Programming Framework). Andere Implementierungen von Active-Record verzichten ganz auf eine “automatische” Relation der Domänenobjekte und bilden die Beziehungen über virtuelle Attribute ab.

Active-Record mit PHP 5.3 und PDO

Der folgende Beispielcode zeigt und erläutert eine prototypische Umsetzung des Active-Record Prinzips. Es handelt sich dabei nur um ein “Proof of concept“, da es im Rahmen des Kurses nicht möglich ist, sich eingehender mit einem der hier bereits erwähnten Frameworks zu beschäftigen. Der dargestellte Code ist nicht vollständig. Bei Interesse an diesem Entwurf einfach eine E-Mail senden.

Bei Active-Record geht es zum einen darum, Datensätze zu verwalten, also (create, update, edit und delete), mindestens ebenso wichtig aber ist das Finden von Datensätzen, erst über das Finden sind update, edit und delete überhaupt möglich. Das finden sollte sich aber nicht auf ein Domänenobjekt beziehen, sondern unabhängig von einer konkreten Instanz funktionieren, die Klasse müsste also sowohl statisch als auch dynamisch aufrufbar sein.

Der Konstruktor – für ein spezifisches Domänenmodell

Der Konstruktor wird nur aufgerufen, wenn ein neues Objekt erzeugt wird, dabei kann man wahlweise eine ID übergeben, die dazu führt, dass diese Instanz mit den Daten aus der entsprechenden Tabelle gefüllt wird, oder einen array an Werten, um die Attribute zu setzen (für create und update).

public function __construct() {
  //setzen des namens - dient als eine moegliche basis
  //fuer den datenbanknamen
  self::$name = get_class($this);
  //pruefen, ob der constructor argumente erhalten hat
  $numargs = func_num_args();
  if($numargs > 0) {
    $args = func_get_args();
    //handelt es sich um einen integeren wert, ist dieses
    //ein primaerschlussel, und das objekt wird mit den
    //entsprechenden daten gefuellt
    if(is_int($args[0])) {
     $this->__setById($args[0]);
    } else {
     //in allen anderen faellen uebernimmt eine spezielle
     //funktion das fuellen der eigenschaften
     call_user_func_array(
      array($this, '__setter'),
      func_get_args()
     );
   }
 }
}
 
/* Aufgerufen ueber den Konstruktor,
 * erwartet eine ID, uebertraegt die
 * Datenbank abfrage in die aktuelle
 * Objektinstanz
 */
protected function __setById($id) {
  //fuer den fall, dass es noch keine datenbank verbindung
  //gibt, wird eine erstellt, db ist eine statische
  //klasse, die ueber den connect ein PDO objekt liefert
  if(!self::$_db) {self::$_db = db::connect();}
  //ermittlung des korrekten tabellen namens
  $name = self::tableName();
  $sql = 'SELECT * FROM '.$name.' WHERE '.self::$_pk .'=?';
  $result = self::$_db->prepare( $sql );
  if(!$result->execute(array($id))) {
   $error = $result->errorInfo();
   throw new Exception('Could not initialize - reason:'.$error[2]);
  }
  //pdo fetch_into - das ergebnis wird direkt in diese
  //objekt instanz uebertragen
  $result->setFetchMode(PDO::FETCH_INTO,$this);
  $result->fetch();
}
 
/*
 * Der setter kann mit einer Kombination von
 * einzelwerten (eigenschaft/wert) oder einem
 * bzw. zwei arrays umgehen, die so entstehende
 * gesamtinformation wird an die eigentlichen
 * attribute uebertragen
 */
protected function __setter($one, $two = null) {
  $data = array();
  if (is_array($one)) {
    if (is_array($two)) {
     $data = array_combine($one, $two);
    } else {
     $data = $one;
    }
  } else {
   $data = array($one => $two);
  }
  foreach ($data as $name => $value) {
    $this->$name = $value;
  }
}

Wer bin ich eigentlich?

Die Active-Record Klasse ist eine Basis Klasse, von der dann die konkreten Domänenmodelle abgeleitet werden. Da sie sowohl statisch als auch dynamisch aufgerufen werden kann, ist es für die Basis Klasse von enormer Wichtigkeit zu wissen, “wer da aufruft” – denn nur über diese Information kann die richtige Tabelle angesprochen werden und können die korrekten Objekte initialisiert werden (bei find). Beim statischen Aufruf ist das vor PHP 5.3 so einfach nicht möglich gewesen, seit 5.3 gibt es die Funktion get_called_class, die genau das leisten kann.

/* Ermittelt den korrekten tabellnamen
 * anhand des Klassennamens und ggf.
 * einem $table Attribut
 */
protected static function tableName() {
 //klassenname, funktioniert auch bei statischem aufruf
 $name = get_called_class();
 //fuer den fall, dass sich tabellenname und klassenname
 //unterscheiden gibt es einen table attribut in den
 //kinderklassen.
 $table = (!empty(self::$table)) ? self::$table : $name::$table;
 if(!empty($table)) {
  return $table;
 }
 //sonst ist klassenname gleich tabellenname
 return $name;
}

Die Magie – der Einsatz der PHP magics

In der Basisklasse werden die Getter und Setter für die einzelnen Attribute “einfach” über die entsprechenden Magics von PHP kontrolliert. So handelt es sich bei den Domänenobjekt Eigenschaften in Wirklichkeit gar nicht um echte Attribute sondern um einen assoziativen Array.

//setzen von attributen
public function __set($property, $value) {
  // kein setzen eines primary keys (id), wenn schon vorhanden!
  if( $property == self::$_pk &&
   $this->_data[self::$_pk] !== NULL) {
    throw new Exception(
     "Setting '".strtoupper(self::$_pk)."' is immutable."
    );
  } elseif(method_exists($this,"set_{$property}")) {
     //fuer den fall, dass es eine definierte methode gibt...
     $methodName = "set_{$property}";
     $this->$methodName($value);
  } elseif (array_key_exists($property, $this->_data)) {
     // wenn es eine entsprechende eigenschaft gibt...
     $this->_data[$property] = $value;
  } else {
    //unbekannte eigenschaft...
    throw new Exception("Unkown property: '$property'.");
  }
}
 
//abholen von attributen...
public function __get($property) {
  if(method_exists($this,"get_{$property}")) {
   //fuer den fall, dass es eine definierte methode gibt...
   $methodName = "get_{$property}";
   return $this->$methodName();
  } elseif (array_key_exists($property, $this->_data)) {
   //wenn es das attribut gibt...
   return $this->_data[$property];
  } else {
   //sonst fehler
   throw new Exception("Unkown property: '$property'.");
  }
}
 
//pruefen ob es die eigenschaft gibt
public function __isset($property) {
 return isset($this->_data[$property]);
}
 
//loeschen der eigenschaft
public function __unset($property) {
  if (isset($this->_data[$property])) {
    unset($this->_data[$property]);
  }
}

Und noch mehr Magie, über __call können Methoden verarbeitet werden, die eigentlich nicht aufrufbar sind, also eigentlich nicht vorhanden, oder geschützt. Die Methoden zum Bearbeiten der Datensätze sind daher protected, sollen nur dann aufrufbar sein, wenn es eine Objektinstanz gibt. Die Methoden zum Finden von Datensätzen werden über __callStatic verarbeitet. Die Idee (nicht meine), eine Methode wie bspw. findAllByTitelAndYear(‘Titel’,2002) soll umgelenkt werden auf eine allgemeine Find-Methode, die ein entsprechendes SQL Statement zusammensetzt.

/* Zusammensetzen von SQL Conditions aus
 * dem Methodennamem, verarbeitet bisher
 * Syntax wie findByTitel_And_Year oder
 * findAllByYear_Or_Type
 */
public static function __callStatic($name,$arguments) {
 if(strpos($name, 'findBy')===0 || strpos($name, 'findAllBy')===0){
  $query = array();
  if(strpos($name, 'findBy')===0) {
   $conditions = substr(strtolower($name),6);
  } else {
   $conditions = substr(strtolower($name),9);
  }
  $or = (strpos($conditions, '_or_') !== false);
  if ($or) {
   $conditions = explode('_or_', $conditions);
  } else {
   $conditions = explode('_and_', $conditions);
  }
  foreach($conditions as $num=>$condition) {
   $query[$condition] = $arguments[$num];
  }
  if($or) {
   $query['OR'] = $query;
  } else {
   $query['AND'] = $query;
  }
  if(strpos($name, 'findBy')===0) {
   return self::find($query);
  } else {
   return self::findAll($query);
  }
 } else {
  throw new Exception("Unkown function: '$name'.");
 }
}
 
public function __call($name,$arguments) {
 //schuetzen vor statischem zugriff...
 if($name=="delete" || $name=="save") {
  $this->$name();
 }elseif(strpos($name, 'get')===0){
  //abfangen von getter methoden
  $props = explode('_',substr(strtolower($name),3));
  $r = array();
  foreach($props as $prop) {
	 $r[$prop] = $this->{$prop};
  }
  return $r;
 } elseif($name=='set') {
  //nutzen von set als aufruf der speziellen setter methode
  //also dem einfachen fuellen von einer objektinstanz
  call_user_func_array(array($this, '__setter'), $arguments);
 } elseif(strpos($name, 'set')===0){
  //abfangen von setter methoden
  $props = explode('_',substr(strtolower($name),3));
  foreach($props as $prop) {
	  $this->__setter($prop,$arguments);
  }
 } else {
  //alles andere ist ein fehler
  throw new Exception("Unkown function: '$name'.");
 }
}

Was der Basis-Klasse jetzt noch fehlt, sind die Standard Operationen Löschen, Editieren, Erstellen und die Finde bzw. Finde-alle Methoden. Erstellen, Editieren und Löschen machen nichts weiter, als die Standard-SQL Befehle anhand des Tabellennamens auszuführen. Erstellen und Editieren habe ich noch einmal in einer Methode Save gekappselt, die – je nachdem ob eine ID vorhanden ist oder nicht, Erstellen oder Editieren aufruft. Auf die Darstellung dieser Methoden sei hier verzichtet, sie bestehen wie gesagt fast nur aus einem SQL-Statement mit Platzhaltern. Finde und FindeAlle unterscheiden sich nur von der fetch Anweisung für das PDO Objekt (fetch bzw. fetchAll) daher auch hier nur eine von beiden:

public static function find($conditions=array()) {
 //verbindung zur datenbank falls notwendig
 if(!self::$_db) {self::$_db = db::connect();}
 //tabelle ermitteln
 $name = self::tableName();
 //sql string zusammenbauen
 $sql = 'SELECT * FROM '.$name;
 //setzt die bedingungen zusammen...
 $con = self::_prepareConditionString($conditions);
 if(!empty($con))  {
	$sql.= ' WHERE '.$con;
 }
 
 $result = self::$_db->prepare( $sql );
 if(!$result->execute()) {
	$error = $result->errorInfo();
	throw new Exception($error[2]);
 }
 //zielobjekt ermitteln
 $classname = (!empty(self::$name)) ? self::$name : get_called_class();
 //und nun - dank pdo - direkt ein entsprechendes objekt erstellen
 $result->setFetchMode(PDO::FETCH_CLASS,$classname);
 return $result->fetch();
}

Und was ist mit den Beziehungen? – Davon ein anderes Mal vielleicht mehr. Aber schon in diesem Zustand kann einem eine solche Active-Record Klasse viel Schreibarbeit für die gängigen CRUD Befehle abnehmen. Wie gesagt, es handelt sich hier nur um eine Beispiel Implementierung, für einen “realen” Einsatz so noch nicht geeignet. Es fehlen Validierungen, Absicherungen gegen SQL-Injections und vieles mehr. Sie soll zum spielen und probieren anregen und – ganz nebenbei – zum besseren Verständnis von PHP Magics. Verbesserungsvorschläge und Anregungen zur Weiterentwicklung sind sehr willkommen, Fragen ebenso. In diesem Sinne – viel Spass beim Programmieren.

Text begrenzen, aber richtig!

16. März 2010

Text begrenzen, aber richtig!
Texte bei der Ausgabe in ihrer Länge zu begrenzen ist nun wirklich eine immer wiederkehrende Aufgabe. Daher ist es nicht verwunderlich, dass es zahlreiche Funktionen gibt, die ein solches Abschneiden gewährleisten sollen. Aber was leisten diese Truncate Funktionen wirklich?

Die allgemeine Verfahrensweise solcher Funktionen ist denkbar einfach: Es muss zunächst getestet werden, ob ein String überhaupt länger ist, als die vorgegebene maximale Länge. Ist dieses der Fall, so wäre der String an entsprechender Stelle abzuschneiden, und diese Kürzung ggf. durch eine Zeichenfolge wie zu kennzeichen. Nur was heisst an entprechender Stelle?

Basis vieler Truncate Funktionen in PHP ist die substr Funktion, über die sich ein beliebig langer Text an einer definierten Stelle abschneiden lässt. Berücksichtigt wird bei substr jedes Zeichen, ganz gleich ob es sich um Satzzeichen, Buchstaben oder Leerzeichen handelt.
Aus Gründen der Lesbarkeit kann es aber sinnvoller sein nicht exakt auf eine bestimmte Zahl an Einzelzeichen zu kürzen, sondern stattdessen das nächste Wort- oder Satzende zu finden. Ein Artikel bei the art of web liefert gleich eine ganze Reihe solcher Funktionen. Bei dieser Art von truncate Funktionen wird wahlweise mit einem regulären Ausdruck oder einer Kombination aus substr und strrpos gearbeitet.

Und was passiert, wenn es sich bei dem Text nicht um einfachen Plain-Text, sondern um eine HTML Struktur handelt? Schneidet man solche Texte an der falschen Stelle ab, kann es einem im schlimmsten Fall das gesamte Layout der Seite zerstören, man muss nur das Pech haben einen geöffneten Tag innerhalb der gekürzten Version zu haben, der nicht wieder geschlossen wird. Und selbst wenn das nicht der Fall sein sollte, so werden bei HTML Code die nicht dargestellten Tags als Zeichen mitgezählt, die ausgebene Länge entspricht also gar nicht der gewünschten maximalen Ausprägung. Auch ein Aufteilen eines solchen HTML-Textes an Leerzeichen hilft nicht – viele Tags enthalten Leerzeichen – die Konsequenz wäre damit die Gleiche.

Selbst wenn man vorab alle HTML Tags durch strip_tags entfernt, bleibt ggf. noch das Problem von vorhandenen Entities: Wie schaut es aus wenn ein ä nach &au abgeschnitten wird? Vom dadurch enstehenden Wegfall an Informationen wie Bildern, Verweisen oder anderen Formatierungen durch den HTML Code ganz zu schweigen.

Sobald man es mit HTML zu tun hat, und dieser Code in seiner Struktur auch nach der Kürzung erhalten bleiben soll, wird die Aufgabe deutlich komplexer. Weder substr oder ein regulärer Ausdruck sind ausreichend, denn alle geöffneten Tags müssen (in umgekehrter Reihenfolge) wieder sauber geschlossen werden. Alle Tags dürften keinen Einfluss auf die maximale Anzeigelänge haben und HTML Entities dürften nur als ein Zeichen gewertet werden.

Der “geöffnete Tags”-Problematik nehmen sich noch einige Funktionen an. Bei DZone gibt es ein entsprechendes Snipplet (die ausgereifteren Lösungen findet man in den Kommentaren zum Snipplet) und für die Template-Engine SMARTY gibt es auch einen entsprechenden Modifier. Die Problematik der “wirklichen maximalen Länge”, oder die Entities werden in den meisten Fällen aber nicht mit berücksichtigt.

Die meines Erachtens ausgereifteste Lösung findet man bei CakePHP, dieses Snipplet gewährleistet wirklich eine reibungslose Kürzung von HTML, inklusive Entities und korrekter Längenberechnung.

Seite 1 von 212