Wenn man an Routen im Zend Framework 2 z.B. die Markierung der aktiven Seite im Navigationsmenü fest macht, ist es oft gut zu wissen welche Route greift. Wenn eine Markierung fehlt oder falsch gesetzt ist, kann man sich entweder durch die Routen-Definition suchen und so heraus finden in welche andere Route das Request lief oder man kann sich auch direkt ausgeben lassen welche Route gerade gewählt wurde. Für Debugging eine sehr praktische Sache.
Die meisten Beispiele nutzen leider nicht die Möglichkeit Objekte in der Function zu nutzen, die außerhalb erzeugt wurden. Dabei ist das eigentlich ja das spannende daran und nur ein "Order By" ist ja in den seltensten Fällen allein was man möchte.
Also hier ein kleines Beispiel, um direkt dazu zu kommen. Man achte auf das Schlüsselwort use.
if($test!=null && $test->getId()>0){
$rset = $this->select(
function (Select $select) use ($test) {
$select->where->equalTo('test_id',$category->getId());
$select->where->lessThan('test_date','CURRENT_DATETIME');
$select->order('test_date DESC')->limit(20,0);
}
);
}
Die Klasse leitet von TableGateway ab. Deswegen $this->select().
Wenn man lokal entwickelt muss man sich oft eine Subdomain für "localhost" einrichten. Das ZF und ZF2 sind am liebsten so installiert und Folders in einer Domain funktionieren nicht immer ganz so gut. Außerdem kann man dann so einfach alles auf die produktive Domain kopiren ohne die htaccess anpassen zu müssen.
So etwas einzurichten ist auch ganz einfach. Es geht in der httpd.conf oder man richtet sich die extra/httpd-vhost.conf ein.
<VirtualHost *:80>
DocumentRoot "C:/workspaces/php/projectXYZ"
ServerName xyz.localhost
ServerAlias www.xyz.localhost
<Directory "C:/workspaces/php/projectXYZ">
DirectoryIndex index.php
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Mit AllowOverride erlaubt man das Verwenden von Rewrites
REST mit dem Zend Framework 2 soll ganz einfach gehen. Geht es auch. Bestimmt gibt es viele verschiedene Möglichkeiten, aber diese scheint erstmal ganz gut zu funktionieren. Den XWJSONConverter hatte ich shcon in einem vorherigen Blog-Post vorgestellt. Man kann natürlich auch JSON:encode aus dem ZF2 verwenden oder json_encode().
class IndexController extends AbstractRestfulController{
private function _getResponseWithHeader(){
$response = $this->getResponse();
$response->getHeaders()
//make can accessed by *
->addHeaderLine('Access-Control-Allow-Origin','*')
//set allow methods
->addHeaderLine('Access-Control-Allow-Methods','POST PUT DELETE GET')
//change content-type
->addHeaderLine('Content-Type', 'application/json; charset=utf-8');
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.
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.
$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:
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();
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;
}
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.
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.
Meine erste Reallife-Erfahrung mit dem ZF2 und Models war eine eher weniger positive. Das TableGateway braucht unbedingt den Service-Manager bzw den DB-Adapter daraus. Meine Vorgängerin hat sich dazu entschlossen eine Basis-Entität zu bauen, die wiederum von TableGateway ableitet und man so relativ einfach save() und
load()-Methoden in die Model-Klasse einbauen konnte. Der Service-Manager wurde dann durch den Constructor der Model-Klasse durch gereicht bis man den Constructor des TableGateways mit dem DB-Adapter aufrufen konnte. Auch zwischen durch wurde der Service-Manager immer mal benötigt. Weil.. man ohne ihn keine Instanz einer Model-Klasse bekommen konnte.
Um so eine Instanz zu bekommen wurde eine Factory verwendet:
/**
* By default, the ServiceManager assumes all services are shared (= single instantiation),
* but you may specify a boolean false value here to indicate a new instance should be returned.
*/
'shared' => array(
'sModel' => false
)
)
Jedes mal aber die Factory zu bemühen eine neue Instanz zu erzeugen ist nicht gerade performant und mit den ganzen Ableitungen ist es auch sehr unübersichtlich. Ich wollte einfach schnell eine Instanz einer Model-Klasse haben und nicht ein riesiges Object bekommen. Also sowas wie in POPO .. eine POJO in PHP eben.
Ein Cosntructor ohne Argumente und eine Klasse nur mit Attributen ohne Logik. Also find ich an mir zu überlegen, ob diese ganzen Ableitungen überhaupt nötig sind oder man mit der Dependency-Injection nicht was viel besseres bauen könnte.
Ich bin dann schnell zu DAOs gwechselt. Die Factory injeziert einmal den Service-Manager und kann immer die selbe Instanz des DAOs liefern. Die DTO/Model-Klasse ist schön klein und beim Befüllen in Schleifen viel schneller als eine Intanz über die Factory anfordern zu müssen.
Das Zend Framework 2 ist das JEE der PHP-Welt. Endlos viele config-Files mit denen man so viel falsch machen kann und man erstmal nicht versteht, wo man was einstellt und welche Einstellungen zusammen hängen, und warum die Verzeichnisstruktur für Module so aussehen muss, wie sie aussieht. Aber mit Kopieren von vorhanden Dingen und etwas Testen kommt man doch zum Ziel und einfache Dinge sind dann doch schnell erledigt. Aber ACL und Resources sind genau so schlimm wie Principales und JAAS.. wenn es einmal läuft.. am Besten so lassen und nicht mehr anfassen :-)
Blog-entries by search-pattern/Tags:
Möchtest Du AdSense-Werbung erlauben und mir damit helfen die laufenden Kosten des Blogs tragen zu können?