Gerade im Zend Framework 2 kommt man immer mehr in Bereiche wo man auf altbekannte Konzept aus der Enterprise Welt und dort auch auf viel bekanntes aus Java (EE) trifft. Neben Depenency-Injection und Factories, findet man immer wieder gute Möglichkeiten Konzept wie
DAO oder
auch POPOs als PHP-Variante der
POJOs anzuwenden (wobei ich hier POJO und Bean gleichsetzen würde).
Auch Getter und Setter gehören hier genau so zum Alltag wie in Java.
Ich möchte hier ein paar Beispiele geben, wie man eine einfache Umsetzung in einem ZF2-Modul realsieren kann. Ich gehe einfach mal davon aus, dass man in der Lage ist ein einfaches Module in ZF2 zu erstellen und auch sich mit Controllern ViewModel und TableGateway zu mindest einmal beschäftigt hat. Der Einstieg ist nicht besonders einfach, wenn man keine lauffähige Anwendung hat, in die man sich einarbeiten kann.
Die hier umgesetzten Ansätze, sollen die Entwicklung möglichst beschleunigen und viel Code sparen, der in vielen Beispielen auch mit drin ist und alles sehr unübersichtlich macht. Auch wenn hier einige Ableitungen vorkommen halte ich das exesive Ableiten und Vererben für einen falschen Ansatz. Factories und Dependency-Incetion halte ich für einen besseren Weg eine Klasse mit bestimmten Eigenschaften auszustaten.
Ich leite auch vom TableGateway ab, wobei die bessere Methode es wohl wäre in der Factory mit Hilfe des ServiceManagers ein TableGateway zuerstellen und dieses an den entsprechenden Constructor zu übergeben.
Aber fanden wir einfach mal mit dem Model bzw der Entität an.
/**
* @dbtable=xw_tests
*/
class XWTest{
/**
* @dbcolumn=test_id
*/
private $_id=0;
/**
* @dbcolumn=test_name
*/
private $_name="";
/**
* @dbcolumn=test_value
*/
private $_value="";
public function getId(){
return $this->_id;
}
public function setId($id){
$this->_id=$id;
}
public function getName(){
return $this->_name;
}
public function setName($name){
$this->_name=$name;
}
public function getValue(){
return $this->_value;
}
public function setValue($value){
$this->_value=$value;
}
}
Die voran gestellen
_ entsprechen dem Zend Code-Style und sollen
private und
protected Attribute der Klasse deutlicher hervor haben.
Eigentlich sollte ja jedes Attribute der Klasse
private oder
protected sein. Nur wer die PHP eigenen JSON Funktionen oder Klassen verwendet, könnte man in die Verlegenheit kommen etwas
public zu machen, aber auch hier gibt es bessere Lösungen.
Ich halte wenig davon alle Attribute grundsätzlich auf
protected zu setzen, nur weil die Möglichkeit besteht mal eine Klasse ableiten zu wollen.
In den meisten Fällen, sollte man dann überlegen, ob es wirklich eine andere Entität ist oder man die vorhandene erweitern oder anpassen könnte so, dass keine weitere sehr ähnliche Entität nötig wird. Ableiten von einer
BasicEntity oder einem
BasicModel ist natürlich sinnvoll. Aber konkrete fachliche
Entitäten abzuleiten ist meistens weniger sinnvoll.
Nun kommen wir zu einem Punkt, der in der ZF2-Welt oft anders gesehen wird. Ich leite das Model nicht direkt vom TableGatway ab. Das Problem ist hier, das man für das TableGateway einen DBAdapter benötigt den man mit Hilfe des ServiceManagers bekommt. Also muss man beim Erzeugen des Model-Objekts immer diesen übergeben und das TableGateway instanzieren. Das kann man natürlich super mit den Factories des ZF2 erledigen. ABER.. wenn ich eine Liste des Models aus der DB lade und in einer Schleife mir für jede Row aus der DB eine Instanz der Klasse erzeuge ist das Vorgehen jede Instanz über diesen Factory bauen zu lassen, alles anderes als elegant oder schnell.
Also bleiben wir bei unserem einfachen POPO und erstellen für die Zugriffe auf die Datenbank nutzen wir eine weitere Klasse. Für jedes Model/Entity erzeugen wir uns eine DAO-Klasse. Diese enthält alle Methoden zum Laden, Speichern und Löschen sowie Listen laden des Models. Wie für fast jedes DAO benötigen wir hier eine Factory um die Einstellungen des Systems (DB-Verbindung) dem DAO bekannt zu machen.
Die Factory tragen wir in der
module.config.php ein, die wir in der Methode
getConfig der Module.php laden.
'service_manager' => array(
'factories' => array(
'testDAO' => function($sm) {
return new XW\Model\DAO\XWTestDAO($sm);
}
)
$sm ist der ServiceManager, den wir hier an den Constructor des DAOs übergeben. Der Constructor nutzt diesen um den DBAdapter zu bekommen und an das TableGateway weiterzugeben. Wie schon weiter oben geschrieben, hätte man dies auch in der Factory machen können und das TableGateway hier an das DAO übergeben können. Könnte einen etwas Code sparen es so zu machen.
Nun möchte man den DAO in dieser Art und Weise gerne verwenden:
$test=$this->getServiceManager()->get("testDAO")->loadTest($id);
Dafür schreiben wir uns jetzt einen eifnachen DAO, der erstmal nur die load-Methode enthält:
class XWTestDAO extends TableGateway{
$this->_sm=null;
public function __construct($serviceManager){
$this->_sm=$serviceManager;
parent::__construct(
$this->_tableName,
$serviceManager->get('dbAdapter')
);
}
//eine einfache Methode zum Kopieren der Daten aus dem Array in das Objekts
private function _copyData($entity,$array){
$dbm=new XWDBMapper();
return $dbm->setValuesFromArray($entity,$array);
}
public function loadTest($id){
$entity=new XWTest();
if($id!=null && $id>0){
$rsetItem=$this->select(
array('test_id' => $id)
)->current();
if($rsetItem){
$this->_copyData($entity,$rsetItem->getArrayCopy());
}
}
return $entity;
}
}
Ideal wäre es hier, wie man deutlich sehen kann
'test_id' auch später mal durch Annotationen zu ersetzen. Dafür wäre eine Annotation wie @
tableid dann geeignet nach der man sucht und sich dann den @dbcolumn-Eintrag aus dem selben DocComment-Block heraus liest.
Aber so weit ist es ja schon mal ganz schön. Wie brauchten kein Array zu erzeugen und sonst haben wir wenig Code schreiben müssen.
Das wichtigste fehlt hier aber natürlich noch. Wie mappe ich die Annotationen auf das Array und umgekehrt?
Dieser Mapper ist ansich auch nicht wirklich kompliziert und basiert hauptsächlich darauf mit der
RefelcetionClass zu arbeiten und mit deren Hilfe die
ReflectionProperties zu bekommen.
class XWDBMapper{
public function getColumnNameFromDoc($doc){
$result=null;
if(preg_match("/@dbcolumn=[a-zA-Z0-9_]+/",$doc)){
$result=preg_replace("/^.+@dbcolumn=([a-zA-Z0-9_]+)\s.+$/Uis","$1",trim($doc));
}
return $result;
}
public function setValuesFromArray($obj,$array){
try{
$ref=new ReflectionClass(get_class($obj));
$props=$ref->getProperties();
$cnt=count($props);
for($i=0;$i<$cnt;$i++){
$prop=$props[$i];
$prop->setAccessible(true);
$column=$this->getColumnNameFromDoc($prop->getDocComment());
if($column!=null){
if(isset($array[$column])){
$prop->setValue($obj,$array[$column]);
}
}
}
}
catch(Exception $e){
}
return $obj;
}
}
Sehr wenig Code und erleichtert einen das Leben sehr. Ist auch relativ schnell. Das händische Schreiben bzw Mappen der Arrays war doch immer sehr fehleranfällig und wenn man mal eine Column zur Datenbanktabelle hinzugefügt hat, müsste man an vielen stellen diese nachpflegen und hoffen, dass man keine Stelle vergessen hat. Jetzt muss man nur noch eine Annotation einbauen und es wird immer richtig gemappt.
Nachdem wir also nun das Laden einmal durchgegangen sind, kommt hier nur kurz die Methode für den Mapper, wie man ein Array aus einem Objekt erhält (die meisten werden schon wissen wie man die Methode da oben umdrehen kann). Deswegen brauche ich hier wohl auch nicht viel dazu schreiben.
public function createValueArray($obj){
$result=array();
try{
$ref=new ReflectionClass(get_class($obj));
$props=$ref->getProperties();
$cnt=count($props);
for($i=0;$i<$cnt;$i++){
$prop=$props[$i];
$prop->setAccessible(true);
$column=$this->getColumnNameFromDoc($prop->getDocComment());
if($column!=null){
$result[$column]=$prop->getValue($obj);
}
}
}
catch(Exception $e){
throw $e;
}
return $result;
}
Um jetzt alles noch schneller zu machen könnte man sich einen Cache für die Propert-Liste schreiben, dass diese nicht immer und immer wieder erzeugt werden muss. Beim Laden von Listen wäre das ein großer Vorteil. Aber selbst so ist alles schon sehr schnell. Jeden Falls habe ich zwischen den beiden Varianten bis jetzt in der Benutzung der Anwendungen keinen gravierenden Unterschied feststellen können, was die Performance betrifft. Mit XDebug durch gemessen habe ich noch nicht.
So haben wir mit nur eine paar kleinen Zeilen Code uns viel Arbeit erspart.. vielleicht habe ich hiermit noch anderen deren Arbeit vereinfacht.