// der php hacker

// archiv

Frankenstein spielen: Objektmutation mit PHP

Geschrieben am 14. Mai 2009 von Cem Derin

Wagen wir Heute ein Experiment: Ich möchte eine Instanz einer beliebigen Klasse mutieren lassen. Und zwar so, dass am Ende eine Instanz einer vollkommen anderen Klasse dabei herauskommt – die Daten aber immer noch die selben sind.

Wozu man das brauchen könnte? Nehmen wir einfach mal an, wir verwenden eine Bibliothek. Eine Klasse dieser Bibliothek leiten wir ab und fügen ein oder mehrere Methoden hinzu. Korrespondierende Klassen aus der Bibliothek allerdings geben immer eine Instanz der Ursprungsklasse zurück. Wir müssten nun also alle Methoden in denen das der Fall ist kopieren, und die Ursprungsklasse durch unsere Ableitung ersetzen.

Das ist aus mehreren Gründen nicht besonders schlau: Zum einen haben wir eine ganze Menge Duplicate Code (wer das mal in den Untiefen des Zend Frameworks gemacht hat, der wird wissen, was ich meine), zum anderen haben wir ein Problem, sollte genau diese Methode von einem Update betroffen sein. Ohne es zu merken, verwenden wir eine veraltete Methode und sorgen evtl. sogar dafür, dass die Applikation nicht mehr lauffähig ist.

Mögliche Lösung: Mutation

Wäre es nicht wesentlich bequemer, wenn wir die Methode zwar ableiten, aber nur die Rückgabe der Parent-Methode entgegen nehmen und diese mutieren lassen, bevor wir sie weiterreichen?

Ein kleines Beispiel. Ein von uns verwendetes Framework hat eine Klasse Baum. Instanzen dieser Klasse haben eine Methode namens holeBlatt, welche eine Instanz der Klasse Blatt zurückliefert.

class Baum {
	public function holeBlatt() {
		/**
		 * Komplexer Algorithmus zum ermitteln des Blattes
		 */
		return new Blatt();
	}
}

Wollen wir nun aber, dass unser Objekt bei besagter Methode eine Instanz der Klasse MeinBlatt zurückgibt, müssen wir auch den Baum ableiten. Viele würden dies wie folgt lösen.

class MeinBaum extends Baum {
	public function holeBlatt() {
		/**
		 * kompletter, komplexe Algorithmus erneut hierher kopiert
		 */
		return new MeinBlatt();
	}
}

Wir sehen, dass wir den kompletten (imaginären) Körper der Methode mitkopieren mussten, und nur am Ende eine andere Instanz zurückgeben.

Nehmen wir nun an, wir haben eine Funktion namens mutateTo, der man eine Objektinstanz einer beliebigen Klasse sowie einen Klassennamen übergibt, und die diese dann entsprechend „mutieren“ lässt. Dann wäre folgende Vorgehensweise denkbar.

class MeinBaum {
	public function holeBlatt() {
		$return = parent::holeBlatt();
		return mutateTo($return, 'MeinBlatt');
	}
}

Viel cooler. Wir können so gut wie ohne Bedenken das Framework updaten, ohne das unser Code davon beeinflusst wird. Ist das doch der Fall, haben wir wesentlich weniger Code zu durchsuchen. Und wir haben ein paar Wiederholungen verhindert.

Einfach toll. Wo kann ich diese Funktion kaufen?

Serialisierung als Übergangszustand

Um das Objekt mutieren zu lassen, müssen wir es dummerweise erst einfrieren. Dazu serialisieren wir das Objekt. Serialisieren wir eine Instanz der Klasse Blatt, so erhalten wir eine Zeichenkette die ungefähr so aussieht.

O:5:"Blatt":0:{}

Dabei enthält das Objekt keinerlei Informationen. Nehmen wir nun aber einmal an, innerhalb des Objektes wird ein Wert gespeichert, der angibt, ob das Blatt grün oder gelb ist, dann würde unsere Zeichenkette so aussehen.

O:5:"Blatt":1:{s:9:"*_color";s:5:"Gruen";}

Hier ist also die Interne Objektinformation in unsere Ausgabe mit eingefügt worden – sie wurde serialisiert. Der Aufmerksame Leser wird bemerkt haben, dass in dieser Serialisierung ebenfalls festgehalten ist, um was für eine Objekt-Instanz es sich handelt.

Ersetzen wir Blatt doch einfach mal gegen MeinBlatt, deserialisieren die Zeichenkette wieder und sehen was passiert.

Notice: unserialize() [function.unserialize]: Error at offset 10 of 46 bytes

Oh, das hat wohl nicht geklappt. Woran könnte es liegen? Untersuchen wir die Fehlermeldung genauer, dann bemängelt die Funktion unserialize Einen Punkt mitten innerhalb unseres veränderten Klassennamens. Um genau zu sein, die Stelle, an der der ursprüngliche geendet wäre. Das liegt daran, dass vor dem Klassennamen festgehalten wird, wie lange der Klassenname ist. Die Klasse Blatt hat 5 Zeichen, MeinBlatt hingegen hat 9 Zeichen. Verändern wir also auch diesen Wert und wagen einen zweiten Versuch. Kein Fehler. Wir übrprüfen das erstelle Objekt: Und siehe da: Es handelt sich um eine Instanz unserer Klasse. Das funktioniert sogar, wenn unsere Klasse MeinBlatt gar keine Ableitung der Klasse Blatt mehr ist.

Frisch aus Dimension X: Mutagen

Man verzeihe mir den Verweis auf Helden meiner Kindheit. Wie dem auch sei. Eine konkrete Implementation einer Mutationsfunktion könnte so aussehen.

function mutateTo($object, $newClass) {
	$originalClass = get_class($object);
	$freezed = serialize($object);

	$lookFor = 'O:'. strlen($originalClass). 	':"'. $originalClass 	.'"';
	$replace = 'O:'. strlen($newClass). 		':"'. $newClass 		.'"'; 

	$manipulated = substr_replace($freezed, $replace, 0, strlen($lookFor));

	return unserialize($manipulated);
}

Ausprobiert, geht. Cool, das war einfach.

Das große Aber

Tja, leider gibt es immer einen Haken: Alle Referenzen auf dieses Objekt gehen flöten. Falls jemand hier eine Möglichkeit kennt: Ich würde mich freuen, wenn er mir sie nennt. Auch so würde ich mich über Feedback freuen – noch mehr sogar über aktive Mitwirkung :-)


#001
15. Mai 2009

hi cem,

ich bin ja nicht der design pattern experte, aber mir kommt das schwer nach hack vor.

Hab jetzt nicht so lange drüber nachgedacht aber spontan würd ich sagen in meiner methode wird die parent:: ausgeführt, das dann rückgegebene “Blatt” object übergebe ich an den constructor von “MeinBlatt” und dieser überschreibt einfach $this und liefert es zurück. also in code ausgedrückt:


class Baum {
function etwas(){
return $this;
}
}

class meinBaum extends Baum {
function etwas(){
$blatt = parent::etwas();
$meinblatt = new MeinBlatt($blatt);
}
}

class meinBlatt extends Blatt {
function __constructor(Blatt $blatt){
$this = $blatt;
return $this;
}
}

Wie gesagt kein anspruch auf richtigkeit bzw durchführbarkeit, nur so eine gedanke zu später stunde.

Ludwig


#002
15. Mai 2009

fehler im code..


class Baum {
function etwas(){
return new Blatt;
}
}

muss ja ein blatt returnen und nicht den baum :)


#003
15. Mai 2009
Cem Derin

Na klar ist das ein Hack, das Blog heißt ja nicht umsonst so :-)

Sauber wäre es, wenn ich der Klasse sage, welche Klassen instanziert werden sollen. Kann ich das aber nicht oder hält die Klasse sich nicht daran, habe ich ein Problem. Klar, ich könnte jetzt im Code des Frameworks oder der Bibliothek die ich verwende arbeiten, aber a) ist das unschön und b) darf ich das evtl. gar nicht.

Zu deinem Vorschlag: $this überschreiben wäre natürlich super. Kann man aber leider nicht. Darüberhinaus noch ein paar andere Probleme: Die Constructor-Signatur ist verändert, womit du ggf. nicht mehr gegen das nötige Interface arbeiten kannst. Darüberhinaus fehlen dir nun protected und private Properties, an die kommst du maximal über Reflektion – läuft dann am Ende aber auf das selbe Spiel raus.


#004
15. Mai 2009
Nils

Decorator Pattern auf den Baum anwenden? Also zumindest falls ich die Frage richtig verstanden habe :)


#005
15. Mai 2009
Cem Derin

Dafür müsstest du die Urpsrungsklasse anpassen können, um entsprechende Unterstützung einzubauen. Das ist in der Vorgabe ausgeschlossen.


#006
15. Mai 2009
Nils
class Baum
{
 function holeBlatt( )  ...
 function macheWasBaumiges( ) ...
}

class MeinBaum
{
  private $baum;
  function __construct( Baum $baum )
  {
    $this->baum = $baum;
  }

  public function holeBlatt( )
  {
    $blatt = $this->baum->holeBlatt( );
   // Mache irgendwas mit dem Blatt
    return $meinBlatt;
  }

  function macheWasBaumiges( )
  {
    return $this->baum->macheWasBaumiges( );
  }
}

#007
15. Mai 2009
Cem Derin

Und noch immer ist das Blatt keine Instanz von meinBlatt ;-)


#008
15. Mai 2009
Nils

Muss es das denn sein? Falls es sich hier um OOP Code handelt, dann wird es auch ein Interface geben, das ein Blatt definiert, gegen das ich implementiere. Und wenn nicht, dass muss ich halt MeinBlatt von Blatt ableiten, delegieren wo es nur geht und somit so tun, als ob es ein wirkliches Kind wäre. Mutieren geht ja mal gar nicht ;)


#009
15. Mai 2009
Cem Derin

Sobald ich eine spezialisierte Methode innerhalb der meinBlatt-Klasse habe, muss das sein. Und wenn die abzuleitenden Klassen dahingehend nichts unterstützen, muss ich mir irgendwie helfen. Da kann ich da noch so oft Proxys vorschieben, wenn ich nicht an protected Propertys komme, ich diese aber brauche, bringt mir das alles nichts. Wie beschrieben: Wem das nicht passt, der kann immer noch die entsprechende Methode kopieren und anpassen. Aber das ist mal was, wo ich sage: Das geht ja mal gar nicht ;-)


#010
15. Mai 2009
Nils

Ich glaub diesmal werde ich einen Beitrag bei mir posten, dann kann ich besser ausholen. Vielleicht habe ich ja am WE Zeit. ;)


#011
15. Mai 2009
Cem Derin

Find ich auch besser, das hier ist ja auch nur eine Kommentar- keine Diskutierfunktion :-) Ich harre gespannt


#012
15. Mai 2009

Wäre hier nicht Casten ein angenehme Lösung?
Geht in PHP zwar nicht ohne weiteres, aber man könnte “MeinBlatt” von “Blatt” ableiten und dann eine Methode “cast” hinzufügen, welche die public und protected Properties kopiert/übernimmt und wenn unbedingt nötig die private properties aus dem serialisierten String ausliest (unserialize mit einem modifizierten String bringt zuviel Nachteile – aber ich find die Idee geil! =)).
Überigens: Wenn die Kindklasse Zugriff auf die private Properties benötigt ist die abzuleitende Klasse meiner Meinung nach nicht richtig entworfen.


#013
15. Mai 2009
Cem Derin

Bzgl. dem cast-Vorschlag: Diese Methode wäre dann in der meinBlatt-Klasse, ich habe aber eine Instanz der Blatt-Klasse – kann also nicht casten.

Was die Properties angeht: Eine abgeleitete Klasse kann nicht auf private Properties/Methoden zugreifen, lediglich auf protected (ones. Man klingt dieser Deutsch-Englisch-Mix kacke ;) ). Und das ist schon des öfteren notwendig. Vor allem, wenn ich eine Methode hinzufüge, werde ich höchstwahrscheinlich wenigstens auf eine protected Methode bzw. ein entsprechendes Property zugreifen.


#014
15. Mai 2009

[klugscheiss mode on]
Geht jetzt wohl an der aufgabenstellung vorbei, aber in einem realworld example würde ich das übers versionsmanagement handhaben. also einfach die fremdklasse edtieren und als eigenen branch im zb. svn/git/cvs halten, kommt ne neue version raus muss man ja nur noch mergen.

oder im einfachsten fall:
sed ‘s/->Blatt/ ->MeinBlatt /g’ class.baum.php
[/klugscheiss mode off]

:)


#015
15. Mai 2009
Cem Derin

Fremdklasse editieren ist in der Aufgabenstellung ausgeschlossen, und eigentlich ist das sogar noch “real life”er als dein Beispiel, denn evtl. ist es mir aus Lizenz-Gründen gar nicht gestattet, die Fremdklassen verändert weiterzugeben. Wie gesagt, wem das nicht schmeckt, der kann halt auch den Weg gehen, viel Code zu kopieren. Das ist allerdings Lizenzrechtlich auch eher bedenklich – und immer noch unschön :-P


#016
15. Mai 2009

@Cem: ich weiß das man nicht direkt auf private properties zugreifen darf. Daher muss/kann man die ja auch über den “Serialize Hack” auslesen.
Und die Cast Methode muss schon in die MeinBlatt Klasse eingebaut werden, denn nur diese weiß ja schließlich, was sie von einem Blatt Objekt benötigt und was nicht:
class MeinBlatt extends Blatt {
public function cast(Blatt $b){
//properties kopieren
}
}
$blatt = new Blatt();
$meinBlatt = new MeinBlatt();
$meinBlatt.cast($blatt);


#017
15. Mai 2009
Nils

Bitte bitte niemals den “Serialize Hack” verwenden.


#018
15. Mai 2009
Cem Derin

Bevor man so finite Wörter in den Mund nimmt, sollte man sich lieber was besseres einfallen lassen ;-)

Denn was viele mit akademischer Sichtweise übersehen: Die reale Welt da draußen sieht oftmals anders aus … ich sage nicht, dass es schön ist, aber es ist eine Möglichkeit. Der Zweck heiligt so gut wie immer die Mittel. Wenn du mir allerdings eine Lösung nennen kannst, die in dem Szenario durchführbar ist: Her damit :-)

Das wäre ja alles gar nicht nötig, wenn PHP eine komplette Reflection Implementierung hätte …


#019
15. Mai 2009
Nils

@cem: Gib doch mal ein konkretes Bsp. und dann versuche ich das schön(er) hinzubekommen. Vielleicht verstehe ich ja auch gar nicht das Problem.


#020
15. Mai 2009
Cem Derin

Das Problem ist doch im Post beschrieben … ?!


#021
15. Mai 2009

Hallo Cem,

bin gerae zufällg drüber gestossen. (vorweg: mir ist klar das deine version davon ausgeht das die einzubindenden klassen eben nicht so geschrieben sind um es zu ermöglichen, aber damit auch hier steht wie es normalerweisse sein soll, find ich es nicht schlecht)

Zend_Db_Table_Abstract hat die protected varialbe $_myRow diese kann auf eine eigene implementation von Zend_Db_Table_Row_Abstract zeigen.

dann liefert model->find(123) nicht mehr eine Zend Row zurück sondern zb. eine MyRow.

Gefunden auf: http://zf-blog.de/dokumentation/zend.db.table.row.html
in Beispiel 9.114


#022
15. Mai 2009
Nils

Dann verstehe ich das ganze wirklich nicht. Den einzigen Vorteil, den du so hast, ist doch dass du an die privaten Attribute rankommst, auf alles was public ist würde ich doch auch mit einem Decorator stemmen. Protected würde ich durch Ableitung des Decorators von Baum auch noch hinbekommen.
Wenn ich an private Methoden rankommen muss, dass ist die Baum Klasse einfach falsch implementiert und wenn ich doch dran will, dann müsste man das doch auch über die Reflection API machen können (da bin ich mir aber nicht ganz so sicher).

Aber noch mal so zum Verständnis: Geht es hier eigentlich hauptsächlich drum ein Objekt $meinBlatt zu erzeugen, dass die gleichen Attribute hat, wie $blatt? Wenn nicht, dann bin ich komplett raus :)


#023
15. Mai 2009
Cem Derin

@Ludwig: Jo, in der Zend_Db_Table ist das ganz gut gelöst. Irgendwo im Dispatch-Vorgang/im Front-Controller war das allerdings auch mal nicht der Fall. Das hat mich genervt. Konkret ging es da um das Request-Objekt.
@Nils: Das ist auch der Grund, warum ich dir grade kein kokretes Beispiel nennen kann, da ich die Stelle auf Anhieb nicht finde. Ich hab mich damals damit arrangiert und das mit dem kopieren der entsprechenden Abschnitte gelöst. Blöderweise finde ich das grade nicht.

Zu deiner Frage: An die protected-Eigenschaften von Blatt komme ich nur innerhalb von Blatt. Wenn Blatt instanziert ist, ist aber alles zu spät, dann kann ich zur Laufzeit keine Methoden aus “meinBlatt” mehr hinzufügen. Und genau das will ich: Eine Instanz von meinBlatt, mit dem Methoden von meinBlatt.

Und an die privaten Attribute komme ich mit keiner Ableitung. Dazu müsste ich die Serialisierung auseinanderparsen, und das ist alles andere als trivial ;-)

Edit: Ich freue mich aber, endlich mal eine Diskussion angezettelt zu haben ;-)


#024
15. Mai 2009
Nils

@cem: Das stimmt nicht so ganz :) Sichtbarkeiten von Methoden/Attributen sind auf Klassenebene und nicht auf Objektebene. Folgendes Beispiel müsste also ohne Probleme funktionieren.

class MeinBlatt extends Blatt
{
public function macheWasMitBlatt( Blatt $blatt )
{
echo $blatt->protectedAttribute;
$blatt->protectedMethode( );
}
}

PS: Ich mag Diskutieren auf einem hohen Niveau :)


#025
15. Mai 2009
Cem Derin

Stimmt, das geht. Damit kann man zumindest das Problem von dupliziertem Code beheben. Wobei meine Aussage ja eigentlich die selbe war “an die protected eigenschaften von Blatt, komme ich nur innerhalb von Blatt” :-P

So oder so: Das ist die saubere(re) Methode.


#026
15. Mai 2009
Philipp


class Baum {
public function holeBlatt() {
return new Blatt();
}
}

class MeinBaum extends Baum {
public function holeBlatt() {
$blatt = parent::holeBlatt();
return new MeinBlatt($blatt);
}
}

class MeinBlatt {
private $blatt;
public function __construct(Blatt $blatt = null) {
$this->blatt = $blatt;
}
public function __call($name, $arguments) {
echo __METHOD__." $name, $arguments\n";
// Wieviele Parameter hat eine Methode maximal?
return $this->blatt->$name(current($arguments), next($arguments), next($arguments), next($arguments), next($arguments), next($arguments), next($arguments), next($arguments));
}
public function __get($name) {
return $this->blatt->$name;
}
public function __set($name, $value) {
$this->blatt->$name = $value;
}
}

class Blatt {
public $farbe = 'grün';
public function getBaum() {
return 'Ich hänge am Apfelbaum';
}
public function foo0() {
return "Keine Parameter";
}
public function foo1($param1) {
return "Parameter $param1";
}
public function foo2($param1, $param2) {
return "Parameter $param1, $param2";
}
}

$meinBaum = new MeinBaum();
$meinBlatt = $meinBaum->holeBlatt();

echo $meinBlatt->farbe . "\n";
echo $meinBlatt->foo0() . "\n";
echo $meinBlatt->foo1('X1') . "\n";
echo $meinBlatt->foo2('4','2') . "\n";


#027
12. Jun 2009
Manuel Grundner

Wenn ich das richtig interpretiert habe hat Nils mit dem Decorator schon recht!
Auch wenn die Funktionalität nicht gegeben ist kann ich ja noch immer von Baum eine abstrakte Klasse BaumDecorateable ableiten, und das Interface anbieten.
Private Properties sind nicht ohne Grund privat, wenn sich der Entwickler hoffentlich darüber Gedanken gemacht hat warum sie privat sein sollen.
Und wenn ich wirklich noch drauf zugreifen muss, ist Reflection definitiv das richtige Stichwort, nur ist es eben sehr mit Vorsicht zu genießen, weils doch ein schwerwiegender Eingriff in die Sichtbarkeit ist.
Ein konkreter Anwendungsfall für so etwas fällt mir auf die Schnelle aber auch nicht ein.

Gruß Manuel


#028
07. Jul 2009
PHP-Desaster

Noch eine Sache zu dem Zugriff auf private Member. Dies ist auch über eine Umwandlung in ein Array möglich:

class A {
private $_x=1;
}

$a=new A();
$arr=(array)$a;
var_dump($arr["A_x"]); // =1


#029
07. Jul 2009
Cem Derin

Im Grunde Obsolet, da nun auch private/protected Propertys zugegriffen werden kann – über Reflection. Es lebe PHP 5.3 :-)


#030
02. Sep 2009

Die saubere Variante ist definitiv die mit Delegation, entweder wie von Philipp mit magischen Methoden geschrieben oder explizit, auch wenn das jede Menge Boilerplate-Code bedeutet. Üblicherweise (?) sollte ja sowieso ein Blatt-Interface vorhanden sein, dass man implementiert, dann muss man Letzteres sowieso machen (oder man schreibt einen Code-Generator).

Auf private Eigenschaften oder Methoden zuzugreifen, ist ganz übel, denn die gehören zur Implementation. Wenn diese dann ein Update erfährt, passiert u.U. genau das, was eigentlich vermieden werden sollte: Die Applikation ist nicht mehr lauffähig.

// kommentieren

// senden
theme von mir, software von wordpress, grid von 960 grid system. funktioniert in allen browsern, aber der safari bekommt das mit der schrift am schönsten hin.