Abstract Restful Controller

Heute habe ich die Controller für die „Restfulen Applikationen“ erweitert. Zend bietet hier bereits einen abstrakten Controller, diesen habe ich, angepasst an die neue Strukturen (Services, etc.) erweitertl Hierzu habe ich die Standard Methoden wie get, getList, save bereits ausimplementiert. Da für jedes Entity auch ein Validator vorhanden sein sollte, kann dies leicht verwiklicht werden. Es muss lediglich eine Instanz der jeweiligen Validatoren zurückgeben werden, sowie die möglich geboten werden, die entsprechenden Services zu laden. Hier kommt uns zu gute das über den ServiceLocater ja bereits mit Strings gearbeitet wird um ein entsprechenden Service zu laden, dadurch müssen wir dann nur mehr diesen bereitstellen.

. Der AbstractRestfulController überprüft vor dem konkreten „Action“ aufruf (preDispatch()), welche HTTP Methode verwendet wurde und reagiert dementsprechend darauf. Wird z.B. die post Methode verwendet, so wird die Methode zum erstellen eines Entities aufgerufen. Bei „get“ gibt es ja dann noch den Sonderfall, wenn keine id beim Aufruf in der Url verwendet wurde, so wird alles zurückgegeben. Dies würde auch bei einen PUT ohne id geschehen, diese Methode wirft aber, wenn sie nicht in der erbenden Klasse überschrieben wird eine Exception in der nur gemeldet wird das diese Methode hier nicht verfügbar ist (von diesen gibt es noch einige siehe Zend\Mvc\Controller\AbstractRestfulController).
Darauf aufbauend müssen wir jetzt nur die entsprechenden Methoden ausimplementieren wie z.B. hier:


/**
 * base controller class for the rest api controllers
 * gives you an basic instantiation of the service for the entity the conroller is for
 */
abstract class AbstractRestfulDoctrineController extends AbstractRestfulController {

    /**
     * Returns the name of the class of the entity
     * @return string
     */
    abstract protected function getMainServiceName();

    /**
     * @return DoctrineReadyService
     * @throws \Exception
     */
    protected function getMainService(){
        if($this->getMainServiceName() === null){
            throw new \Exception("Entity Name must be set is (".$this->getMainServiceName().") instead");
        }
        return $this->getServiceLocator()->get($this->getMainServiceName());
    }

    /**
     * @return JSONValidator
     */
    abstract protected function getMainValidator();

    /**
     * @param $data
     * @return BaseEntity
     */
    abstract protected function getMainEntityWithData($data);
    /**
     * Return list of resources
     *
     * @return mixed
     */
    public function getList()
    {
        return new JsonModel($this->getMainService()->findAllAsArray());
    }

    /**
     * Return single resource
     *
     * @param  mixed $id
     * @return mixed
     */
    public function get($id)
    {
        $id = $this->getEvent()->getRouteMatch()->getParam("id");
        $entity = $this->getMainService()->findByIdAsArray($id);
        if($entity === null){
            return $this->getResponse()->setStatusCode(Response::STATUS_CODE_404);
        }else{
            return new JsonModel($this->getMainService()->findByIdAsArray($id));
        }
    }

    /**
     * Create a new resource
     *
     * @param  mixed $data
     * @return mixed
     */
    public function create($data)
    {
        $response = $this->getResponse();
        $validator = $this->getMainValidator();
        if($validator->isValid($data)){
            $response->setStatusCode(Response::STATUS_CODE_201); //created
            return new JsonModel($this->getMainService()->save($this->getMainEntityWithData($data))->toArray());
        }else{
            $response->setStatusCode(Response::STATUS_CODE_400); // not acceptable
            return new JsonModel(array("status"=>"error", "messages"=>$validator->getMessages()));
        }
    }

    /**
     * Update an existing resource
     *
     * @param  mixed $id
     * @param  mixed $data
     * @return mixed
     */
    public function update($id, $data)
    {
        $validator = $this->getMainValidator();
        $response = $this->getResponse();
        if($validator->isValid($data)){
            return new JsonModel($this->getMainService()->save($this->getMainEntityWithData($data))->toArray());
        }else{
            $response->setStatusCode(Response::STATUS_CODE_400); // not acceptable
            return new JsonModel(array("status"=>"error", $validator->getMessages()));
        }
    }

    /**
     * Delete an existing resource
     *
     * @param  mixed $id
     * @return mixed
     */
    public function delete($id)
    {
        $response = $this->getResponse();
        $entity = $this->getMainService()->findById($id);
        if($entity != null){
            $this->getMainService()->delete($entity);
            $response->setStatusCode(Response::STATUS_CODE_202); //accepted
            return new JsonModel(array("status"=>"ok", "message"=>"entity deleted"));
        } else{
            $response->setStatusCode(Response::STATUS_CODE_404);
            return new JsonModel(array("status"=>"error", "messages"=>"entity not found"));
        }
    }
}

Hierbei ist zu sagen, dass das HTTP StatusCode management noch nicht optimal ausgebaut ist, bzw. man mit diesen sicherlich noch mehr arbeiten könnte.
Ein konkreter Controller könnten dann z.B. so aussehen:

class PatStammApiController extends AbstractRestfulDoctrineController {

    /**
     * Returns the name of the class of the entity
     * @return string
     */
    protected function getMainServiceName()
    {
        return 'Medinfo\Service\PatStammService';
    }

    /**
     * @return JSONValidator
     */
    protected function getMainValidator()
    {
        return new PatStammValidator();
    }

    /**
     * @param $data
     * @return BaseEntity
     */
    protected function getMainEntityWithData($data)
    {
        $entity = new PatStamm();
        $entity->fillWithArray($data);
        return $entity;
    }

Nun da wir einen funktionierenden Controller haben müssen wir auch noch die Routen anpassen, da diese ja hier doch leicht anders aussehen. Eine Route könnte dann z.B. so aussehen:


...
              'behandlungapi' => array(
                    'type' => 'segment',
                    'options' => array(
                        'route' => '/api/behandlung/[:id]',
                        'constraints' => array(
                            'id' => '[0-9]+',
                        ),
                        'defaults' => array(
                            'controller' => 'Medinfo\Controller\BehandlungApi',
                        ),
                    ),
                ),
...

Wie man sieht wird hier nur mehr die Id übergeben, die dann im AbstractRestfulController abgegriffen wird. Für komplexere Datenstrukturen (z.B. nur Behandlungen von einen Patienten) z.B. child routes gerabeitet werden um so eventuell speziellere Aufrufe zu ermöglichen.

siehe auch

DoctrineService und ein BaseEntity

Heute habe ich mir eine Abstrakte Klasse geschaffen für Services geschaffen. Ich habe hierbei alle Funktionalitäten abgebildet die man für die Basis Operationen mit den Entities braucht. Hiermit kann nun einfach aus der Datenbank ein und ausgelesen werden, weiters bietet diese Klasse einen guten Startpunkt für weitere Buisnesslogik und sollte auch als zentraler Zugangspunkt, für den Controller gesehen werden um mit der Datenbank zu kommunizieren. Weiters habe ich in Kombination mit einen einheitlichen Basis Entity die möglichkeit geschaffen die Entities einfach in Arrays umzuwandeln, dies ist vor allem für die Restbasierten Controller gedacht, da diese ja z.B. über JSON Kommunizieren und so lediglich das Array zurückgegeben werden kann ohne sich um die generierung zu kümmern. Angedacht ist dann auch noch den Controller so zu erweitern das er die Basisvalidierung von allen Entities fast fertig vor implementiert und nur mehr spezielle Ausprägungen aus implementiert werden müssen.

Nun aber zum Code, hier erst mal das Service:


<?php
namespace Vico\Service;

use Doctrine\ORM\EntityManager;
use Vico\Entity\BaseEntity;

/**
 * User: Zelle
 * Date: 05.01.13
 * Time: 14:21
 *
 */ 
abstract class DoctrineReadyService{

    /**
     * Returns the name of the class of the entity
     * (represents the name of the entity)
     * should be overwritten in all inheritated classes
     * @return string
     */
    protected function getMainEntityName(){
        return null;
    }

    /**
     *
     * @var Doctrine\ORM\EntityManager
     */
    protected $entityManager;

    public function __construct(EntityManager $em) {
        $this->entityManager = $em;
    }

    /**
     * Sets the EntityManager
     *
     * @param EntityManager $em
     * @access protected
     * @return DoctrineReadyService
     */
    protected function setEntityManager(EntityManager $em)
    {
        $this->entityManager = $em;
        return $this;
    }

    /**
     * Returns the EntityManager
     *
     * Fetches the EntityManager from ServiceLocator if it has not been initiated
     * and then returns it
     *
     * @access protected
     * @return EntityManager
     */
    protected function getEntityManager()
    {
        return $this->entityManager;
    }

    /**
     * Search for an entity with the unique identifier
     * @param $id
     * @return BaseEntity
     */
    public function findById($id){
        $this->checkRepository();
        return $this->getMainRepository()->find($id);
    }

    /**
     * search for an entity with the unique identifier and creates an array out of it
     * @param $id
     * @return array|null
     */
    public function findByIdAsArray($id){
        $entity = $this->findById($id);
        if($entity != null){
            return $this->entityToArray($entity);
        }else{
            return null;
        }
    }

    /**
     * Returns all entities in the database
     * @return BaseEntity[]
     */
    public function findAll(){
        $this->checkRepository();
        return $this->getMainRepository()->findAll();
    }

    /**
    * Returns all entities in the database in form of an array
    * @return BaseEntity[]
    */
    public function findAllAsArray(){
        $this->checkRepository();
        return $this->entitiesToArray($this->getMainRepository()->findAll());
    }

    /**
     * @param $entity
     * @return BaseEntity
     * @throws \Exception
     */
    public function save($entity){
        if(!is_a($entity, $this->getMainEntityName())){
            throw new \Exception("Unexcpected entity class ".$this->getMainEntityName()." ".__CLASS__." ".__METHOD__);
        }
        $this->getEntityManager()->persist($entity);
        $this->getEntityManager()->flush();
        return $entity;
    }

    /**
     * @param BaseEntity $entity
     * @return array
     * @throws \Exception
     */
    private function entityToArray($entity){
        if(!is_a($entity, $this->getMainEntityName())){
            throw new \Exception("Unexcpected entity class ".$this->getMainEntityName()." ".__CLASS__." ".__METHOD__);
        }
        return $entity->toArray();
    }

    /**
     * @param BaseEntity[] $entities
     * @return array
     */
    private function entitiesToArray(array $entities){
        $array = array();
        foreach($entities as $entity){
            $array[] = $this->entityToArray($entity);
        }
        return $array;
    }

    /**
     * @throws Exception
     */
    private function checkRepository(){
        if($this->getMainEntityName() === null){
            throw new \Exception("Entity Name must be set is (".$this->getMainEntityName().") instead");
        }
    }

    /**
     * @return EntityRepository
     */
    private function getMainRepository(){
        return $this->getEntityManager()->getRepository($this->getMainEntityName());
    }

}

Es muss prinzipiell nur getMainEntityName überschrieben werden, damit das Service ordnungsgemäß funktionieren kann. In dieser Methode wird der Name des Entities festgelegt, mit welchen gearbeitet wird.
Ist dieser in der erbenden Klasse nicht gesetzt, so wird eine Exception geworfen um zu verhindern das nicht laufende Services instanziert bzw. genutzt werden. Außerdem wird bei der Methode save überprüft ob das Entity wirklich zu den Service gehört. Prinzipiell könnte zwar jedes Entity gespeichert werden, jedoch sollen die Service explizit nur für das definierte Entity nutzbar sein um so die Rollen für die Datenbankzugriffe klar zu trennen.
Gelöst wurde das hier über eine nicht abstracte Methode die null zurückliefert, ich wollte zwar auch diese abstract machen um so die Erbendeklasse zu zwingen, diese Methode zu überschreiben, jedoch kann dann in der abstrakten Klasse nicht mehr auf dieses Feld zugegriffen werden, deshalb also diese Lösungsvariante.
Das BaseEntity bietet grundsätzlich 2 Funktionalitäten an: eine Methode um ein Array aus dem Entity zu erstellen und eine Methode um aus einen Array ein Entity zu befüllen, wobei lediglich geprüft wird ob das Feld im Array existiert, wenn nicht wird es mit null befüllt. Das Validieren und Filtern sollte hier bereits nicht mehr nötig sein bzw. bereits vorher geschehen.

abstract class BaseEntity {

    /**
     * creates an array based on all fields of the entity
     * @return array
     */
    abstract public function toArray();

    /**
     * sets all fields with data out of the array, there is no checking off violations necessary
     * @param array $data
     * @return void
     */
    abstract public function fillWithArray(array $data);
}

Ein konkretes Service kann z.B. so dazu aussehn


class InstitutionService extends DoctrineReadyService {
    /**
     * Returns the name of the class of the entity
     * (represents the name of the entity)
     * @return string
     */
    protected function getMainEntityName()
    {
        return 'Vico\Entity\Institution';
    }

}

Wie man sieht muss nur mehr der Name zurückgegeben werden und alles sollte funktionieren.

Der nächste Schritt ist nun den Controller dahingehend zu abstrahieren, das nur mehr spezielle Methoden implementiert werden müssen.

JavaScript Module – Kommunikation mit dem Server

Nachdem ich letztens auf Serverseitig validert habe, baue ich heute ein allgemeines Modul um mit den Server zu kommunizieren und bei einen Standard Bootstrap Formular Fehler anzuzeigen.

Dazu erstelle ich mir ein JavaScript Modul:



var standardFormHandler = (function(window, document, $){
  
    var module = {};
    module.init = function(options){
        $.extend(settings, options);
    }
    return module;
}(window, document, jQuery));

Prinzipiell soll das Modul ein Entity erstellen und updaten können, löschen ist vorerst nicht angedacht.
Es werden durch die Settings die Felder, das Formular und die Ziel URL festgelegt. Weiters gibt es ein Array mit vorgefertigten Fehlermeldungen.
Die Methoden new und update greifen beide auf die _sendToServer Methode zu, die den eigentlich Ajax-Call darstellt. Es wird eine Default success und fail Funktion angeboten, die aufgerufen wird sollten success und fail nicht übergeben werden.
Weiters baut der Request auf die Methoden done() und fail() auf und nicht mehr auf die seit JQuery 1.8 als @deprecated markierten Felder success und error. Außerdem wird durch das Formular vorher von alten Fehlermeldungen bereinigt.
Der Standard-Success-Handler, liest alle Fehlermeldungen aus und schreibt sie in Felder mit den CSS-Klassen .help-inline oder .help-block. Die Fehlermeldungen liegen in einen gewissen Format vor das ich in meinen letzten Blog Eintrag vorgestellt habe.


/**
 * Standard Form Handler
 * Bietet funktionalitaeten fuer das Erstellen bzw Updaten von Entities in der Datenbank
 * in Kombination mit den Bootstrap Formularen
 * Der Handler muss zuerst initialisiert werden bevor er verwendet werden kann
 * @type {*}
 */
var standardFormHandler = (function(window, document, $){
    /**
     * Zeigt an ob die Initialisierung bereits stattgefunden hat
     * @type {Boolean}
     */
    var initialized = false;
    var errormessages = {
        NOT_INITIALIZED: "module must be initialized",
        REPSONSE_NOT_WELL_FORMED: "response was not well formed",
        SETTINGS_NOT_WELL_FORMED: "settings are not well formed"
    }
    var settings = {
        serverUrl: "",
        formfield: "",
        varstocheck: {}
    };

    var module = {};

    /**
     * Initialisiert den Handler
     * Es muessen alle Felder belegt werden
     * @param options
     */
    module.init = function(options){
        initialized = true;
        $.extend(settings, options);
        JSLogger.log(settings);
        if(settings.serverUrl === "" || typeof settings.serverUrl === 'undefined' || settings.serverUrl === null){
            JSLogger.log("serverUrl " + settings.serverUrl);
            throw Error(errormessages.SETTINGS_NOT_WELL_FORMED);
        }

        if(settings.formfield === "" || typeof settings.formfield === 'undefined' || settings.formfield === null){
            JSLogger.log("formfield " + settings.formfield);
            throw Error(errormessages.SETTINGS_NOT_WELL_FORMED);
        }

        if(settings.varstocheck === "" || typeof settings.varstocheck === 'undefined' || settings.varstocheck === null){
            JSLogger.log("varstocheck " + settings.varstocheck);
            throw Error(errormessages.SETTINGS_NOT_WELL_FORMED);
        }
    };

    module.new = function(data, success, fail){
        _sendToServer("PUT","JSON", data, success, fail);
    };

    module.update = function(data, success, fail){
        //daten absenden
        _sendToServer("POST", "JSON", data, success, fail);
    };

    /**
     * Sendet die Daten zum Server (Wrapper fuer den Standard AjaxRequest)
     * @param type
     * @param dataType
     * @param data
     * @param done
     * @param fail
     * @private
     */
    function _sendToServer(type, dataType, datatoSend, done, fail){
        if(initialized === false){
            throw Error(errormessages.NOT_INITIALIZED);
            return;
        }
        //parameter gegeben?
        if(typeof done === 'undefined' || done=== null){
            done = _standardSuccessHandler; //binde standardhandler auf das objekt
        }
        if(typeof fail === 'undefined' || fail=== null){
            fail = _standardSuccessHandler; //binde standardhandler auf das objekt
        }
        //alte fehlermeldungen entfernen
        _cleanUpForm();
        var ajaxRequest = $.ajax({
            url: settings.serverUrl,
            type: type,
            data: datatoSend,
            dataType: dataType
        });
        //done und fail anstatt success und error, da diese ab 1.8.3 deprecated sind
        ajaxRequest.done(done); //install success handler
        ajaxRequest.fail(fail); //install fail handler
    };

    /**
     * Standard Success Handler
     * @param data
     * @param textStatus
     * @param jqXHR
     * @private
     */
    function _standardSuccessHandler(data, textStatus, jqXHR){
        if(data.hasOwnProperty("status") && data.status === "error"){
            if(data.hasOwnProperty("messages")){
                var messages = data.messages;
                $.each(settings.varstocheck, function(index, value){
                    //durch felder iterieren und fehler suchen
                    if(messages.hasOwnProperty(index)){
                        $('#control-group-'+value).addClass("error"); // fehler klasse hinzufuegen
                        $.each(messages[index], function(count, message){ // durch fehler iterieren
                            $('#control-group-' + value).children(".help-inline, .help-block").html("Bitte etwas eintragen!");
                        });
                    };
                });
            } else {
                throw Error(errormessages.REPSONSE_NOT_WELL_FORMED);
            }
        }
    };

    /**
     * Standard Fail/Error Handler
     * Fehler werden in die Konsole geloggt
     * Es gibt nur einen alert als Fehler zurück
     * @param jqXHR
     * @param textStatus
     * @param errorThrown
     * @private
     */
    function _standardErrorHandler(jqXHR, textStatus, errorThrown){
        JSLogger.log("Error Hanlder invoked");
        JSLogger.log(jqXHR);
        JSLogger.log(textStatus);
        JSLogger.log(errorThrown);
        alert("Fehler beim Speichern!");
    };

    /**
     * Entfernt alle Fehlermeldung in den Formular
     * @private
     */
    function _cleanUpForm(){
        $('.control-group', settings.formfield).each(function(){
            $(this).removeClass("error");
            $(this).children(".help-inline, .help-block").html("");
        });
    }

    return module;
}(window, document, jQuery));

Nun muss der Standardhandler nur noch für die jeweilige View angepasst bzw. initialisiert werden, so wie z.B. hier:


var patStammView = (function(window, document, $){
    var module = {};
    var settings = {
        varstocheck: {  "nr":"nr", "vorname":"name", "nachname":"name", "anrede":"name", "ort":"plz", "plz":"plz", "strasse":"strasse",
                        "gebDatum":"gebDatum", "versicherungsnr":"versichnr", "versicherungsanstalt":"versichnr", "telnr":"tel","email":"email"
                      }
    };

    module.init = function(options){
        $.extend(settings, options);

        standardFormHandler.init({serverUrl: $('#baseUrl').val(), formfield: '#patient-form', varstocheck: settings.varstocheck});
    };

    module.save = function(data){
        standardFormHandler.new(_getJSONFromForm());
    };

    return module;

    /**
     * serialisiert das patienten formular zu einen json objekt
     * @private
     * @return {{nr: (*|jQuery), anrede: (*|jQuery), vorname: (*|jQuery), nachname: (*|jQuery), ort: (*|jQuery), plz: (*|jQuery), strasse: (*|jQuery), gebDatum: (*|jQuery), versicherungsnr: (*|jQuery), versicherungsanstalt: (*|jQuery), telnr: (*|jQuery), email: (*|jQuery)}}
     */
    function _getJSONFromForm(){
        var json = { patstamm : {
            nr: $('#patnr').val(),
            anrede: $('#patanrede').val(),
            vorname: $('#patvorname').val(),
            nachname: $('#patnachname').val(),
            ort: $('#patort').val(),
            plz: $('#patplz').val(),
            strasse: $('#patstrasse').val(),
            gebDatum: $('#patgebDatum').val(),
            versicherungsnr: $('#patversicherungsnr').val(),
            versicherungsanstalt: $('#patversicherungsanstalt').val(),
            telnr: $('#pattelnr').val(),
            email: $('#patemail').val()
        }
        };
        return json;
    }

}(window, document, jQuery));
</script>

siehe auch:

Scroll to top