"Christmas - the time to fix the computers of your loved ones" « Lord Wyrm

__get und __set

Paxi 25.03.2013 - 20:04 3112 7
Posts

Paxi

Overclocking Team Member
Avatar
Registered: Oct 2009
Location: Wien
Posts: 389
Hab am Wochenende einen kleinen Post über __set und __get verfasst.
Ich entwickle gerade mit einem Freund ein Webprojekt und dachte mir, es wäre nett, einen "allround" getter und setter für alle Businessobjekte zu haben.
Nach etwas rumprobieren bin ich auf einen meiner Meinung nach recht guten Ansatz gestoßen, wie man zusätzlich auch pro Property eine gültige Validierungsfunktion integriert, welche vor dem Setzen eines neuen Wertes den neuen Wert auf Gültigkeit überprüft. Das ganze will ich letztendlich noch erweitern, dass die Klasse als Basisklasse für diverse Businessobjekte einsetzbar ist.

Bin für jegliches Feedback und Verbesserungsvorschläge offen :)


Klick hier

mat

Administrator
Legends never die
Avatar
Registered: Aug 2003
Location: nö
Posts: 25429
Ich bin persönlich kein Fan von automatisierten Gettern und Settern. Magic-Functions sind schon etwas sehr Praktisches und geben viel Freiheit für Design Patterns, können aber auch "gefährlich" sein. Da PHP mit seinem Weak Typing ohnehin eher schwer lesbar ist, machen automatisierte Funktionen den Code noch schwerer nachvollziehbar und dadurch können Fehler entstehen, die einem Entwickler im späteren Verlauf eines Projekts das Leben schwer machen.

Abgesehen davon geht bei deiner Implementierung die Sichtbarkeit bzw. der Schutz davor verloren. Die Variablen werden alle public und zwischen private und protected kann bei Vererbung gar nicht unterschieden werden - es müsste auch bei vererbten Klassen der Getter/Setter verwendet werden. Dazu gesellt sich, dass Getter und Setter selbst nicht per Function Overloading verändert werden kann (zumindest nicht leicht und wenn dann alles andere als lesbar).

Unterm Strich würde ich es bei größeren Projekten und Frameworks nicht empfehlen.

Paxi

Overclocking Team Member
Avatar
Registered: Oct 2009
Location: Wien
Posts: 389
Danke für das Feeback mat.
Ich habe mir auch bereits gedacht, dass es ableitenden Klasse weiters möglich sein sollte __set und __get zu ändern. Das ist auch im aktuellen Code bereits implementiert.
Bezüglich der Sichtbarkeit stimme ich dir auch zu, aber liegt das "Problem" da nicht allgemein an der Verwendung von __get und __set?
__set() is run when writing data to inaccessible properties.
__get() is utilized for reading data from inaccessible properties.

Oder meinst du weil ich ein Array verwende und die Properties nicht wirklich existieren? Ich habe auch schon überlegt, die einzelnen Properties wirklich in jeder Klasse zu speichern und dann zb mit $this->$i im getter zurückzugeben, wäre das besser womöglich?

Hab hier nochmal einen kleinen Zusatz zum vorherigen Beitrag gemacht mit der Basisklasse.

hier

mat

Administrator
Legends never die
Avatar
Registered: Aug 2003
Location: nö
Posts: 25429
Zitat von Paxi
Danke für das Feeback mat.
Ich habe mir auch bereits gedacht, dass es ableitenden Klasse weiters möglich sein sollte __set und __get zu ändern. Das ist auch im aktuellen Code bereits implementiert.
Bezüglich der Sichtbarkeit stimme ich dir auch zu, aber liegt das "Problem" da nicht allgemein an der Verwendung von __get und __set?
__set() is run when writing data to inaccessible properties.
__get() is utilized for reading data from inaccessible properties.

Oder meinst du weil ich ein Array verwende und die Properties nicht wirklich existieren? Ich habe auch schon überlegt, die einzelnen Properties wirklich in jeder Klasse zu speichern und dann zb mit $this->$i im getter zurückzugeben, wäre das besser womöglich?

Hab hier nochmal einen kleinen Zusatz zum vorherigen Beitrag gemacht mit der Basisklasse.

hier
Mit den überschreibbaren Funktionen ist es jetzt besser, auch wenn noch nicht alles abgedeckt wird. Zum Beispiel überschreiben ich gerne den Setter von bestimmten Objekten, um dort zusätzlichen Code in einer abgeleiteten Klasse unterzubringen. Das würde bei deinem Code ein unübersichtliches if() oder gar ein switch() erfordern. Deutlich schlechter lesbar und nachvollziehbar.

Abgesehen davon sollten Getter und Setter meiner Meinung nach ohnehin nur für Attribute existieren, die auch wirklich einen Zugriff benötigen. Daher beginne ich bei einer Variablen immer private und ohne Zugriffsfunktionen und erlaube/implementiere nur mehr, wenn es auch wirklich nötig ist.

Übrigens gibt es bei deiner Implementierung auch Probleme mit Referenzen. Solltest du &$obj->myvar verwenden wollen, wird das sicher nicht funktionieren. Und da ist auch wieder das Grundproblem: Es passiert verdammt viel Magie für einen sonst sehr einfachen Code. Es geben sich Funktionen als Variablen aus und das ist meiner Meinung gefährlicher Code.

Paxi

Overclocking Team Member
Avatar
Registered: Oct 2009
Location: Wien
Posts: 389
Der Setter ist mit der Methode onSet in jeder Klasse implementierbar, nur die Validierungsfunktionen werden immer bereits in der Basisklasse aufgerufen. Das reicht mir ansich für meine Businessobjekte an Erweiterbarkeit, hier kann ich zb auch noch Werte in anderen Properties der ableitenden Klasse speichern oder ändern.

Bzg Punkt 2 stimme ich dir zu, das habe ich bislang auch so gehandhabt wie am Anfang von meinem ersten Post. Die Sichtbarkeit müsste in diesem Fall etwas unter dem generischen setter und getter leiden. Mit entsprechenden Validierungen kann man aber jedenfalls gewährleisten, dass alle Werte im Objekt immer gültig sind.
Ich gehe bei einem Businessobjekt zudem davon aus, dass es genau dieselben Properties wie die zugehörige Spalten in der Tabelle in der Datenbank hat, somit kann durchaus jedes Element einen getter und setter haben denke ich. Ausnahmen gäbe es wohl schon, zum Beispiel beim verschlüsselten Passworts eines Nutzers, dann könnte man sie aber gleich als property aus der Klasse weglassen.

Punkt 3 habe ich bislang noch nicht bedacht, das würde natürlich ein Problem geben.

Das der Ansatz in Hinblick auf Sichtbarkeit und Referenzen im Endeffekt immer noch kleinere Schwächen aufweist, würde ich vermutlich für die einfache Wiederverwendbarkeit in Businessobjekten in Kauf nehmen, sofern sonst keine weiteren Probleme auftauchen.

Paxi

Overclocking Team Member
Avatar
Registered: Oct 2009
Location: Wien
Posts: 389
Ich hab den Code noch ein letztes mal verfeinert und die Klassenvariablen nicht mehr in einem Array gespeichert sondern in wirklichen Properties, jetzt kann ich auch den Zugriff auf private properties von außen vermeiden. War doch noch nicht ganz zufrieden mit dem vorherigen Ansatz. Sollte besser sein nun
Code: PHP
<?php
abstract class BaseObject {
	
protected $validationfunctions;
	
/**
 * constructor
 */
public function __construct() {
	$this->reset();
	$this->initFunctions();
}

/**
 * magic method __get
 * calls onGet() in deriving class
 *
 * @param String $i
 *
 * @return mixed
 */
public function __get($i) {
	// only returns non private fields
	if(!array_key_exists($i, get_class_vars($this->className()))) {
		return $this->onGet($i);
	}

	echo '<br />'.$this->className().' property $'.$i. ' is private or does not exist';
	return null;
}

/**
 * magic method __set
 * calls onSet() in deriving class
 * 
 * @param String $i
 * @param mixed $val
 *
 * @return void
 */
public function __set($i, $val) {
	if(!array_key_exists($i, get_class_vars($this->className()))) {
		echo '<br />'.$this->className().' property $'.$i. ' is private or does not exist';
		return;
	}

	if(call_user_func($this->validationfunctions[$i], $val)) {
		$this->onSet($i, $val);
	} else {
		echo '<br />'.$this->className().' error setting property $'.$i;
	}
}


/**
 * initialize all properties
 * invoked in constructor
 * 
 * @return void
 */
public abstract function reset();

/**
 * initialize all validation functions
 * invoked in constructor
 * 
 * @return void
 */
protected abstract function initFunctions();

/**
 * @see BaseObject::set()
 * @param String $i
 * @param mixed $val
 * 
 * @return void
 */
protected abstract function onSet($i, $val);

/**
 * @see BaseObject::get()
 * @param String $i
 * 
 * @return mixed
 */
protected abstract function onGet($i);

/**
 * @return String classname
 */
protected abstract function className();
}

class Object extends BaseObject {
	
	// private data stays private from outside and deriving classes but can be used within this class
	private $id;
	
	// protected data can be accessed from outside and deriving classes but is validated before setting
	protected $username;

/**
 * initialize member var(s)
 *
 * @return void
 */
public function reset() {
	$validationfunctions = array();
	
	$this->id = 10;
	$this->username = 0;
}

protected function onSet($i, $val) {
	$this->$i = $val;
}
	
protected function onGet($i) {
	return $this->$i;
}

protected function className() {
	return 'object';
}

/**
 * initialize functions
 * 
 * @return void
 */
protected function initFunctions() {
	
	$this->validationfunctions['username'] = function($val) {
		return ( is_numeric($val) ? false : true);
	};
}

}
?>

Edit: kleines Problem gerade noch bemerkt, private properties haben jetzt auch keinen Getter mehr per default, also muss man diesen wenn benötigt als Methode in der ableitenden Klasse hinzufügen, zb id() für $id;
Bearbeitet von Paxi am 29.03.2013, 14:00

mat

Administrator
Legends never die
Avatar
Registered: Aug 2003
Location: nö
Posts: 25429
Eine verschmerzbare Alternative wäre meiner Meinung nach die Nutzung der Magic Function __call(). Damit kannst du Funktionsaufrufe für Getter und Setter abfangen und automatisieren, sofern diese nicht für ein Overloading schon von der Klasse selbst definiert wurden. Die __call()-Funktion könnte per isset() und property_exists() bzw. auch Reflection überprüfen, ob die Membervariablen vorhanden ist und sie entsprechend zurückgeben.

Damit hast du wenigstens Getter- und Setter-Funktionen, die sich 1:1 so bedienen lassen, als wären sie tatsächlich definiert. Allerdings solltest du noch einen Mechanismus einbauen, um zu verhindern, dass auf manche Membervariablen zugegriffen werden kann. Schließlich ist das Teil eines guten Designs!

Paxi

Overclocking Team Member
Avatar
Registered: Oct 2009
Location: Wien
Posts: 389
Danke nochmals für dein Feedback.
Ich habe jetzt auch noch einen Ansatz mit __call implementiert, den Zugriff auf definierte Membervariablen verhindere ich mit einem array, welchem jede ableitenden Klasse die Namen der nicht zugreifbare Properties hinzufügen kann.
In der Anwendung verhaltet sich die __call Implementierung jetzt ähnlich zu meiner vorherigen Version mit __get und __set, nur mit dem Unterschied, dass eben jetzt eine Funktion immer aufgerufen wird. Sofern das $arguments array in der __call Methode ein Element hat und eine Property mit $name existiert, rufe ich onGet auf, sofern es zwei Parameter hat rufe ich wieder onSet auf nachdem die Validierungsfunktion das okay gegeben hat.
Grundsätzlich sehe ich persönlich keinen großen Unterschied, aber nachdem deine Argumente durchaus schlüssig sind, werde ich es nun bei der __call Variante belassen.
Kontakt | Unser Forum | Über overclockers.at | Impressum | Datenschutz