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:

JSON Validatoren und JQuery

Heute habe ich versucht ein per JSON geliefertes Objekt in einen Controller abzufangen und zu validieren. Zuerst wollte ich dies mit der von Zend angebotenen Klasse AbstractValidator lösen. Jedoch habe ich nach genaueren Nachforschungen herausgefunden, das dieser wohl nur für Validatoren gedacht ist, die eine Variable betrachten bzw. diese validieren. Daher habe ich mich entschlossen nur das ValidatorInterface zu implementieren. dieses verlangt lediglich die Methoden isValid($value) und getMessages().
Wobei ich hier die Methode getMessages() auf ein 2-dimensionales Array erweitere, so das ein Array z.B. so aussieht


{"nr":["fieldempty","errors"],"vorname":["fieldempty","errors"],"nachname":["fieldempty","errors"]}

Hierzu wird zwar das ValidatorInterface etwas zweckentfremdet, dies hat aber den Vorteil das nun alle Felder gleichzeitig „validiert“ werden und nicht wie bei der Möglichkeit mit den AbstractValidator, Feld für Feld validiert wird und sobald ein Fehler gefunden wird dieser zurückgegeben wird.

Felder die das JSON Objekt enthalten soll stehen in $fields. Diese können über die setter- Methode gesetzt werden oder per Konstruktor übergeben werden.
Außerdem wird eine Möglichkeit geboten die Felder auf einen Leerstring zu überprüfen, dies kann mit isEmptyStringChecked(boolean) aktiviert werden.

Meine Implementierung sieht also wie folgt aus:
JSONValidator BasisKlasse


<?php
class JSONValidator implements ValidatorInterface{
    /** const messages */
    const FIELD_MISSING = "fieldmissing";
    const FIELD_EMPTY = "fieldempty";

    /**
     * Contains all error messages
     * @var array|null
     */
    private $messages = null;

    /**
     * Should contain all fields of the json object, that are expected
     * @var array|null
     */
    private $fields = null;

    /**
     * If set to true, fields must also be not empty strings
     * @var boolean
     */
    private $emptystring = false;

    public function __construct($fields = array()){
        $this->messages = array();
        $this->fields = $fields;
    }

    /**
     * Returns true if and only if $value meets the validation requirements
     *
     * If $value fails validation, then this method returns false, and
     * getMessages() will return an array of messages that explain why the
     * validation failed.
     *
     * @param  array $value
     * @return boolean
     * @throws Exception\RuntimeException If validation of $value is impossible
     */
    public function isValid($value){
        $isValid = true;
        foreach($this->fields as $field){
            if(!array_key_exists($field, $value)){
                $this->addMessage($field, self::FIELD_MISSING);
                $isValid = false;
            }else if($this->emptystring && $value[$field] == ""){
                $this->addMessage($field, self::FIELD_EMPTY);
                $isValid = false;
            }
            echo $value[$field];
        }

        return $isValid;
    }

    /**
     * checks if field already has an error
     * @param $field
     * @return bool
     */
    private function fieldHasError($field){
        return !array_key_exists($field, $this->messages);
    }

    /**
     * Returns an array which contains all the error messages
     * if isValid($value) returned true, the array will only be status => ok and no messages, otherwise the message part will contain all the error
     * messages. The key of the message array is the field which contained the error.
     * @return array
     */
    public function getMessages(){
        return $this->messages;
    }

    /**
     *
     * @param array $messages
     */
    public function setMessages(array $messages){
        $this->messages = $messages;
    }

    public function addMessage($field, $message){
        $this->messages[$field][] = $message;
    }

    /**
     * @param array $fields
     */
    public function setFields($fields){
        $this->fields = $fields;
    }

    /**
     * @return array
     */
    public function getFields(){
        return $this->fields;
    }

    /**
     * @param boolean $checkempty
     */
    public function checkEmptyString($checkempty = true){
        $this->emptystring = $checkempty;
    }

    /**
     * @return boolean
     */
    public function isEmptyStringChecked(){
        return $this->emptystring;
    }
}

Und hier z.B. eine von JSONValidator abgeleitete Klasse, es wird nur der Konstruktor überschrieben, um die Felder des JSON Objekts festzulegen, weiters könnte man hier auch noch die die isValid()-Methode erweitern, wenn manche Felder eine spezielle Validation benötigen.


class PatStammValidator extends JSONValidator{

    public function __construct(){
        $fields = array( "nr", "vorname", "nachname", "anrede", "ort", "plz", "strasse", "gebDatum", "versicherungsnr", "versicherungsanstalt", "telnr", "email" );
        parent::__construct($fields);
    }
}

Und hier noch der Aufruf in der Action im Controller


...
$validator = new PatStammValidator();
$validator->checkEmptyString();
if($validator->isValid($patstammarray)){
   $patstamm = $this->createPatStammFromRequest($patstammarray);
   return new JsonModel($patstamm->toArray());
}else{
   return new JsonModel(array("status"=>"error", "messages"=>$validator->getMessages()));
}
...

Somit können die meisten Formulare zumindest schnell auf Leerstrings geprüft werden und es kann geprüft werden, ob von der Clientseite überhaupt die richtigen Daten gesendet worden sind.

Die Ausgabe auf der Client Seite könnte in etwa mit JavaScript so aussehen:


var patStammView = (function(window, document, jQuery){
  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);
  };

  module.save = function(){
      $('.control-group').each(function(){
          $(this).removeClass("error");
          $(this).children(".help-inline, .help-block").html("");
      });
      var jsonstring = $('#patient-form').serialize();
      JSLogger.log(jsonstring);
          $.ajax({
              url: $('#basePath').val() + "/api/patient/put",
              type: "PUT",
              data: _getJSONFromForm(),
              success: function(data){
                  if(data.hasOwnProperty("status") && data.status === "error"){
                      //$('#errormessage').html(data);
                      if(data.hasOwnProperty("messages")){
                          var messages = data.messages;
                          $.each(settings.varstocheck, function(index, value){
                              JSLogger.log(index);
                              if(messages.hasOwnProperty(index)){
                                  $('#control-group-'+value).addClass("error");
                                  $.each(messages[index], function(count, message){
                                   $('#control-group-' + value).
                                   children(".help-inline, .help-block").html("Bitte etwas eintragen!");
                                  });
                             }
                          });
                      }
                  }
              },
              error: function(data){
                  alert("Fehler beim Speichern!");
              },
              fail: function(){
                  alert("Fehler beim Speichern!");
              }
          });
      };
      return module;

      function _getJSONFromForm(){
          ...
          return json;
      }
}(window, document, jQuery));

Es wird in der Variable „varstocheck“ ein assoziatives Array angelegt, wobei der Key aus dem Feldnamen im zurückgelieferten JSON besteht, der Wert dazu der Name des Feldes ist, das „markiert“ werden soll.

Hier z.B. ein Teil der control-group id (Beispiel: „vorname“:name“ => „control-group-name“)


<div class="control-group" id="control-group-name">
    <label class="control-label">Name:</label>
    <input type="text" name="anrede" class="span2" id="patanrede" placeholder="Anrede" />
    <input type="text" name="nachname" id="patnachname" placeholder="Nachname">
    <input type="text" name="vorname" id="patvorname" placeholder="Vorname" />
    <span class="help-block"></span>
</div>

Weiters könnte man hier noch die entsprechenden Fehler Codes abarbeiten und dementsprechende Fehler ausgeben, dies habe ich mir hier mal gespart.

Siehe auch:

Posts navigation

1 2 3
Scroll to top