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')

Überschreiben des JsonModels – Automatische Konvertierung von Entities in Json gerechte Arrays

Heute habe ich die Klasse Zend\View\Model\JsonModel etwas erweitert um meine Entities automatisch in für das JsonModel benötigte Array umzuwandeln. Dies geschieht mit ein Paar einfachen Zeilen im neuen JsonModel


<?php
namespace Vico\ViewModel;

use Zend\View\Model\JsonModel as ZendJsonModel;
use Vico\Entity\BaseEntity;
/**
 * User: Christian Zellot
 * Date: 11.06.13
 * Time: 17:58
 */

class JsonModel extends  ZendJsonModel{

    /**
     * Erweitert den Konstruktor um die Fähigkeit Entity Klassen automatisch in Arrays umzuwandeln
     * @param  null|array|Traversable $variables
     * @param  array|Traversable $options
     */
    public function __construct($variables = null, $options = null)
    {
        $variables_copy = array();
        if($variables != null && is_array($variables)){
            foreach($variables as $variable){
                $variables_copy[] = BaseEntity::isSubclass($variable) ? $variable->toArray() : $variable;
            }
        }
        parent::__construct($variables_copy, $options);
    }

}

Außerdem habe ich das Base Entity um eine statische Methode erweitert, um diese Subklassen von sich erkennen zu lassen, dies geschieht einfach mit der Methode isSubclass($object)


<?php
namespace Vico\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* User: Zelle
* Date: 24.12.12
* Time: 12:22
*
* Base entity class from which all entities should inherit
*
*/
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);

    /**
     * checks if the given variable is an subclass of BaseEntity
     * @param $object
     * @return bool
     */
    public static function isSubclass($object){
        return is_subclass_of($object, __CLASS__);
    }
}

Dies bietet mir jetzt die Möglichkeit dem JsonModel ein Array mit Entities zu übergeben und diese werden automatisch in das passende Format gebracht. Lästige Umwandlungen in Arrays kann man sich also dadurch leicht ersparen.

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  ... 

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.

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:

Zend 2 – RESTful Request

Ich versuche nun schon seit längerer Zeit mithilfe von \Zend\Http\Request bzw \Zend\Http\PhpEnvironment\Request eine REST fähige Applikation auf die Beine zustellen. Leider habe ich festgestellt, das diese Klasse keine Methode bietet um Daten die mit PUT oder DELETE mitgeliefert werden auszulesen.
Nach ein paar kurzen Recherchen bin ich auf mehrere Posts auf stackoverflow gestoßen, die ähnliche Probleme hatten. (Auch allgemein in PHP z.B. http://stackoverflow.com/questions/2081894/handling-put-delete-arguments-in-php). Dort wurde die Möglichkeit erwähnt durch

<?php parse_str(file_get_contents('php://input'), $data); >

die Parameter auszulesen. Ich habe mir daraufhin eine Klasse geschrieben die im Grunde von der \Zend\Http\PhpEnvironment\Request Klasse erbt (diese befüllt sich bereits im Konstruktor selbst), mit der Erweiterung Daten auch bei PUT und DELETE auszulesen.
<?php
use Zend\Http\PhpEnvironment\Request as ZendRequest;
use Zend\Stdlib\ParametersInterface;
use Zend\Stdlib\Parameters;

/**
 * User: Zelle
 * Date: 18.01.13
 * Time: 21:04
 *
 */
class Request extends ZendRequest{

    private $putParams;
    private $deleteParams;

    /**
     * Erstellt aus den Zend Request Objekt ein eigenes Request Objekt das die moeglichkeit bietet
     * auch bei PUT und DELETE anfragen gesendete Daten auszulesen
     * @use $_SERVER
     * @use $_GET
     * @use $_POST
     * @use php://input
     *
     */
    public function __construct(){
        parent::__construct();
        $data = array();
        parse_str(file_get_contents('php://input'), $data);
        switch($this->method){
            case self::METHOD_PUT;
                $this->putParams = new Parameters($data);
                break;
            case self::METHOD_DELETE:
                $this->deleteParams = new Parameters($data);
                break;
        }
    }

    /**
     * Liefert aus den Put Container die passenden Variablen zurück
     *
     * @param null $name
     * @param null $default
     * @return mixed|null|\Zend\Stdlib\Parameters|\Zend\Stdlib\ParametersInterface
     */
    public function getPut($name = null, $default = null){
        if($this->getMethod() !== self::METHOD_PUT){
            return $default;
        }
        if ($this->putParams === null) {
            $this->putParams = new Parameters();
        }

        if ($name === null) {
            return $this->putParams;
        }

        return $this->putParams->get($name, $default);
    }

    /**
     *
     * @see \Zend\Http\Request setPost
     *
     * @param ParametersInterface $put
     * @return Request
     */
    public function setPut(ParametersInterface $put)
    {
        $this->putParams = $put;
        return $this;
    }

    /**
     * Liefert aus den Put Container die passenden Variablen zurück
     *
     * @param null $name
     * @param null $default
     * @return mixed|null|\Zend\Stdlib\Parameters|\Zend\Stdlib\ParametersInterface
     */
    public function getDelete($name = null, $default = null){
        if($this->getMethod() !== self::METHOD_DELETE){
            return $default;
        }
        if ($this->deleteParams === null) {
            $this->deleteParams = new Parameters();
        }

        if ($name === null) {
            return $this->deleteParams;
        }

        return $this->deleteParams->get($name, $default);
    }

    /**
     *
     * @see \Zend\Http\Request setPost
     *
     * @param ParametersInterface $put
     * @return Request
     */
    public function setDelete(ParametersInterface $put)
    {
        $this->putParams = $put;
        return $this;
    }

}


Diese muss jetzt nur mehr befüllt werden. Dies funktioniert über den ServiceManager in der Modulklasse des jeweiligen Modules. Nun kann im Controller mit $this->getRequest darauf zugegriffen werden oder auch über $this->getServiceLocator()->get(‚Request‘);.
use Zend\ModuleManager\Feature\ConfigProviderInterface,
    Zend\ModuleManager\Feature\ServiceProviderInterface;

class Module implements ConfigProviderInterface, ServiceProviderInterface
{

    /**
     * Expected to return \Zend\ServiceManager\Config object or array to
     * seed such an object.
     *
     * @return array|\Zend\ServiceManager\Config
     */
    public function getServiceConfig()
    {
        return array(
            'factories' => array(
                'Request' => function() {
                    return new \Medinfo\Request\Request();;

                },
            )
        );
    }

Weiters kann durch das Überschreiben des Request Objekts auch im Controller darauf zugegriffen werden

<?php 
use Zend\Mvc\Controller\AbstractActionController; 
use Zend\Http\Request; 
/**  
* User: Zelle  
* Date: 13.01.13  
* Time: 22:20  
*  
*/  class PatStammApiController extends AbstractActionController {     
public function putAction(){         
$request = $this->getRequest();
$sevlrequest = $this->getServiceLocator()->get('Request');

    }
}

Posts navigation

1 2
Scroll to top