Category: Zend Framework


Idee für einen einfachen, abstrakten Custom Validator mit PHP 5.3 Closures

Oktober 27th, 2010 — 11:45am

Gerade eben hatte ich Spaß daran mit der neuen Möglichkeit Closures in PHP 5.3 zu verwenden herumzuspielen. Mir kam die Idee, das Ganze mit Zend_Validate und Zend_Form zu einem Custom Validator zu kombinieren.

Folgende Klasse kam dabei heraus:

namespace Application\Validators;

class ClosureValidator extends \Zend_Validate_Abstract
{
    const GENERIC_ERROR_KEY = 'genericErrorKey';

    protected $_messageTemplates = array();

    /**
     * @var \Closure
     */
    private $predicate;

    public function __construct(\Closure $predicate)
    {
        $this->predicate = $predicate;
    }

    public function isValid($value, $context = null)
    {
        $predicate = $this->predicate;
        list($result, $errorMessage) = $predicate($value, $context);

        if(!is_bool($result))
            throw new \RuntimeException('First return value of predicate must be boolean value.');

        if(!is_string($errorMessage))
            throw new \RuntimeException('Second return value of predicate must be string value (error message).');

        if($result) return true;

        $this->_messageTemplates[self::GENERIC_ERROR_KEY] = $errorMessage;
        $this->_error(self::GENERIC_ERROR_KEY);

        return false;
    }
}

Wie habe ich die Klasse verwendet?

Die Klasse kam bei mir zum Einsatz bei einem Zend_Form mit dem ein User sein Passwort aktualisieren kann.

Die Bedingung war, dass der User sein Passwort nur aktualisieren kann, wenn er sein aktuelles Passwort richtig eingibt. Der Einfachheit halber gehe ich jetzt mal davon aus, dass die Passwörter in der DB im Klartext gespeichert sind und nicht gehashed (natürlich sollte man NIE die original Passwörter speichern, sondern immer nur als Hash!).

So wird zunächst mal das Formular verwendet (bekommt also eine User Instanz im Konstruktor mit übergeben):

$user = new \Application\Models\User();
$user->setPassword('CURRENT_PASSWORD');

$form = new \Application\Forms\UserPasswordChangeForm($user);

Im Formular wird der ClosureValidator dann wie folgt hinzugefügt:

namespace Application\Forms;

class UserPasswordChangeForm extends \Zend_Form
{
    /**
     * @var \Application\Models\User
     */
    private $user;

    public function __construct(\Application\Models\User $user, $options = null)
    {
        $this->user = $user;
        parent::__construct($options);
    }

    public function init()
    {
        $user = $this->user;

        $this->setMethod('post');

        $this->addElement(new \Zend_Form_Element_Password('currentPassword', array(
            'label'         => 'Current Password:',
            'required'      => true,
            'filters'       => array('StringTrim'),

            // Validator mit Closure hier hinzufügen
            'validators'    => array(new \Application\Validators\ClosureValidator(function($value) use(&$user) {
                return array(
                    $user->getPassword() == $value,
                    'Invalid Password'
                );
             })),
        )));

        // ... weitere Formularelemente weggelassen

        $this->addElement(new \Zend_Form_Element_Submit('submit', array(
            'ignore'   => true,
            'label'    => 'Save',
        )));
    }
}

Der Validator bekommt eine Closure Funktion mitübergeben, welche ein Array mit zwei Elementen zurückgeben muss: zum einen das Validation Ergebnis (boolean true oder false) und zum anderen einen String mit einer Error-Message, die im Fehler-Fall verwendet werden soll.

Über die Implementierung kann man sicher reden und obs das ganze einfacher oder komplizierter macht ist sicher auch diskussionswürdig, aber als Idee finde ich es doch irgendwie ganz nett. Mal schauen, ob man das so brauchen kann.

Comment » | PHP, Zend Framework

Zend Framework Screencasts

Oktober 27th, 2010 — 11:29am

Kurzer Tip für zwischendurch: wers nicht schon kennt, unter http://www.zendcasts.com/ sind sehr interessante Sceencasts verfügbar, welche verschiedene Themen rund um die Entwicklung mit dem Zend Framework beschreiben. Themen sind unter anderen Unit Testing, Doctrine 2 Integration, Access Control und noch einige andere. Die Screencasts erklären die Vorgehensweise wirklich gut, manchmal kann man über den Coding Stil zumindest diskutieren (wie meistens eigentlich), aber oft bringen einen die Screencasts auf neue Ideen wie man das ein oder andere Problem angehen könnte.

Comment » | PHP, Zend Framework

Doctrine 2 ins Zend Framework integrieren

Oktober 3rd, 2010 — 7:25pm

Ich verwende Doctrine in der Version 2.0.0RC1-DEV und das ZF 1.10. Die Doctrine Dateien kommen ins /library/ Verzeichnis (wo auch die Dateien des Zend Frameworks abgelegt sind), so dass dieses wie folgt aussieht:

library/
library/Zend/
library/Zend/Acl
...
library/Doctrine/
library/Doctrine/Common
library/Doctrine/DBAL
library/Doctrine/ORM
library/Doctrine/Symfony

Folgendes hat bei mir funktioniert, um Doctrine mit dem Zend Framework zum Laufen zu bringen:

Zuerst folgende Einstellungen in die application.ini (sind die Verbindungsdaten für die Datenbank):

db.host = localhost
db.username = root
db.password = password
db.dbname = my_db
db.driver = Pdo_Mysql

Danach diese Methode in die Bootstrap.php:

    protected function _initDb()
    {
        $dbConfig = $this->getOption('db');

        $db = Zend_Db::factory($dbConfig['driver'], array(
            'host'      => $dbConfig['host'],
            'username'  => $dbConfig['username'],
            'password'  => $dbConfig['password'],
            'dbname'    => $dbConfig['dbname'],
        ));

        return $db;
    }

Hier werden die DB Zugangsdaten ausgelesen und eine neue Datenbankverbindung per Zend_DB erstellt. Doctrine hat nämlich auch die Möglichkeit, bestehende Verbindungen, welche per PDO erstellt wurden, zu verwenden.

Jetzt kommen wir zum Doctrine Setup, ebenfalls in die Bootstrap.php NACH _initDb():

    protected function _initEntityManager()
    {
        require_once('Doctrine/Common/ClassLoader.php');

        $classLoader = new \Doctrine\Common\ClassLoader('Doctrine');
        $classLoader->setIncludePath(realpath(APPLICATION_PATH . '/../library/'));
        $classLoader->register();

        $config = new \Doctrine\ORM\Configuration();

        $cache = new \Doctrine\Common\Cache\ArrayCache();

        $config->setMetadataCacheImpl($cache);
        $config->setProxyDir(realpath(APPLICATION_PATH . '/proxies'));
        $config->setProxyNamespace('App\Proxies');
        $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver(realpath(APPLICATION_PATH . '/models')));
        $config->setAutoGenerateProxyClasses(true);
        // (01)
        $config->setSQLLogger(new Doctrine\DBAL\Logging\EchoSqlLogger());

        $connectionOptions = array(
           // (02)
            'pdo' => $this->getResource('db')->getConnection(),
        );

        $em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config);
        // (03)
        $em->getConnection()->setCharset('UTF8');

        // 04
        Zend_Registry::set('entityManager', $em);
    }

Wichtig ist, das im application Verzeichnis die Ordner /proxies (hier speichert Doctrine generierte Proxy Klassen ab) und /models (hier werden die DB Models abgelegt) existieren.

Folgende Anmerkungen noch zu den Fußnoten im Code:

  • 01: Diese Zeile reinnehmen wenn die von Doctrine generierten SQL Abfragen ausgegeben werden sollen
  • 02: Hier die in _initDb() erstellte DB Verbindung nutzen – getConnection() gibt das PDO Connection Objekt zurück
  • 03: Reinnehmen wenn die Daten in der DB UTF-8 encoded sind
  • 04: Ganz auf simpel und Ugly: den entityManager in der ZendRegistry ablegen – man könnte den entityManager ($em) auch aus der _initEntityManager Methode zurückgeben, dann würde dieser in einem Controller via $this->getInvokeArg('bootstrap')->getResource('entityManager') zur Verfügung stehen.

Comment » | Doctrine, PHP, Zend Framework

Action Helper in Zend Framework und PHP 5.3

September 15th, 2010 — 7:01pm

Gehen wir von folgendem ActionHelper aus (da ich PHP 5.3 verwende mit namespace):

namespace MyStuff\Controller\Action\Helper;

class Foo extends \Zend_Controller_Action_Helper_Abstract {
    public function direct() {
        print 'direct() called';
    }
}

Einen ActionHelper registriert man im Zend Framework in etwa so (hier in der Bootstrap.php):

class Bootstrap {
    protected function _initHelper() {
        Zend_Controller_Action_HelperBroker::addHelper(new \MyStuff\Controller\Action\Helper\Foo());
    }
}

Nun sollte der ActionHelper in einer ControllerAction ja wie folgt verfügbar sein (da wir ja die direct() Methode implentiert haben):

class MyController extends Zend_Controller_Abstract() {
    public function indexAction() {
        $this->_helper->foo();
    }
}

… aber statt der erwarteten Ausgabe wird eine Exception geworfen mit folgender Message:

Action Helper by name Foo not found

Das Problem ist die Methode Zend_Controller_Action_Helper_Abstract::getName() welche im Zend Framework wie folgt implementiert ist:

    public function getName()
    {
        $full_class_name = get_class($this);

        if (strpos($full_class_name, '_') !== false) {
            $helper_name = strrchr($full_class_name, '_');
            return ltrim($helper_name, '_');
        } else {
            return $full_class_name;
        }
    }

Der ActionHelper wird also im PriorityStack des HelperBrokers unter seinem vollen Klassennamen INKLUSIVE namespace registriert und kann daher nicht einfach per $this->_helper->foo(); (=Klassenname ohne namespace) angesprochen werden.

In dem wir die Methode Zend_Controller_Action_Helper_Abstract::getName() in unserem ActionHelper überschreiben, können wir abhilfe schaffen, bis das Zend Framework in einer zukünftigen Version PHP 5.3 vollständig unterstüzt:

namespace MyStuff\Controller\Action\Helper;

class Foo extends \Zend_Controller_Action_Helper_Abstract {
    const HELPER_NAME = 'Foo';

    public function direct() {
        print 'direct() called';
    }

    /**
     * @override
     */
    public function getName()
    {
        return self::HELPER_NAME;
    }
}

… dann klappts auch mit dem Aufruf in einer Controller Action via $this->_helper->foo();

Comment » | PHP, Zend Framework

Back to top