Persistierung von Objekten in PHP
Persistierung von Sitzungsdaten
Die Session ist dann als Ziel geeignet, wenn die Aufgabe das Caching von nutzer- oder sitzungsspezifischen Daten geht. Beispielsweise ein "User"-Objekt mit Konfigurationseinstellungen des Nutzers, welches nach dem Login aus der Datenbank geladen und erzeugt wurde.
class MyUser
{
private final function __construct()
{
// do something
}
public function __destruct()
{
$_SESSION[__CLASS__] = serialize($this);
}
public static function factory()
{
if (isset($_SESSION[__CLASS__])) {
return unserialize($_SESSION[__CLASS__]);
} else {
return new self();
}
}
}
Ein privater Konstruktor sorgt dafür, dass das Objekt nicht unter Umgehung des Caches manuell erstellt werden kann. Durch den Aufruf der Funktion factory() wird das Objekt aus dem Cache rekonstruiert oder bei Bedarf neu erzeugt. Der Destruktor sorgt dafür, dass das Objekt bei Beendigung des Skripts automatisch in seinem aktuellen Zustand persistiert wird.
Persistierung in Dateien
Üblicherweise werden vor allem Klassen, die recht lange für die Initialisierung benötigen, in einem Cache gespeichert. Zum Beispiel indem diese serialisiert in Dateien in einem temporären Verzeichnis abgelegt werden.
Die Funktion serialize() sowie der Destruktor haben einige Einschränkungen derer man sich bewusst sein sollte: 1. Statische (also als "static" markierte) Eigenschaften werden nicht serialisiert 2. Der Destruktor wird außerhalb des Kontext des Skriptes ausgeführt. Dies bedeutet, der Destruktor wird nicht im Arbeitsverzeichnis des Skripts gestartet und deshalb greifen sämtliche relativen Pfadangaben stets ins Leere. Um das Objekt in eine Datei zu speichern ist es daher erforderlich den absoluten Pfad zu speichern.
class MyObject
{
private $id = null;
private final function __construct($id)
{
$this->id = $id;
// do something
}
public function __destruct()
{
$content = serialize($this);
$file = self::getFile($this->id);
file_put_contents($file, $content);
}
public static function factory($id)
{
$file = self::getFile($id);
if (file_exists($file)) {
$content = file_get_contents($file);
return unserialize($content);
} else {
return new self($id);
}
}
private static function getFile($id)
{
$cwd = dirname(__FILE__);
return "$cwd/temp/" . __CLASS__ . "$id.tmp";
}
}
Im Gegensatz zum ersten Beispiel zur Speicherung der Sitzungsdaten ist dieser Code nicht auf Singletons beschränkt. Daher kommt hier die Eigenschaft $id zur Speicherung eines Identifiers hinzu. Das Mapping des Identifiers zu einem Dateinamen übernimmt die Funktion getFile(). Das Fragment dirname(__FILE__) liefert den absoluten Pfad des Skripts und kann verwendet werden, um relative Pfadangaben in absolute umzuwandeln. Konstruktor und Destruktor arbeiten wie zuvor, speichern jedoch das serialisierte Objekt in eine Datei.
Wie erwähnt werden nicht alle Teile des Objektes serialisiert. Alle statischen Eigenschaften bleiben außen vor. Falls nicht-statische Eigenschaften, wie zum Beispiel eine Datenbankverbindung, ausgespart werden sollen, so bietet PHP zwei magic functions. Die Funktion __sleep() wird automatisch vor dem Serialisieren aufgerufen und liefert ein Array mit den Namen der Eigenschaften, welche serialisiert werden sollen. Alle nicht erwähnten Eigenschaften fehlen dem serialisierten Objekt. Die Funktion __wakeup wird ausgeführt nachdem das Objekt deserialisiert wurde. Sie dient dazu das unvollständige Objekt zu reinitialisieren.
class MyObject
{
private $id = null;
private $database = null;
private final function __construct($id)
{
// do something
}
public function __destruct()
{
// do something
}
public static function factory($id)
{
// do something
}
private static getFile($id)
{
// do something
}
public function __sleep()
{
return array('id');
// note that 'database' is missing
}
public function __wakeup()
{
$this->database = MyDb::getConnection();
}
}
Persistierung in Datenbanken
Datenbanken werden verwendet, wenn die Daten dauerhaft gespeichert bleiben sollen. Dabei wird üblicherweise jeweils eine Klasse auf eine Tabelle und deren Eigenschaften auf die Tabellenspalten abgebildet. Die Abbildung ist noch einfach, sofern das Objekt nur skalare Werte hat und die Benennung der Klasse und der Eigenschaften identisch zu den Namen der Tabelle und der Spalten ist. Eingeschlossene Objekte können in zusätzlichen Tabellen abgelegt sein. Verbunden sind beide Tabellen über Fremdschlüssel. Beim wiederherstellen der Objekten müssen eingeschlossene Objekte rekursiv aus der Datenbank geholt werden. (Der Sonderfall einer kreisförmigen Referenz soll hier einmal nicht betrachtet werden.)
Diese wiederkehrende Aufgabe kann zum Teil automatisiert werden. Dadurch ist es ausreichend die persistente Klasse von einer Basisklasse abzuleiten, welche die entsprechenden Funktionen implementiert. Notwendig ist lediglich das Mapping des Datentyps der Eigenschaften des Objektes auf den der Spalten der Tabelle.
class MyClass
{
protected static $database = null;
protected static $table = 'my_class';
protected static $columns = array(
'id' => 'int'
);
protected $id = null;
private final function __construct($id)
{
// do something
}
public final function getId()
{
return $this->id;
}
public function __destruct()
{
$row = array();
foreach (static::$columns as $name => $type)
{
$value = $this->{$name};
if (is_scalar($value)) {
$row[$name] = $value;
} else {
$row[$name] = $value->getId();
}
}
self::$database->insertOrUpdate(static::$table, $row);
self::$database->commit();
}
public static function factory($id)
{
if (!isset(self::$database)) {
self::$database = Yana::connect('myDb');
}
$stmt = "{static::$table}.{$id}";
$row = self::$database->select($stmt);
$instance = new static();
foreach (static::$columns as $name => $type)
{
$value = $row[$name];
if (!class_exists($type)) {
settype($value, $type);
$instance->$name = $value;
} else {
$instance->$name = $type::factory($value);
}
}
}
}
Das obige Beispiel benutzt Late Static Binding mit dem Schlüsselwort static. Dieses Feature wurde mit PHP 5.3 eingeführt. Wer diese Version nicht verwenden kann oder möchte, kann den gleichen Effekt mit einiger zusätzlicher Schreibarbeit erreichen. Darauf werde ich in diesem Artikel jedoch nicht näher eingehen. Für die Datenbankverbindung wird hier absichtlich ein Framework mit Querybuilder verwendet. Ersetzen Sie die Datenbankaufrufe gegebenenfalls durch Code für den Datenbanktreiber bzw. das Framework Ihrer Wahl.
Die Funktion factory() stellt hier automatisch die Datenbankverbindung her, falls dies noch nicht geschehen ist. Anschließend wird der Datensatz aus der Datenbank geladen. Typ und Namen der Spalten sind im Array columns hinterlegt. Dank dieses Arrays ist ein automatisches Mapping möglich. Das Array kann erweitert werden um weitere Eigenschaften zu speichern. Beispielsweise die Spaltenlänge.
Der Destruktor erzeugt seinerseits eine Datensatz mit den jeweiligen Eigenschaften des Objektes. Der Datensatz wird dann jeweils eingefügt oder aktualisiert. Achten Sie auf Fremdschlüssel! Sollten Sie Foreign-Key-Constraints definiert haben, so kommt es eventuell auf die Reihenfolge an, in welcher die Objekte zerstört werden. In diesem Fall sollten Sie eventuell selbst die Kaskade auslösen und steuern.
Persistierung in XML-Dateien
Was bei Datenbanken noch die Ausnahme ist, ist bei XML-Dateien die Regel: verschachtelte Elemente. Jedes XML-Element kann auf jeweils eine Klasse abgebildet werden. Im Folgenden dazu ein etwas komplexeres Beispiel:
class MyClass
{
protected $tag = 'my_class';
protected $attributes = array(
'name' => array('name', 'string')
);
protected $children = array(
'foo' => array('foo', 'Foo')
);
protected $name = '';
protected $foo = array();
public function toXML($parentNode = null)
{
// is root node
if (is_null($parentNode)) {
$instance = new SimpleXMLElement('<' . $this->tag . '/>');
// is inner node with pcdata-section
} elseif (isset($this->attributes['#pcdata'])) {
$property = $this->attributes['#pcdata'][0];
$value = (string) $this->$property;
$instance = $parentNode->addChild($this->tag, $value);
// is inner node with children
} else {
$instance = $parentNode->addChild($this->tag);
}
// set attributes
foreach ($this->attributes as $name => $attribute)
{
if ($name !== '#pcdata') {
$property = $attribute[0];
$type = $attribute[1];
if (isset($this->$property)) {
if (!is_null($this->$property) && $this->$property !== "") {
$instance->addAttribute($name, $this->$property);
}
}
}
}
// set children
foreach ($this->children as $name => $tag)
{
$property = $tag[0];
$class = $tag[1];
// skip undefined tags
if (is_null($this->$property)) {
continue;
}
foreach ($this->$property as $object)
{
$object->toXML($instance);
}
}
return $instance;
}
public function __construct(SimpleXMLElement $node)
{
$attributes = $node->attributes();
// set attributes
foreach ($this->attributes as $xml => $attribute)
{
$value = null;
$property = $attribute[0];
$type = $attribute[1];
if ($xml === '#pcdata') {
$value = trim("$node");
} elseif (isset($attributes->$xml)) {
$value = (string) $attributes->$xml;
}
if (isset($value)) {
settype($value, $type);
$this->$property = $value;
}
}
foreach ($node->children() as $node)
{
$xml = $node->getName();
if (isset($this->children[$xml])) {
$tag = $this->children[$xml];
$property = $tag[0];
$type = $tag[1];
$object = new $type($node);
$this->{$property}[] = $object;
}
}
}
}
Dieses Beispiel verwendet kein Late Static Bindung und ist somit auch für PHP 5.2 geeignet. Allerdings ergeben sich dadurch einige Einschränkungen. So dürfen die Eigenschaften zum Speichern der Metainformationen bspw. nicht statisch sein. Es ist auch nicht mehr möglich, einen nicht-öffentlichen Konstruktor zu verwenden.
Der Konstruktor und die Funktion toXML() dienen dem Laden bzw. Speichern des Objektes in ein SimpleXML-Element. Dazu sind drei Dinge erforderlich: der Name des Tags, die Liste der Attribute und die Liste der Kindelemente. Die Listen der Tags und Attribute enthalten die Namen der Tags, die Namen der dazu passenden Eigenschaften des Objektes, sowie deren Datentyp.
Für dieses Beispiel gelten folgende Einschränkungen: 1. Attribute sind stets skalare Werte. 2. Kindeelemente werden stets auf eine Klasse abgebildet. 3. Kindeelemente werden stets als Arrays von Objekten dargestellt. 4. die Persistierung findet nicht automatisch statt. (Sie können jedoch bei Bedarf selbst einen geeigneten Destruktor ergänzen.)
Im Gegensatz zu Datenbanken muss für XML-Dateien zusätzlich zwischen Attributen und Tags unterschieden werden. Ist ein Wert NULL, so darf die Funktion das entsprechende Attribut nicht schreiben, weil ein leeres Attribut einem leeren String entspricht (und somit nicht NULL ist).
Die Funktionen durchlaufen jeweils die Listen der Kindeelemente und Attribute und weisen die Werte des Objektes der XML-Datei zu und umgekehrt.
Zum Speichern des XML-Strings in eine Datei benutzen Sie folgenden Code:
$xml = $this->getXML();
$xml->asXML($filename);
Beachten Sie bei der Arbeit mit SimpleXML-Elementen, dass diese nicht mit der Funktion serialize() serialisiert werden können.
Fazit
Die obigen Beispiele sind selbstverständlich unvollständig. Sie decken eine Reihe von Sonderfällen nicht ab (und sind aus Zeitgründen nicht ausreichend getestet). Sie werden den Code also wahrscheinlich an Ihre Bedürfnisse anpassen wollen. Was dieses Tutorial Ihnen vorrangig vermitteln sollte ist, dass Persistierung von Objekten nicht kompliziert geschweige Hexenwerk ist. Es gibt für unterschiedliche Bedürfnisse unterschiedliche Lösungen. Prüfen Sie daher zunächst Ihre Anforderungen und wählen Sie dann die Technik, welche am besten passt.
Ich habe absichtlich beschlossen dieses Tutorial unter eine CreativeCommons-Lizenz zu stellen um anderen Lesern die Möglichkeit zu geben, dieses mit eigenen Ideen und Beispielen zu erweitern und auch auf anderen Seiten zu veröffentlichen in der Hoffnung, dass es nützlich ist. Wer weitere Anregungen oder konkrete Fragen hat kann diese im Übrigen auch direkt an mich richten über Twitter unter twitter.com/YanaFramework.
(ac/tom) Diskussion