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.

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.

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

    }
}

Scroll to top