Asynchrones Iteration Pattern

Ein nettes Pattern wenn man in JavaScript z.B. über eine Liste iterieren will, anhand deren man ajax – Calls staret. Hierbei tritt ja das Problem auf das man nicht zu viele Verbindungen aufn einmal öffnen kann. Nun bietet sich der Einfachheit wegen eine simple foreach Schleife an, bei der man pro Element abwartet bis die Antwort da ist. In jQuery würde dies dann ca so aussehen


$.ajax({
url: "url.php",
data: "data1=1",
async: false
}).done(//successhandler);

Dies führt aber zu den Problem das der Browser dadurch „steht“ (Ausnahme ist hier Firefox). Dies kann durch interne JavaScript Optimizer sogar dazuführen das eventuell ausgeführte Logik zum ein- und ausblenden erst danach geschieht (getestet mit der aktuellen Version von Google Chrome).

Abhilfe schafft hier das Asynchrone Iteration Pattern. Dies könnte wie folgt aussehen:


function iterate(){
 var data = "data1=1";
 var liste = [1,2,3,4,5,6,7,8,9];
 _next(0);
 return;   
       
 /**
  * Asynchroner Iterator über die Liste
  * @param position
  * @private
  */
  function _next(position){
      $.ajax({
          url: "test.php",
          type: 'POST',
          data: data + "&pos="+position,
      })
      .done(function(){
      //successhandler
      }).fail(function(jqXHR, textStatus, errorThrown){
      //errorhandler
     }).always(function(){
       position++;
       if(position == liste.length){
         return;
       }else{
         _next(position)
      });   
   }
}

Es wird einfach nach jeden fertigen Call, die nächste Position in der Liste aufgerufen, falls noch ein Element vorhanden ist.

View Helper, Routennamen und Action in der View

Oft benötigt man in den View – Files die aktuelle Route, den Controllernamen und den Actionnamen. In Zend2 ist es ganz einfach zu lösen. Zuerst brauch man einen View Helper, der einen in den View – Files zur Verfügung steht.


use Zend\View\Helper\AbstractHelper;

class RouteHelper extends AbstractHelper
{
    
    protected $routeMatch;

    public function __construct($routeMatch){
        $this->routeMatch = $routeMatch;
    }

    public function __invoke(){
        return $this;
    }
    
    public function getControllerName(){
        if($this->routeMatch != null){
           return $this->routeMatch->getParam('controller', 'index');  
        }
        return '';
    }
    
    public function getMatchedRoute(){
        if($this->routeMatch != null){
           return $this->routeMatch->getMatchedRouteName(); 
        }
        return '';
    }
    
    public function activeIfMatch($route, $action = null){
        if($this->getMatchedRoute() == $route){
            if($action == null || $this->routeMatch->getParam('action', 'index') == $action){
                return 'active';
            }
        }
        return '';
    }
}


In der Module Klasse


...
public function onBootstrap(MvcEvent $e)
    {
        $e->getApplication()->getServiceManager()->get('translator');
        $eventManager        = $e->getApplication()->getEventManager();
        //view helper for route name
        $e->getApplication()->getServiceManager()->get('viewhelpermanager')->setFactory('routeHelper', function($sm) use ($e) {
            $viewHelper = new \AAUShare\Helper\RouteHelper($e->getRouteMatch());
            return $viewHelper;
        });
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);
        
    }
...

Wichtig ist hier vor allem die Methode activeIfMatch welche eine Route und eine Action erwartet. Wenn diese erfüllt sind so wird active zurückgegeben. Dies habe ich vor allem mit den Gedanken gebaut nur mehr diese Methode in der View ausgeben zu müssen (z.B. um den Menüpunkt als active zu Markieren). In der View dann:


... <li class="<?php echo $this->routeHelper()->activeIfMatch('home'); ?>"><a  ... 

Doctrine2 Subquerys und der IDENTITY Helfer

Vor allem bei Suchquerys kommt man oft nicht an Subquerys vorbei. Konkretes beispielsweiße ist hier die Suche nach Einträgen mit gewissen Tags im AAUShare System. Es gibt hier das Post Entity mit den Mappings zu den Tags, bzw PostTags(Mapping zwischen Tags und Posts). Um hier z.B. alle Posts zu erhalten die mit einen Tag besitzen ist noch recht unkompliziert, möchte man jedoch nur Posts mit z.B. den Tags Informatik und ESOP so wird die Query schon komplizierter. Ich habe dazu folgende Methode implementiert:


public function findByTagTitles(array $tags, $additive){
        $tagtsrings = '';
        $logicalop = 'OR';
        if($additive == true){
            $logicalop = 'AND';
        }
        //create query statement ors and ands
        for($i=0; $i<sizeof($tags); $i++){
            $tagtsrings .= ' p.id IN (SELECT IDENTITY(pt'.$i.'.post) FROM AAUShare\Entity\PostTag pt'.$i.' JOIN pt'.$i.'.tag t'.$i.' WITH t'.$i.'.title LIKE :tag'.$i.')';
            if(($i+1) < sizeof($tags)){ //last and or or
                $tagtsrings .=' '.$logicalop;
            }
        }
        if(sizeof($tags)==0){
            $tagtsrings.='1';
        }
        //
        $querystring = 'SELECT p FROM AAUShare\Entity\Post p WHERE'.$tagtsrings;
        $query = $this->getEntityManager()->createQuery($querystring);
        //bind
        for($i=0; $i<sizeof($tags); $i++){
            $query->setParameter('tag'.$i, $tags[$i]);
        }
        //do query
        return $query->getResult();
    }

Hier wird ein Array mit Tags übergeben, nach welchen gesucht wird, der zweite Parameter bestimmt ob die Tags mit „AND“ oder mit „OR“ verknüpft werden. Interessant sind hierbei vor allem die for-Schleifen. In der ersten werden die Tags in Subquerys gepackt und geprüft ob der Post in der Menge der zurückgelieferten postids vorhanden ist. Wichtig ist hierbuach der IDENTITY Befehl für Doctrine2, damit wird auf den Foreign Key der OneToMany Relation in PostTag zugegriffen. Außerdem werden die Subquerys durchnummeriert und auch damit die Parameter

In der zweiten for-Schleife werden die durchnummerierten Query Parameter noch mit den Werten aus den Tags verknüpft.

Neu anlegen von vorhanden Entities by ManyToOne Relationen, obwohl Entity bereits existiert

Heute bin ich auf ein im ersten Moment doch sehr seltsames Verhalten von Doctrine2 gestoßen. Benutzt man verschiedene Entity Manager innerhalb einer Applikation, so beispielsweise bei einer ManyToOne Relation versucht das hinzugefügte Entity neu anzulegen

  /**
     * @ORM\ManyToOne(targetEntity="User", inversedBy="user")
     * @ORM\JoinColumn(name="userid", referencedColumnName="id")
     */
    protected $createdFrom;

Ursache sind wie bereits erwähnt die beiden verschiedenen EntityManager Instanzen. Abhilfe schafft hier logischerweiße das verwenden von der gleichen Instanz, oder aber das Entity aus dem EntityManger neu zu laden mit dem der Speichervorgang durchgeführt werden soll. Wichtig ist jedoch hier zu beachten, das man keine inkonsistenzen schafft wenn man beide EntityManager verwendet!

Anbei noch der Link zu den Stackoverflow Beitrag, der mir hier weitergeholfen hat.

PHP Markdown in Zend2

Oft hat man das Problem das bei von User eingetragenen Daten bzw. Textfeldern zwar Formatierungen erwünscht sind, jedoch auf der Serverseite für viel mehr Aufwand sorgen. Abhilfe schafft hier Markdown, eine eigene Notation für Formatierungen die dann in html umgewandelt wird. Es gibt hier bereits einige fertige Implementierungen für alle möglichen Sprachen, z.B. auch für PHP. (https://github.com/michelf/php-markdown). Hier habe idh die Klassen kopiert und in ein eigenes Modul gepackt. Die Ordnerstruktur sieht wie folgt aus:

Michelf
| Module.php
|__src
__|__Michelf
____|__Markdown.php

Jetzt muss nur noch im application.config.php File das Modul eingebunden werden ..


'modules' => array(
         ...,
        'DoctrineORMModule',
        'Michelf',

    ),

Nun kann der Text leicht in HTML umgewandelt werden


echo \Michelf\Markdown::defaultTransform($text);

AAUShare – Teile Lösungen und Skripten

Ein Studienkollege hat mich auf eine gute Idee für ein neues Wochenendprojekt gebracht. Es handelt sich um eine Online Plattform in welcher Studenten ihre Skripten teilen können. Man sollte sich dort einfach einloggen können und Skripten und Lösungen hochladen können bzw. nach diesen Suchen können.

Ein solches System müsste mit wenig Tabellen auskommen können. Die Datenbank besteht hauptsächlich aus der Tabelle Post – Ein Eintrag mit einen Titel, Text, Files und Tags sowie einen Rating – welcher in mehrere Tabellen aufgespalten worden ist. Es sollen pro Eintrag mehrere Files hochgeladen werden können, sowie ein Rating möglich sein. Um missbrauch zu vermeiden sollte man als User nur einmal pro Beitrag abstimmen können. Um dies zu gewährleisten muss ein Loginbereich geschaffen werden. Dies sollte mit Sozialen Netzwerken schaffbar sein, da heutzutage fast jeder einen Account in einen der Netzwerke besitzt.
Außerdem sollte man den Post auch Tag’s hinzufügen können um leichter gefunden werden zu können, hierbei habe ich an etwas ähnliches wie bei StackOverflow gedacht, da dies dort meiner Meinung nach einfach und schnell funktioniert.
Eine weitere wichtige Funktionalität ist das Kommentieren von Beiträgen, bzw. die Möglichkeit eine Antwort zu erstellen, wobei es wichtig ist zwischen den beiden zu unterscheiden. Eine Antwort kann wieder ein Beitrag mit Files sein, ein Kommentar sollte jedoch lediglich aus Text bestehen und auch in der Anzeige entsprechend gekennzeichnet werden.

Aufgrund der obigen Funktionalitäten habe ich mir folgendes ER-Diagramm überlegt:

ER

Ich habe hier auch mal das Rating aus dem Post herausgezogen, aus der Überlegung heraus, das für das RatingUpdate nicht auf die Post Tabelle zugegriffen werden muss, ich bin mir aber noch nicht sicher ob das sinnvoll ist.

Als Technologien habe ich ZF2, Doctrine2, Bootstrap (+Font Awesome), JQuery (+Plugins) gewählt, da ich mit diesen das meiset abdecken sollte.

Die Grundstruktur ist gleich wie bei meinen letzten Posts zu Zend / Doctrine. Ein Stolperstein bei den Entities waren die Mappings von Post zu File und Post zu PostTag. Es musste z.B. beim Mapping Post – PostTag auf der PostTag Seite folgendes hinzugefügt werden.


/**
* @ORM\ManyToOne(targetEntity="Post", inversedBy="post")
* @ORM\JoinColumn(name="postid", referencedColumnName="id")
*/
protected $post;

Ein einfaches Mapping auf die postid hat nicht funktioniert, warum genau habe ich nicht herausfinden können.

User Auth

Da die OAuth Lösungen auf die schnell nicht funktionierten habe ich mich Entschieden das Auth Beispiel von https://github.com/samsonasik/SanAuth/ zu modifizieren und an meine Bedürfnisse anzupassen. In dieser Auth wird noch nicht auf Doctrine Entities zurückgegriffen, daher habe ich mithilfe von https://github.com/doctrine/DoctrineModule/blob/master/docs/authentication.md dieses angepasst. Die Anpassungen werde ich in einen anderen Beitrag nochmal genauer erklären.

Nächsten Schritte

Aktuell funktioniert das hochladen von Dateien und das Erstellen von Posts, sowie das einloggen dran. Als nächstes kommen die Kommentare und die Antwortfunktion dran. Außerdem müssen sich noch User registrieren können, dies werde ich über ein Aktivierungsmail lösen. Der nächste Blogeintrag kommt also bald.

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:

Posts navigation

1 2 3
Scroll to top