MicroFrontends with Vue

Microservices are a well known concept nowadays. Handling encapsulated logic in different applications, can be a nice fit for bigger teams to avoid problems in deployments and development speed. Most of the time though, people are talking about the backend, where the UI part to some extend stays as a black box. One of the solutions to also further break down the UI is the concept of microfrontends which we will further explore here.

There are obviously also other ways to solve issues (I can highly recommend reading Sam Newmans awesome book on this https://samnewman.io/books/building_microservices_2nd_edition/ ), but one way to decompose your UI might be as following.

Each service not only contains the backend logic which then gets called the from one service to render the UI, but rather each service comes with its own UI which then gets loaded into an application which keeps an overview of all the systems to load. This allows us to deploy the full service UI and backend on its own without the need to deploy one big system for each change. This avoids keeping two systems too much in sync in terms of deployments.

Basic structure

In order to do this I used a vue js setup, where we have one container app, which further loads asynchronously applications into the main container (this is heavily inspired by https://blog.bitsrc.io/how-to-develop-microfrontends-using-react-step-by-step-guide-47ebb479cacd

flow of data

To achieve this one component was written:

<template>
  <div :id="this.containerName()" />
</template>

<script>
//utility function to generate a uuid (I didn't wanted to import the uuid
//lib just for a random string
function uuidv4() {
  return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
      (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
  );
}
export default {
  name: "MicroFrontend",
  props: {
    // name of the microfrontend like `wisdom` or `dicreoll`
    name: {
      type: String,
      required: true
    },
    // host where the script should be loaded from
    host: {
      type: String,
      required: true
    },
    // define a static container name rather than a generic one
    // not needed if you are ok with random divs all the time
    staticContainerName: {
      type: String,
      required: false,
      default: null
    }
  },
  data: function () {
    return {
      uuid: uuidv4()
    }
  },
  methods: {
    // calculates the container id we load the microfrontend app into
    containerName() {
      if(this.staticContainerName != null) {
        return this.staticContainerName;
      }
      return `${ this.name }-${this.uuid}-container`;
    }
  },
  mounted() {//
    // id is generated by frontend - this will avoid loading the same
    // script twice
    const scriptId = `micro-frontend-script-${this.name}`;
    const renderMicroFrontend = () => {
      const fnName = `render${this.name}`;
      const containerName = `#${this.containerName()}`;
      //load the render function per convention and handover the container id
      window[fnName](containerName);
    };
    if (document.getElementById(scriptId)) {
      renderMicroFrontend();
      return;
    }
    //first load the manifest.json this contains the way forward
    fetch(`${this.host}/manifest.json`)
        .then((res) => res.json())
        .then((manifest) => {
          const script = document.createElement("script");
          script.id = scriptId;
          
          script.crossOrigin = "";
          //load out the path to the main.js file
          script.src = `${this.host}/${manifest["src/main.js"]["file"]}`;
          script.onload = () => {
            // call the function defined on top which will resolve then the app
            renderMicroFrontend();
          };
          document.head.appendChild(script);
        });
  }
};
</script>

<style scoped>
</style>

At the mounted stage we will trigger a check and if necessary a load of the corresponding script files.

Each script is ensured to be loaded only once, where the script id is the identifier here. Each microfrontend must follow a convention here and offer in its own application a function called render<name> (e.g. renderDiceRoll ). The manifest json must per convention offer a file with src/main.js which consists of the render functions. The script is than attached to the document header section so that it can be loaded. Afterwards the function is called, which calls the actual render function of the microfrontend app.

Calling one microfrontend is than as simple as including this in the main container.

<vue-micro-frontend name="diceroll" host="http://localhost:8000"/>

The full sample can be found here https://github.com/zelle7/vue3_microfrontend_poc

Not solved here

Things which are left out here:

  • Authentication
  • Communication between services
  • Different technologies (like loading vue3 and vue2 or even things like react)

Links

Twitters Typeahead and Ajax

Twitters JQuery-Autocompleter Typeahead (nicht Bootstrap Typeahead, welcher in Bootstrap 3 auch nicht mehr integriert ist) ist ein recht praktikabler Autocompleter den man mit ein paar Zeilen gut an seine eigenen Bedürfnisse anpassen kann. Ich zeige es hier Anhand eines Ajax Calls mit zuerst noch nicht passenden Daten für Typeahead. Um diesen jetzt mit Custom Ajax Daten zu befüllen, werden der typeahead Funktion folgende Optionen übergeben.

Remote sagt hier aus das die Daten nachgeladen werden (könnte z.B. auch eine Json Datei auf einen Server sein oder ähnliches). Remote wiederum besteht aus mehreren Parametern die befüllt werden können. Die Url gibt logischerweise nur das Ziel des Ajax Calls an. Wie man bei URL sieht stelle ich mir hier z.B. die url durch eine Variable und den Wert des Inputfelds zusammen %QUERY wird anschließend von der Funktion automatisch mit dem Wert des Inputfeldes ersetzt. Um jetzt die noch nicht ganz passenden Daten in passende Daten zu konvertieren gibt es hier auch noch das filter – Feld. Diese erwartet eine Funktion, wobei der erste Parameter die vom Server geholten Daten repräsentieren. Hier erstelle ich ganz ein ganz simples Array, welches ich anschließend zurückgebe (Twitter Doku)


        $('#input').typeahead({
            remote: {
                url: baseUrl+'?q=%QUERY',
                filter: function(parsedResponse){
                    var data = [];
                    $.each(parsedResponse, function(index, item){
                        data.push(  {
                            value: item.name,
                            tokens: [item.name],
                            name: item.name
                        });
                    });
                    return data;
                }//A function with the signature filter(parsedResponse) that transforms the response body into an array of datums. Expected to return an array of datums.
            }
        });

So hat man mit ein paar Zeilen Daten vom Server abgeholt und an seine Bedürfnisse angepasst. Für Bootstrap 3 müssen momentan noch ein paar Zeilen CSS eingefügt werden (Typeahead CSS ). Zusätzlich musste ich aber hierbei noch für die hints display: none setzen da der Hint nicht an die passende Stelle wollte. Zusätzlich musste ich für den Span die Breite auf 100% erhöhen da sonst das input Feld zu klein bleibt.


.twitter-typeahead .tt-hint
{
    display: none;
    height: 34px;
    padding: 6px 12px;
    font-size: 14px;
    line-height: 1.428571429;
    border: 1px solid transparent;
    border-radius:4px;
}

.twitter-typeahead .hint-small
{
    height: 30px;
    padding: 5px 10px;
    font-size: 12px;
    border-radius: 3px;
    line-height: 1.5;
}

.twitter-typeahead .hint-large
{
    height: 45px;
    padding: 10px 16px;
    font-size: 18px;
    border-radius: 6px;
    line-height: 1.33;
}

.twitter-typeahead .tt-query,
.twitter-typeahead .tt-hint {
    margin-bottom: 2px;
}

.tt-dropdown-menu {
    min-width: 240px;
    margin-top: 2px;
    padding: 5px 0;
    background-color: #fff;
    border: 1px solid #ccc;
    border: 1px solid rgba(0,0,0,.2);
    *border-right-width: 2px;
    *border-bottom-width: 2px;
    -webkit-border-radius: 6px;
    -moz-border-radius: 6px;
    border-radius: 6px;
    -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
    -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
    box-shadow: 0 5px 10px rgba(0,0,0,.2);
    -webkit-background-clip: padding-box;
    -moz-background-clip: padding;
    background-clip: padding-box;
}

.tt-suggestion {
    display: block;
    padding: 3px 20px;
}

.tt-suggestion.tt-is-under-cursor {
    color: #fff;
    background-color: #0081c2;
    background-image: -moz-linear-gradient(top, #0088cc, #0077b3);
    background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));
    background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);
    background-image: -o-linear-gradient(top, #0088cc, #0077b3);
    background-image: linear-gradient(to bottom, #0088cc, #0077b3);
    background-repeat: repeat-x;
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0)
}

.tt-suggestion.tt-is-under-cursor a {
    color: #fff;
}

.tt-suggestion p {
    margin: 0;
}

.twitter-typeahead {
    width: 100%;
}

Language Switch

Heute stelle ich eine Lösung für einen Language switch im Zend2 Framework mit Ajax und Cookies.

Zu allererst muss die module.config.php Datei angepasst werden. In der Sekeletton Application von Zend2 ist der Translator bereits Konfiguriert und im Service Manager eingebunden. Ich habe lediglich die Übersetzungsdatei auf ein php – Array umgestellt:

(type => phparray und pattern => %s.php)


    'service_manager' => array(
        'factories' => array(
            'translator' => 'Zend\I18n\Translator\TranslatorServiceFactory',
        ),
    ),
    'translator' => array(
        'locale' => 'de_DE',
        'translation_file_patterns' => array(
            array(
                'type'     => 'phparray',
                'base_dir' => __DIR__ . '/../language',
                'pattern'  => '%s.php',
            ),
        ),
    ),

Außerdem habe ich für die Cookies die Module Klasse modifiziert:


class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        $e->getApplication()->getServiceManager()->get('translator');

        $translator = $e->getApplication()->getServiceManager()->get('translator');
        if(isset($e->getRequest()->getCookie()->language) && $e->getRequest()->getCookie()->language != '' && $e->getRequest()->getCookie()->language != null){
            $language = $e->getRequest()->getCookie()->language;
            $translator->setLocale($language)->setFallbackLocale('en_EN');
        }
     }
}

Hierbei wird der Translator geladen. Weiters wird in der onBoostrap – Methode der Request abgefangen und ausgelesen. Ich hole mir mithilfe von getCookie() die Cookies aus den Request. Warum hier konkret language geholt wird werden wir anschließend im Controller sehen. Wenn die Sprache gesetzt worden ist wird sie dem Translator übergeben bzw. die Locale gesetzt. Also jene Variable die die Sprache bestimmt. Als Fallback wird hier z.B. Englisch angenommen.

Weiters benötigen wir für die Lösung einen Controller der uns den Cookie setzt. ich habe ihn hier in einen ApiController von mir geladen der Json für den Ajax Call als Antwort zurückliefert.


class LanguageApiController extends AbstractActionController
{

   public function indexAction(){
      $language = htmlspecialchars($this->getRequest()->getQuery('language', 'de_DE'));
      $cookie = new SetCookie('language', $language);
      $this->getResponse()->setStatusCode(Response::STATUS_CODE_201);
      $this->getResponse()->getHeaders()->addHeader($cookie);
      return new JsonModel(array('cookiecreated'=>'ok', 'language'=>$language));
   }
}

Hier wird, wie wir später noch sehen werden, einfach die Sprache mit einen GET Parameter gesetzt. Es wird ein Cookie erstellt der ‚language‘ auf den ausgelesen Wert speichert. Anschließend wird er der Response übergeben und dem Header des Requests hinzugefügt. Zum Schluss benötigen wir noch das notwendige JavaScript um die Sprache umzustellen.


$(document).ready(function(){

    $(document).on('click','a[data-action|=languagechanger]',function(event){
        event.preventDefault();

        $.ajax({
            url: baseUrl + '/languagesetter',
            data: 'language='+$(this).attr('data-value')
        }).always(function(){
           location.reload();
        });
    })

});

Mit dieser einfachen Methode wird nun der Controller aufgerufen und der Cookie gesetzt. Anschließend wird die Seite neu geladen und wir haben die Sprache im Cookie gespeichert. Wir können nun einfach in der View die translate Methode des Viewhelpers aufrufen.


## übersetzungsfile
return array(
    'Search'=>'Suche', 
....);

## in der view
echo $this->translate('Search')

Ankeränderungen

Des öfteren kommt es vor das man anhand des Ankers auch in JavaScript bestimmte Aktionen setzen will, vor allem um z.B. Permalinks zu bieten oder Suchergebnisse, anhand des Links weiter geben zu können.

Nach kurzer Suche bin ich über folgenden Eintrag in Stackoverflow gestoßen http://stackoverflow.com/questions/2161906/handle-url-anchor-change-event-in-js


if ("onhashchange" in window) { // event supported?
    window.onhashchange = function () {
        hashChanged(window.location.hash);
    }
}
else { // event not supported:
    var storedHash = window.location.hash;
    window.setInterval(function () {
        if (window.location.hash != storedHash) {
            storedHash = window.location.hash;
            hashChanged(storedHash);
        }
    }, 100);
}

Da in den neueren Browsern das event bereits unterstützt wird, kann hier auf die übliche Art eine Callbackfunction gelegt werden, für ältere Browser geht der Umweg über eine Variable als Sicherung und der Prüfung auf Zeit ob sich der Hash geändert hat.

Zusätzlich zu den Änderungen will man auch oft den Hashwert nach Seitenaufbau (z.B. $(document).ready(function(){…}); ) wissen, hierzu kann einfach nach Seitenaufbau wieder das window Objekt hergenommen werden


 if(window.location.hash != null && window.location.hash != undefined && window.location.hash != ""){
        hashChanged(window.location.hash);
 }

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.

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:

Scroll to top