Zend Framework

Part 5 – user registration and activation finished. User login and logout. Zend_Auth.

Please, note that this text is written in May 2008. Since then Zend Framework have evolved and now provides Zend_Application class. It is good idea to use it. The user registration and login stuff, presented here is still actual though. Also there are many other ideas presented, which I hope will be usefull for the readers.

In part 5 of this series we will finish the registration process (actually saving the user data to database), implement sending email with activation link, implement the actual activation of users. Then we will implement login and logout actions in the site, utilizing Zend_Auth. In the next part we will implement very simple access controll system – for now without Zend_Acl, because we still don’t have idea what resources and roles we will have in our system later. On this stage of the development we will have only ‘admin’ module, which can be accessed by users set as admin by flag in their profile. But more on this – later.

From part 4 we have registration form ready, but after submiting it – nothing happens, so new it is time to fill in some code to handle form submition. We will need new table in our DB:

CREATE TABLE `users` (
  `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(32) NOT NULL,
  `password` VARCHAR(64) NOT NULL,
  `email` VARCHAR(200) NOT NULL,
  `active` TINYINT(4) NOT NULL,
  `code` VARCHAR(40) NOT NULL,
  `last_login` DATETIME NOT NULL,
  `registered_on` DATETIME NOT NULL,
  `gender` ENUM('male','female') NOT NULL,
  `birthday` DATE NOT NULL,
  `real_name` VARCHAR(64) NOT NULL,
  `isAdmin` TINYINT(4) NOT NULL DEFAULT '0',
  PRIMARY KEY  (`id`),
  UNIQUE KEY `username` (`username`),
  UNIQUE KEY `email` (`email`)
) ENGINE=INNODB;

Now we have to create our class Users, which will handle saving and retrieving user data to database.
Here is the code, comments are below:
application/models/Users.php:

< ?php
 
class Users extends Zend_Db_Table_Abstract
{
    protected $_name = 'users';
 
    public static function computePasswordHash($password)
    {
        return hash('sha256', '21' . $password . 'eoka3b');
    }
 
     /**
     * Adds user to database, sends confirmation email
     *
     * @param string $username
     * @param string $password
     * @param string $email
     * @throws Zend_Db_Statement_Exception
     * @return int The id of the new user
     */
    public function add($username, $password, $email)
    {
        $activationCode = sha1(uniqid('xyz', true)); 
 
        $newUser = array(
            'username' => $username,
            'password' => $this->computePasswordHash($password),
            'email' => $email,
            'active' => 0,
            'code' => $activationCode,
            'registered_on' => new Zend_Db_Expr('NOW()')
        );
 
        $userId = $this->insert($newUser);
 
        // send activation email
        $mailer = new Mailer();
        $languageCode = (string) Zend_Registry::get('Zend_Translate')->getAdapter()->getLocale();
        $activationLink = Zend_Registry::get('configuration')->general->url . '/user/activate/' . $userId . '/' . $activationCode;
        $mailer->sendRegistrationMail($email, $username, $activationLink, $languageCode);        
 
        return $userId;
    }
 
    public function editProfile($userId, $profileData)
    {
        $userRowset = $this->find($userId);
        $user = $userRowset->current();
        if (!$user)
        {
            throw new Zend_Db_Table_Exception('User with id '.$userId.' is not present in the database');
        }
 
        foreach ($profileData as $k => $v)
        {
            if (in_array($k, $this->_cols))
            {
                if ($k == $this->_primary)
                {
                    throw new Zend_Db_Table_Exception('Id of user cannot be changed');
                }
                // special case - hash have to be computed for password
                if ($k == 'password')
                {
                    $user->password = $this->computePasswordHash($v);
                }
                else
                {
                    $user->{$k} = $v;
                }
            }
        }
 
        $user->save();
 
        return $this;
    }
}

our class extends Zend_Db_Table_Abstract, so it inherits from there the functionality to save and retrieve data to and from our database. Because we need some higher level of abstraction we introduce some new public functions here – to add new user and to edit user profile. Also we have function to compute password hash – we will save passwords in the database in hashed format – with sha256 algorithm, prepending and appending the password with some random-looking but fixed strings. The idea behind that is if someone somehow gets access to the database and gets the hashes of the passwords – to prevent him from easy cracking and getting the actual passwords.

Lets take closer look at our public function add() – it takes 3 parameters – $username, $password and $email – strings. The code is quite simple – we generate random string activation code, which we will send to the user via email, then we construct an array for inserting in the users DB table and actually inserting the new row. Finally we send the email. We use not-yet-existing class – Mailer and its method – sendRegistrationMail. We will write these shortly. Also we have added new configuration lines in our config.ini file, one of which is general.url – which takes value something like “http://example.com” – the base url of our site – we use this to generate absolute URLs. We do not use $_SERVER['HTTP_HOST'] here, because later we may have different servers for different types of resources (for example our images may be hosted on different web server with different domain or subdomain). Full listing of our new config.ini will follow after a while, when we discus the Mailer class.

One more function we have in Users.php to comment – editProfile. It takes $userId as parameter along with an array $profileData, which contains the new, updated fields of the profile to save. User Id cannot be changed with this function, so we have explicit check for this – if trying to change the id we throw exception. For each element of the array $profileData we check if it is valid column from our users table – we do this by testing

if (in_array($k, $this->_cols))

in $this->_cols property we have an array, inherited by our base class, containing all columns for the database table. This way we can change our underlaying database schema without need to change some hardcoded values in this class.
It is important to note also that these two functions – both editProfile and add – will throw Zend_Db_Exception if unique constraints are broken – so if trying to insert new row with used already username or email address, or updating a row, changing username or email address to existing in other record.

Now lets see our Mailer class and after this – the new config.ini file:
application/models/Mailer.php

< ?php
 
require_once 'Zend/Mail.php';
require_once 'Zend/Mail/Transport/Smtp.php';
require_once 'Zend/Registry.php';
 
class Mailer
{
    /**
     * Directory path, where mail templates are located
     *
     * @var string
     */
    protected $templatesDir = '';
 
    /**
     * Constructor for the class, provide directory path where mail templates are saved
     *
     * @param string $templatesDir Directory path, where mail templates are located
     */
    public function __construct($templatesDir = 'languages/mailtemplates')
    {
        $this->templatesDir = $templatesDir;
 
        $mailConfig = Zend_Registry::get('configuration')->mail;
        if ($mailConfig->smtp)
        {
            $transport = new Zend_Mail_Transport_Smtp($mailConfig->host, $mailConfig->smtpconfig->toArray());
        }
        else
        {
            $transport = new Zend_Mail_Transport_Sendmail();
        }
 
        Zend_Mail::setDefaultTransport($transport);
 
    }
 
    public function sendRegistrationMail($emailAddress, $name, $activationLink, $languageCode)
    {
        $translate = Zend_Registry::get('Zend_Translate');
        $sitename = Zend_Registry::get('configuration')->general->sitename;
 
        $templatePath = Zend_Registry::get('siteRootDir') . '/' . $this->templatesDir . '/registration/' . $languageCode . '.txt';
        if (!is_file($templatePath))
        {
            throw new My_Exception('Missing template for registration mail - language code: '.$languageCode);
        }
 
        $templateTxt = file_get_contents($templatePath);
 
        //replace tags: [name], [sitename], [activation link]
        $templateTxt = str_replace('[name]', $name, $templateTxt);
        $templateTxt = str_replace('[sitename]', $sitename, $templateTxt);
        $templateTxt = str_replace('[activation_link]', $activationLink, $templateTxt);
 
        $mailer = new Zend_Mail('utf-8');
        $mailer->addTo($emailAddress, $name);
        $mailer->setSubject(sprintf($translate->_('Confirm your registeration in %s'), $sitename));
        $mailer->setBodyHtml($templateTxt, 'utf8');
        $mailer->setFrom(Zend_Registry::get('configuration')->mail->from);
        $mailer->send();
    }
}

We introduce new directory here – languages/mailtemplates – there we will save the templates for our email messages, which our application sends. For every mail type (registration email, lost password email and so on) we have different file for each language of the site. The construction of Mailer constructs Zend_Mail_Transport class for sendmail or smtp according to our settings and sets it as a default transport for Zend_Mail. sendMailRegistration loads the according template and replaces some tags into it, then creates instance of Zend_Mail and sends the new email. Nothing complex. Here is the config.ini file:
configuration/config.ini

[main]

;General settings
general.sitename=ZFSite
general.url=http://example.com

;Database connection settings
db.adapter=Mysqli
db.params.host=localhost
db.params.username=zfsite
db.params.password=123456
db.params.dbname=zfsite

;Mail transport settings

;mail.smpt - when false - sendmail is used
mail.smtp=true
mail.host=127.0.0.1
mail.smtpconfig.name=localhost
mail.smtpconfig.port=25
;mail.smtpconfig.auth = plain | login | crammd5
mail.smtpconfig.auth=
mail.smtpconfig.username=
mail.smtpconfig.password=
mail.from=no-reply@example.com

And the simple code for My_Exception class:
library/My/Exception.php

< ?php
 
require_once 'Zend/Exception.php';
 
class My_Exception extends Exception
{}

Our mailing system supports both smtp and sendmail methods, and when using smtp we have to set also host, port, authentication if any.

Before we see the registerAction method we will see the updated version of getRegistrationForm method of UserController class:

    private function getRegistrationForm(Users $users)
    {
        $form = new Zend_Form();
 
        $form->setAction('/user/register')->setMethod('post')->setAttrib('id', 'register');
 
        $filterTrim = new Zend_Filter_StringTrim();
        $validatorNotEmpty = new Zend_Validate_NotEmpty();
        $validatorNotEmpty->setMessage(__('This field is required, you cannot leave it empty'));
 
        $username = new Zend_Form_Element_Text('username');
        $validatorAlnum = new Zend_Validate_Alnum();
        $validatorAlnum->setMessage(__('You can use only latin letters and numbers'));
        $validatorStringLength = new Zend_Validate_StringLength(3, 32);
        $validatorStringLength->setMessages(array(
        Zend_Validate_StringLength::TOO_SHORT => __('Your username have to be between 3 and 32 symbols long'),
        Zend_Validate_StringLength::TOO_LONG => __('Your username have to be between 3 and 32 symbols long'),
        )
        );
        $validatorUniqueUsername = new My_Validate_DbUnique($users, 'username');
        $validatorUniqueUsername->setMessage(__('This username is already registered, please choose another one.'));
        $username->addValidator($validatorNotEmpty, true)->setRequired(true)->setLabel('Username')
        ->addFilter($filterTrim)
        ->addValidator($validatorAlnum)
        ->addValidator($validatorStringLength)
        ->addValidator($validatorUniqueUsername);
        $form->addElement($username);
 
        /**
         * @todo Change this wired error messages to something more user friendly, or even use simple email regex matching validator
         */
        $email = new Zend_Form_Element_Text('email');
        $validatorHostname = new Zend_Validate_Hostname();
        $validatorHostname->setMessages(
        array(
        Zend_Validate_Hostname::IP_ADDRESS_NOT_ALLOWED  => __("'%value%' appears to be an IP address, but IP addresses are not allowed"),
        Zend_Validate_Hostname::UNKNOWN_TLD             => __("'%value%' appears to be a DNS hostname but cannot match TLD against known list"),
        Zend_Validate_Hostname::INVALID_DASH            => __("'%value%' appears to be a DNS hostname but contains a dash (-) in an invalid position"),
        Zend_Validate_Hostname::INVALID_HOSTNAME_SCHEMA => __("'%value%' appears to be a DNS hostname but cannot match against hostname schema for TLD '%tld%'"),
        Zend_Validate_Hostname::UNDECIPHERABLE_TLD      => __("'%value%' appears to be a DNS hostname but cannot extract TLD part"),
        Zend_Validate_Hostname::INVALID_HOSTNAME        => __("'%value%' does not match the expected structure for a DNS hostname"),
        Zend_Validate_Hostname::INVALID_LOCAL_NAME      => __("'%value%' does not appear to be a valid local network name"),
        Zend_Validate_Hostname::LOCAL_NAME_NOT_ALLOWED  => __("'%value%' appears to be a local network name but local network names are not allowed")
        )
        );
 
        $validatorEmail = new Zend_Validate_EmailAddress(Zend_Validate_Hostname::ALLOW_DNS, false, $validatorHostname);
        $validatorEmail->setMessages(
        array(
        Zend_Validate_EmailAddress::INVALID            => __("'%value%' is not a valid email address"),
        Zend_Validate_EmailAddress::INVALID_HOSTNAME   => __("'%hostname%' is not a valid hostname for email address '%value%'"),
        Zend_Validate_EmailAddress::INVALID_MX_RECORD  => __("'%hostname%' does not appear to have a valid MX record for the email address '%value%'"),
        Zend_Validate_EmailAddress::DOT_ATOM           => __("'%localPart%' not matched against dot-atom format"),
        Zend_Validate_EmailAddress::QUOTED_STRING      => __("'%localPart%' not matched against quoted-string format"),
        Zend_Validate_EmailAddress::INVALID_LOCAL_PART => __("'%localPart%' is not a valid local part for email address '%value%'")
        )
        );
        $validatorUniqueEmail = new My_Validate_DbUnique($users, 'email');
        $validatorUniqueEmail->setMessage(__('This email address is already registered, please choose another one.'));
        $email->addValidator($validatorNotEmpty, true)->setRequired(true)->setLabel('Email Address')
        ->addFilter($filterTrim)
        ->addValidator($validatorEmail)
        ->addValidator($validatorUniqueEmail);
        $form->addElement($email);
 
        $password = new Zend_Form_Element_Password('password');
        $password->addValidator($validatorNotEmpty, true)->setRequired(true)->setLabel('Password')
        ->addValidator(new Zend_Validate_StringLength(3));
        $form->addElement($password);
 
        $password2 = new Zend_Form_Element_Password('password2');
        $validatorPassword = new My_Validate_PasswordConfirmation('password');
        $validatorPassword->setMessage(__('Passwords do not match'));
        $password2->setLabel('Confirm Password')->addValidator($validatorPassword);
        $form->addElement($password2);
 
        $gender = new Zend_Form_Element_Select('gender');
        $gender->setLabel('Gender')
        ->addMultiOption('',' ')->addMultiOption('male',__('male'))->addMultiOption('female',__('female'));
        $form->addElement($gender);
 
        $date = new My_Form_Element_DateSelects('birthday');
        $validatorDate = new Zend_Validate_Date();
        $validatorDate->setMessages(
        array(
        Zend_Validate_Date::NOT_YYYY_MM_DD => __("'%value%' is not of the format YYYY-MM-DD"),
        Zend_Validate_Date::INVALID        => __("'%value%' does not appear to be a valid date"),
        Zend_Validate_Date::FALSEFORMAT    => __("'%value%' does not fit given date format")
        )
        );
        $date->setLabel('Birthdate')->addValidator($validatorDate);
        $date->setShowEmptyValues(true)->setStartEndYear(1900, date("Y")-7)->setReverseYears(true);
 
        $form->addElement($date);
 
        $realName = new Zend_Form_Element_Text('realname');
        $realName->setLabel('Real Name')->addFilter($filterTrim);
        $form->addElement($realName);
 
        $validatorNotEmptyAgreement = new Zend_Validate_NotEmpty();
        $validatorNotEmptyAgreement->setMessage(__('You have to accept our terms and conditions before you register'));
        $agreement = new Zend_Form_Element_Checkbox('agreement');
        $agreement->setLabel('I agree to terms and conditions')
        ->addValidator($validatorNotEmptyAgreement, true)->setRequired(true);
        $form->addElement($agreement);
 
        if (!isset($this->session->passedRegisterCaptcha) || !$this->session->passedRegisterCaptcha)
        {
            //if we have set captcha in the session for this request - use it, else generate new one
            if (isset($this->session->registerCaptcha))
            {
                $captchaCode = $this->session->registerCaptcha;
            }
            else
            {
                $md5Hash = md5($_SERVER['REQUEST_TIME']);
                $captchaCode = substr($md5Hash, rand(0, 25), 5);
                $this->session->registerCaptcha = $captchaCode ;
            }
 
            $captcha = new Zend_Form_Element_Text('captcha');
            $validatorIdentical = new Zend_Validate_Identical($captchaCode);
            $validatorIdentical->setMessage('The text entered is not the same as the shown one, please try again.');
            $captcha->setLabel('Enter the text')
            ->addValidator($validatorIdentical, true)
            ->addValidator($validatorNotEmpty, true)->setRequired(true);
            $captchaDecorator = new My_Form_Decorator_Captcha();
            $captchaDecorator->setOption('namespace', 'User')->setOption('captchaId', 'registerCaptcha');
            $captchaDecorator->setTag('div');
            $captcha->addDecorator($captchaDecorator);
            $form->addElement($captcha);
 
        }
 
        $submit = new Zend_Form_Element_Submit('register');
        $submit->setLabel('Submit');
        $form->addElement($submit);
 
        return $form;
    }

now the function takes one argument – $users – instance of class Users. This is needed because of a new validator which we will attach to username and email address fields. This validator will check if the value, which is entered by the user, already exists in the users DB table and if it exists – the validation will fail. The purpose of this is to prevent users to register with dublicate username and email addresses. The validator is My_Validate_DbUnique and here is its listing:

library/My/Validate/DbUnique.php

< ?php
require_once 'Zend/Validate/Abstract.php';   
 
class My_Validate_DbUnique extends Zend_Validate_Abstract
{
    const NOT_UNIQUE = 'dbUniqueNotUnique';
 
    protected $_messageTemplates = array(
        self::NOT_UNIQUE => "'%column%' '%value' already exists"
    );
 
    /**
    * @var array
    */
    protected $_messageVariables = array(
        'column'  => '_column',
    );
 
    /**
     * The table where to check for unique value in column
     *
     * @var Zend_Db_Table
     */
    protected $_dbTable = NULL;
 
    /**
     * The column name where to check for unique value
     *
     * @var string
     */
    protected $_column = '';
 
    /**
     * The values of the primary key for this row if updating - to exclude the current row from the test
     *
     * @var array
     */
    protected $_rowPrimaryKey = NULL;
 
    public function __construct(Zend_Db_Table_Abstract $table, $column, $rowPrimaryKey = NULL)
    {
        $this->_dbTable = $table;
        $this->_column = $column;
        $this->_rowPrimaryKey = $rowPrimaryKey;
    }
    public function isValid($value)
    {
        $this->_setValue($value);
 
        $select = $this->_dbTable->select();
        $select->where($this->_dbTable->getAdapter()->quoteInto($this->_column . ' = ?', $value));
        if (isset($this->_rowPrimaryKey))
        {
            $rowPrimaryKey = (array) $this->_rowPrimaryKey;
            $info = $this->_dbTable->info();
 
            foreach ($info['primary'] as $key => $column)
            {
                $select->where($this->_dbTable->getAdapter()->quoteInto($column . ' != ?', $rowPrimaryKey[$key - 1]));
            }
        }
 
        $row = $this->_dbTable->fetchAll($select);
        if ($row->count())
        {
            $this->_error();
            return false;
        }
 
        return true;
    }
}

Its constructor takes 3 arguments – instance of Zend_Db_Table_Abstract – in our case this is instance of Users, column name for which to check the uniqueness of the value and the third argument is used only when updating old row – then the primary key of the current row is given (in array format if the key is from more than one field).

It is important to understand, that having these validators here does not prevent actual insertion in the DB of rows with not unique username for example. Race condition can occur – imagine that 2 users are registering at the same time with same username. They both click on the submit button on same time (I know this is not at all likely situation, but the probability exist and we should prevent ugly error message or not expected application dying) – then if both validators are run for the 2 users before their DB rows are inserted then for the second user process, which tries to insert the DB row – exception will be thrown, so we will just try {} and catch the exception. Now it’s time to see the actual registration form handling code – here is the registerAction in our UserController class:

    public function registerAction()
    {
        $usersTable = new Users();
        $this->view->registerForm = $this->getRegistrationForm($usersTable);
        if ($this->getRequest()->isPost())
        {
            if ($this->view->registerForm->isValid($_POST))
            {
                $values = $this->view->registerForm->getValues();
 
                $badRegistration = false;
                try
                {
                    $newUserId = $usersTable->add($values['username'], $values['password'], $values['email']);
                }
                catch (Zend_Db_Statement_Exception $e)
                {
                    // race condition can occure between validator and actual DB operation
                    // so check if username or email is already registered
                    // NOTE: this should be very rare case, because we check this in validators just before inserting the row
                    $message = $e->getMessage();
 
                    if (strpos($message, $values['email']) !== false)
                    {
                        //email already registered
                        $badRegistration = true;
                        $this->view->globalPageError = 'email';
                    }
                    else if (strpos($message, $values['username']) !== false)
                    {
                        //username already registered
                        $badRegistration = true;
                        $this->view->globalPageError = 'username';
                    }
                    else
                    {
                        // other error, rethrow the exception
                        throw $e;
                    }
 
                }
 
                if (!$badRegistration)
                {
                    $profileFields = array(
                        'gender' => $values['gender'],
                        'birthday' => $values['birthday'],
                        'real_name' => $values['realname']
                    );
 
                    $usersTable->editProfile($newUserId, $profileFields);
 
                    // delete captcha code from session:
                    $this->session->registerCaptcha = NULL;
 
                    /*
                     * after successfull submit of the form, we want to prevent double submit with the
                     * same data, because this will lead to error - trying to register again the same
                     * username, so we set in our session 'success flag' and redirect to activate action
                     */
 
                    $this->session->userJustRegistered = $values['username'];
                    $this->_redirect($this->_helper->url('activate', 'user'), array('exit'=>true));
                }
            }
 
            //we want to know if we have passed the captcha, so won't show it again:
            if (!isset($this->session->passedRegisterCaptcha) || !$this->session->passedRegisterCaptcha)
            {
                $captchaField = $this->view->registerForm->captcha;
                if ($captchaField->isValid($captchaField->getValue()))
                {
                    $this->session->passedRegisterCaptcha = true;
                    $this->view->registerForm->removeElement('captcha');
                }
            }
 
        }
 
    }

After user is registered – we save his username in the session and redirect him to the activate action of the controller. There he should recieve message that the he is registered and to check his email for the registration link. Other things that the activate action should do is to actually activate the users when they click on the link. Here is the code:

    public function activateAction()
    {
        $parameters = $this->_getAllParams();
        if (isset($parameters['userId']) && isset($parameters['activationCode']))
        {
            // activate the user if the activation code is valid.
            $usersTable = new Users();
            $rows = $usersTable->find($parameters['userId']);
            $user = $rows->current();
            if ($user)
            {
                if (!$user->active && $user->code == $parameters['activationCode'])
                {
                    // activate the user
                    $user->active = 1;
                    $user->save();
                    $this->view->activatedOK = true;
                }
                else if ($user->active)
                {
                    $this->view->userAlreadyActive = true;
                }
            }
        }
 
        if (isset($this->session->userJustRegistered))
        {
            $this->view->username = $this->session->userJustRegistered;
 
            // use 'user/justRegistered.phtml' template.
            $this->renderScript('user/justRegistered.phtml');
        }
    }

If the user is not activating, but have just registered – we render different view template than the default one – user/justRegistered.phtml. Here it is:

< ?php echo sprintf($this->translate->_('Welcome to our site. You are now registered with username %s.
In order to activate your account, please check the email you provided in the registration
process. We have sent you email message with your account information. Please follow the activation link,
which you will find in the email message.'), '<strong>'.$this-&gt;escape($this-&gt;username).'</strong>') ?&gt;

And the template for the actual activation: application/modules/default/views/scripts/user/activate.phtml

&lt; ?php if ($this-&gt;activatedOK) { ?&gt;
    &lt; ?php echo $this-&gt;translate-&gt;_('Thank you for your registration. Your account is now active.'); ?&gt;
&lt; ? } else if ($this-&gt;userAlreadyActive) { ?&gt;
    &lt; ?php echo $this-&gt;translate-&gt;_('This account is already active.'); ?&gt;
&lt; ? } else { ?&gt;
    &lt; ?php echo $this-&gt;translate-&gt;_('Bad activation code. If you have followed an email sent by us may be your registration is expired because you have not activated it for several days. You can register again if you wish.'); ?&gt;
&lt; ? } ?&gt;

Now the registration and activation should be working. We register some users, activate them and… we need a login form.

    private function getLoginForm()
    {
        $form = new Zend_Form();
        $form-&gt;setAction('/user/login')-&gt;setMethod('post')-&gt;setAttrib('id', 'login');
 
        $username = new Zend_Form_Element_Text('username');
        $username-&gt;setLabel('Username');
        $form-&gt;addElement($username);
 
        $password = new Zend_Form_Element_Password('password');
        $password-&gt;setLabel('Password');
        $form-&gt;addElement($password);
 
        $submit = new Zend_Form_Element_Submit('login');
        $submit-&gt;setLabel('Submit');
        $form-&gt;addElement($submit);
 
        return $form;
    }

getLoginForm is method of UserController. Here is the loginAction itself:

    public function loginAction()
    {
        if ($this-&gt;_helper-&gt;authUsers-&gt;isLoggedIn())
        {
            $this-&gt;_redirect($this-&gt;_helper-&gt;url('index', 'index'), array('exit'=&gt;true));
        }
 
        $this-&gt;view-&gt;loginForm = $this-&gt;getLoginForm();
 
        if ($this-&gt;getRequest()-&gt;isPost())
        {
            if ($this-&gt;view-&gt;loginForm-&gt;isValid($_POST))
            {
                $username = $this-&gt;view-&gt;loginForm-&gt;getValue('username');
                $password = $this-&gt;view-&gt;loginForm-&gt;getValue('password');
                $auth = Zend_Auth::getInstance();
 
                $db = Zend_Registry::get('db');
                $authAdapter = new Zend_Auth_Adapter_DbTable($db, 'users', 'username', 'password');
                $authAdapter-&gt;setIdentity($username)-&gt;setCredential(Users::computePasswordHash($password));
                $authResult = $auth-&gt;authenticate($authAdapter);
                if ($authResult-&gt;isValid())
                {
                    //valid username and password
                    $userInfo = $authAdapter-&gt;getResultRowObject();
                    if ($userInfo-&gt;active)
                    {
                        //save userinfo in session
                        $userInfo-&gt;password = '';
                        Zend_Registry::get('defSession')-&gt;currentUser = $userInfo;
 
                        //redirect to home
                        $this-&gt;_redirect($this-&gt;_helper-&gt;url('index', 'index'), array('exit'=&gt;true));
                    }
                    else
                    {
                        $this-&gt;view-&gt;notActivated = true;
                        $auth-&gt;clearIdentity();
                    }
                }
                else
                {
                    $this-&gt;view-&gt;loginError = true;
                }
            }
        }
    }

The action helper authUsers will be examined in a while. We utilize Zend_Auth_Adapter_DbTable as adapter for Zend_Auth and if the user have provided valid username and password we use getResultRowObject() to retrieve the full info for this user. If the user is activated we delte the password hash and save the user info to the session. Then redirect to index. If the user is not activated we use clearIdentity() method to effectively logout him and set notActivated variable of the view object to true – we will show appropriate message there.

Here it is the code for the login view template:
application/modules/default/views/scripts/user/login.phtml

Enter your username and password to login:
&lt; ?php echo $this-&gt;loginForm; ?&gt;
 
&lt; ? if ($this-&gt;loginError) : ?&gt;
<div class="errors">Invalid password</div>
&lt; ? endif; ?&gt;
 
&lt; ? if ($this-&gt;notActivated) : ?&gt;
<div class="errors">Your account is not yet activated, please check your email and click on the activation link there.</div>
&lt; ? endif; ?&gt;

Now back to the action helper. We need an easy way to determine whether the current user of the application has authenticated or not. We will use action helper for this.
library/My/Controller/Action/Helper/AuthUsers.php

&lt; ?php
 
require_once 'Zend/Controller/Action/Helper/Abstract.php';
 
class My_Controller_Action_Helper_AuthUsers extends Zend_Controller_Action_Helper_Abstract
{
    /**
     * Constructor
     *
     * @return void
     */
    public function __construct()
    {
    }
 
    public function preDispatch()
    {
        $actionController = $this-&gt;getActionController();
        $actionController-&gt;view-&gt;isLoggedIn = $this-&gt;isLoggedIn();
        $actionController-&gt;view-&gt;username = $this-&gt;getUsername();
    }
 
    public function isLoggedIn()
    {
        $auth = Zend_Auth::getInstance();
        return $auth-&gt;hasIdentity();
    }
 
    public function getUsername()
    {
        $auth = Zend_Auth::getInstance();
        $identity = $auth-&gt;getIdentity();
        if (!$identity)
        {
            return null;
        }
 
        return $identity;
    }
}

This action helper provides two mehtods – isLoggedIn() and getUsername() – as the names suggest the first returns true if the user is authenticated with valid username and password, and the second one returns the username (or null if not logged in).
The action helper also have preDispatch hook where it sets two view template variables – isLoggedIn and username. Simple enough.
Now lets change our main layout to include sort of menu column:

application/layouts/main.phtml

&lt; !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
<div id="container">
<div id="header">
<h1><a href="/">Zf Site</a></h1>
</div>
<div id="main-holder">
<div id="main">
&lt; ?php echo $this-&gt;layout()-&gt;content ?&gt;</div>
<div id="subnav">
<h2>Sub-Nav Column</h2>
<ul>
&lt; ?php if ($this-&gt;isLoggedIn) :  ?&gt;
	<li>Hello &lt; ?php echo $this-&gt;username; ?&gt;!</li>
	<li><a href="/user/logout">Logout</a></li>
&lt; ? else : ?&gt;
	<li><a href="/user/login">Login</a></li>
	<li><a href="/user/register">Register</a></li>
&lt; ? endif; ?&gt;</ul>
</div>
</div>
<div id="extra">
<h2>Extra Column</h2>
Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. 
 
Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet. Lorem ipsum sit dolor amet.</div>
<div id="footer">footer placeholder</div>
</div>

Here we check the view variable isLoggedIn set by the preDispatch hook and displays appropriate links. We have a login form working now, but we still need logout action in our UserController. Here it is:

    public function logoutAction()
    {
        $auth = Zend_Auth::getInstance();
        $auth-&gt;clearIdentity();
        Zend_Registry::get('defSession')-&gt;currentUser = null;
        $this-&gt;_redirect($this-&gt;_helper-&gt;url('index', 'index'), array('exit'=&gt;true));
    }

We call clearIdentity method of Zend_Auth and delete the user info from the session. Then redirect to index.

Some changed are needed in our bootstrap file to tie things together. The include path statement now includes /application/models path:

set_include_path(
    $siteRootDir . '/library' . PATH_SEPARATOR
    . $siteRootDir . '/application' . PATH_SEPARATOR
    . $siteRootDir . '/application/models' . PATH_SEPARATOR
    . get_include_path()
);

We save the $siteRootDir variable in the registry:

//save $siteRootDir in registry:
$registry-&gt;set('siteRootDir', $siteRootDir);

New route is added to the router for the user activation:

$activateUserRoute = new Zend_Controller_Router_Route('user/activate/:userId/:activationCode',
    array('controller'=&gt;'user', 'action'=&gt;'activate')
    );
$router-&gt;addRoute('activateUser', $activateUserRoute);

And finally – we add our new action helper – authUsers to the helper broker, so its preDispatch hook is executed.

Zend_Controller_Action_HelperBroker::addPath('My/Controller/Action/Helper', 'My_Controller_Action_Helper');
$authUsersHelper = new My_Controller_Action_Helper_AuthUsers();
Zend_Controller_Action_HelperBroker::addHelper($authUsersHelper);

Here is the whole bootstrap.php file:

&lt; ?php
// For our dev environment we will report all errors to the screen
error_reporting(E_ALL | E_STRICT);
ini_set('display_startup_errors', 1);
ini_set('display_errors', 1);
 
// Set our timezone
date_default_timezone_set('Europe/Sofia');   
 
// Add /library and /application directory to our include path
$siteRootDir = dirname($_SERVER['DOCUMENT_ROOT']);
 
set_include_path(
    $siteRootDir . '/library' . PATH_SEPARATOR
    . $siteRootDir . '/application' . PATH_SEPARATOR
    . $siteRootDir . '/application/models' . PATH_SEPARATOR
    . get_include_path()
);
 
// Turn on autoloading, so we do not include each Zend Framework class
require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();
 
// Create registry object and setting it as the static instance in the Zend_Registry class
$registry = new Zend_Registry();
Zend_Registry::setInstance($registry);
 
//save $siteRootDir in registry:
$registry-&gt;set('siteRootDir', $siteRootDir);
 
// Load configuration file and store the data in the registry
$configuration = new Zend_Config_Ini($siteRootDir . '/configuration/config.ini', 'main');
Zend_Registry::set('configuration', $configuration);
 
// Construct the database adapter class, connect to the database and store the db object in the registry
$db = Zend_Db::factory($configuration-&gt;db);
$db-&gt;query("SET NAMES 'utf8'");
Zend_Registry::set('db', $db);
// set this adapter as default for use with Zend_Db_Table
Zend_Db_Table_Abstract::setDefaultAdapter($db);
 
// Now set session save handler to our custom class which saves the data in MySQL database
$sessionManager = new My_Session_Manager();
Zend_Session::setOptions(array(
    'gc_probability' =&gt; 1,
    'gc_divisor' =&gt; 5000
    ));
Zend_Session::setSaveHandler($sessionManager);
 
// we will always use session, so this is good place to create this and save it to the registry
$defSession = new Zend_Session_Namespace('Default', true);
Zend_Registry::set('defSession', $defSession);
 
// Setup translation adapter
// Check if language is set in session - if not - use english as default
$lang = 'en';
if (isset($defSession-&gt;lang))
{
    $lang = $defSession-&gt;lang;
}
$translate = new Zend_Translate('gettext', $siteRootDir . '/languages/'.$lang.'.mo', $lang);
Zend_Registry::set('Zend_Translate', $translate);
Zend_Form::setDefaultTranslator($translate);
 
// Setup the Front Controller, disable the error handler, set our controller directories
$frontController = Zend_Controller_Front::getInstance();
$frontController-&gt;throwExceptions(true);
$frontController-&gt;addModuleDirectory($siteRootDir . '/application/modules');
//we want the front controller to return the response, instead of emitting it automatically
$frontController-&gt;returnResponse(true);
 
//$frontController-&gt;setParam('noErrorHandler', true);
 
// define some routes (URLs)
$router = $frontController-&gt;getRouter();
$captchaRoute = new Zend_Controller_Router_Route('captcha/get/:namespace/:captchaId',
    array('controller'=&gt;'captcha', 'action'=&gt;'get')
    );
$router-&gt;addRoute('captcha', $captchaRoute);
 
$activateUserRoute = new Zend_Controller_Router_Route('user/activate/:userId/:activationCode',
    array('controller'=&gt;'user', 'action'=&gt;'activate')
    );
$router-&gt;addRoute('activateUser', $activateUserRoute);    
 
/*
 * We want to set the encoding to UTF-8, so we won't rely on the ViewRenderer action helper by default,
 * but will construct view object and deliver it to the ViewRenderer after setting some options.
 */
$view = new Zend_View(array('encoding'=&gt;'UTF-8'));
//$view-&gt;addHelperPath($siteRootDir . '/library/My/View/Helper', 'My_View_Helper');
$view-&gt;addHelperPath('My/View/Helper', 'My_View_Helper_');
$viewRendered = new Zend_Controller_Action_Helper_ViewRenderer($view);
Zend_Controller_Action_HelperBroker::addHelper($viewRendered);
Zend_Controller_Action_HelperBroker::addPath('My/Controller/Action/Helper', 'My_Controller_Action_Helper');
$authUsersHelper = new My_Controller_Action_Helper_AuthUsers();
Zend_Controller_Action_HelperBroker::addHelper($authUsersHelper);
 
// Now we initialize the Zend_Layout object with MVC support
Zend_Layout::startMvc(
    array(
        'layoutPath' =&gt; $siteRootDir . '/application/layouts',
        'layout' =&gt; 'main'
    )
);
 
// run the dispatch, get the response and send it to the client
$response = $frontController-&gt;dispatch();
$response-&gt;sendResponse();
 
function __($string)
{
    return $string;
}

One little change to application/modules/default/views/scripts/index/index.phtml:

&lt; ?php if ($this-&gt;isLoggedIn) :  ?&gt;
    Hello &lt; ?php echo $this-&gt;username; ?&gt;, this is your (not so yet) personalized home page :)
&lt; ? else : ?&gt;
    Hello anonymous user, you can register in the site by <a href="/user/register">clicking here</a> or <a href="/user/login">login</a>.
&lt; ? endif; ?&gt;

so we know if we are logged in or not.
Again many new things in this part, I hope I have not missed something – here is the code for download: part5.rar

Share and Enjoy: These icons link to social bookmarking sites where readers can share and discover new web pages.
  • bodytext
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • Fark
  • Furl
  • Live
  • Ma.gnolia
  • Reddit
  • Spurl
  • Technorati

Building Sample Web Site Based On Zend Framework Series
Zend Framework

Comments (24)

Permalink

Part 4 – Zend_Form, CAPTCHA, Password Confirmation, Date selector field, Zend_Translate

Please, note that this text is written in May 2008. Since then Zend Framework have evolved and now provides Zend_Application class. It is good idea to use it. The user registration and login stuff, presented here is still actual though. Also there are many other ideas presented, which I hope will be usefull for the readers.

Long title for part 4 and much new things to cover while building the registration form. In this part I will cover just the registration form, and not the actual registration process, but believe me – this is not small topic at all :)

EDIT: This code is written with Zend Framework version 1.5. Since then version 1.6 have been released. There we can find many new components, along with Zend_Captcha and Zend_Form_Element_Captcha. It takes different approach than mine in showing the captcha, you can still take a look at my code for an example how to build components that you need but are not shipped with Zend Framework. But I recommend in new projects to use Zend_Captcha and Zend_Form_Element_Captcha.

First we will write the outline for our User controller. We will rely for now on the default URLS provided to us by Zend_Controller_Router_Rewrite, so assuming we are running from localhost these are the actions we will have for the user controller:

http://localhost/user/login

http://localhost/user/register

http://localhost/user/lostpassword

http://localhost/user/activate – here users will activate their profiles after registration (clicking on a link in sent email)
http://localhost/user/profile – here users will be able to edit their profiles

and of course we need index action – default action, which for now we will redirect to the register action. So here is the outline of
application/modules/default/controllers/UserController.php‘:

&lt; ?php
require_once 'Zend/Controller/Action.php';
 
class UserController extends Zend_Controller_Action
{
    public function indexAction()
    {
        $this-&gt;_forward('register');
    }
 
    public function loginAction()
    {
    }
 
    public function registerAction()
    {
 
	}
 
    public function lostpasswordAction()
    {
 
    }
 
    public function activateAction()
    {
 
    }
 
    public function profileAction()
    {
 
    }
}

in the index action we just redirect to register action in the same controller. And here – in registerAction() – is where we will concentrate in this part of the series. I want here to have form with the following elements: username, email address, password, password confirmation – these will be required, gender, birth date, real name, checkbox for terms and conditions or agreement – tell it what you want – also required to be checked for registration to take place, and finally – CAPTCHA field.

It is good idea to keep the code, which generates a form separated from the other logic, so several approaches are possible here – to extend Zend_Form class is one obvious way to go, but here I will write getRegistrationForm() method in the UserController class. This method (or member function, I still don’t know which term is better for this :) ) is quite long and have many interesting points, so I will guide you step by step in the process of writing it, instead of just pasting the code and dropping small notes on it. So first we will write small form with just one field (in ‘application/modules/default/controllers/UserController.php‘ we add this function as member function for the class)

private function getRegistrationForm()
{
	$form = new Zend_Form();
 
	$form-&gt;setAction('/user/register')-&gt;setMethod('post')-&gt;setAttrib('id', 'register');
 
	$username = new Zend_Form_Element_Text('username');
	$validatorAlnum = new Zend_Validate_Alnum();
	$username-&gt;setRequired(true)-&gt;setLabel('Username');
	$form-&gt;addElement($username);
 
	$submit = new Zend_Form_Element_Submit('register');
	$submit-&gt;setLabel('Submit');
	$form-&gt;addElement($submit);
 
	return $form;
}

now in registerAction() we call this getRegistrationForm() function and assign the returned form as property of our View object. Then we check if the request is POST (so the form is submitted) and then call isValid() method of the form object – this method hides the big magic of Zend_Form – with just this simple call we have saved ourselves from typing many tedious lines of code validating the form input, assigning error messages and setting default values to allready filled in fields of the form. I strongly recommend to all of you, who will use Zend_Form for something more serious than just the default behaviour which it provides – to read through isValid() method of Zend_Form and follow the flow of the logic there – validating each of the fields of the form by calling their own isValid() method, which in turn iterates over the validators added to the fields and checks if the value provided is valid. Actually here – in Zend_Form_Element::isValid() – is the place, where the value of the element is stored in the element with setValue() call. So if you want to use getValue() method first you should validate the field (or the whole form). What we also do in registerAction() is to save the values submitted in a property of the View object, so we can verify how our form works (this is just debug code, we will use this in the view template)

    public function registerAction()
    {
        $this-&gt;view-&gt;registerForm = $this-&gt;getRegistrationForm();
        if ($this-&gt;getRequest()-&gt;isPost())
        {
            if ($this-&gt;view-&gt;registerForm-&gt;isValid($_POST))
            {
                $values = $this-&gt;view-&gt;registerForm-&gt;getValues();
                $this-&gt;view-&gt;values = $values;
            }
        }
      }

and here is the view template:
application/modules/default/views/scripts/user/register.phtml

<h1>Register:</h1>
&lt; ?php echo $this-&gt;registerForm-&gt;render(); ?&gt;
 
&lt; ?php var_dump($this-&gt;values); ?&gt;

Very simple for now, so lets throw in some more complicated things – filters and validators. But before this we need to have a way to set custom error messages to the validators keeping in mind that we want to make multilangual web site. Zend_Translate will help us with this. I will use gettext translation adapter, because I really don’t like to write the files with messages for translation on my own, and Poedit has the nice feature of parsing source files and gathering strings for translation. So in our bootstrap.php we add:

//we will always use session, so this is good place to create this and save it to the registry
$defSession = new Zend_Session_Namespace('Default', true);
Zend_Registry::set('defSession', $defSession);
 
// Setup translation adapter
// Check if language is set in session - if not - use english as default
$lang = 'en';
if (isset($defSession-&gt;lang))
{
    $lang = $defSession-&gt;lang;
}
$translate = new Zend_Translate('gettext', $siteRootDir . '/languages/'.$lang.'.mo', $lang);
Zend_Registry::set('Zend_Translate', $translate);
Zend_Form::setDefaultTranslator($translate);

So we add to our project new directory – languages/ – where language files will be placed. I want Poedit to parse my source files and autodiscover my strings which need to be translated. When we use validators, for example Zend_Validate_StringLength – we want to set custom error messages. Because for the choosen language to translate to is possible to be different at the time of defining the error messages to the time of showing of the form – we cannot do:

$validatorStringLength = new Zend_Validate_StringLength(3, 32);
        $validatorStringLength-&gt;setMessages(array(
            Zend_Validate_StringLength::TOO_SHORT =&gt; $translate-&gt;_('Your username have to be between 3 and 32 symbols long'),
            Zend_Validate_StringLength::TOO_LONG =&gt; $translate-&gt;_('Your username have to be between 3 and 32 symbols long'),
            )
        );

If we go like this we get the translation at the time of the defining the message, and we DO NOT use the translator which we set for Zend_Form. In fact the translator will be used but the already translated strings will be used as messages to be again translated – and of course they won’t be in the messages database with the translations.
What we want is to have:
Zend_Validate_StringLength::TOO_SHORT => ‘Your username have to be between 3 and 32 symbols long’,
but this way Poedit won’t find our string. So here is my tricky solution to this. I introduce a function __($string) – these are two underscores. So I put in my bootstrap.php (for now, later all this auxiliary functions should go into some library, for now we have just 1 of these):

function __($string)
{
    return $string;
}

So the function just returns its argument.
Now some tips for Poedit. When we create the catalog in the Keywords tab we can add function names, from which to collect messages for translation in addition to the default _() function. Here I add __ – two underscores. Also I have added setLabel, but you can just use the __() function for the purposes of adding Labels to form elements also. Another thing you should do is to add .phtml file extension to the Parser setup for PHP files, so your template files are parsed actually. And finally – add ‘-L php’ to Parser command, so to become ‘xgettext –force-po -o %o %C %K %F -L php’. This prevents nasty error from showing.

Now back to our getRegistrationForm() and lets add the other fields (except the birthdate and the captcha) with desired validators and filters.

&lt; ?php
	private function getRegistrationForm()
    {
        $form = new Zend_Form();
 
        $form-&gt;setAction('/user/register')-&gt;setMethod('post')-&gt;setAttrib('id', 'register');
 
        $filterTrim = new Zend_Filter_StringTrim();
        $validatorNotEmpty = new Zend_Validate_NotEmpty();
        $validatorNotEmpty-&gt;setMessage(__('This field is required, you cannot leave it empty'));
 
        $username = new Zend_Form_Element_Text('username');
        $validatorAlnum = new Zend_Validate_Alnum();
        $validatorAlnum-&gt;setMessage(__('You can use only latin letters and numbers'));
        $validatorStringLength = new Zend_Validate_StringLength(3, 32);
        $validatorStringLength-&gt;setMessages(array(
            Zend_Validate_StringLength::TOO_SHORT =&gt; __('Your username have to be between 3 and 32 symbols long'),
            Zend_Validate_StringLength::TOO_LONG =&gt; __('Your username have to be between 3 and 32 symbols long'),
            )
        );
        $username-&gt;addValidator($validatorNotEmpty, true)-&gt;setRequired(true)-&gt;setLabel('Username')
        -&gt;addFilter($filterTrim)
        -&gt;addValidator($validatorAlnum)
        -&gt;addValidator($validatorStringLength);
        $form-&gt;addElement($username);
 
        /**
         * @todo Change this wired error messages to something more user friendly, or even use simple email regex matching validator
         */
        $email = new Zend_Form_Element_Text('email');
        $validatorHostname = new Zend_Validate_Hostname();
        $validatorHostname-&gt;setMessages(
            array(
                Zend_Validate_Hostname::IP_ADDRESS_NOT_ALLOWED  =&gt; __("'%value%' appears to be an IP address, but IP addresses are not allowed"),
                Zend_Validate_Hostname::UNKNOWN_TLD             =&gt; __("'%value%' appears to be a DNS hostname but cannot match TLD against known list"),
                Zend_Validate_Hostname::INVALID_DASH            =&gt; __("'%value%' appears to be a DNS hostname but contains a dash (-) in an invalid position"),
                Zend_Validate_Hostname::INVALID_HOSTNAME_SCHEMA =&gt; __("'%value%' appears to be a DNS hostname but cannot match against hostname schema for TLD '%tld%'"),
                Zend_Validate_Hostname::UNDECIPHERABLE_TLD      =&gt; __("'%value%' appears to be a DNS hostname but cannot extract TLD part"),
                Zend_Validate_Hostname::INVALID_HOSTNAME        =&gt; __("'%value%' does not match the expected structure for a DNS hostname"),
                Zend_Validate_Hostname::INVALID_LOCAL_NAME      =&gt; __("'%value%' does not appear to be a valid local network name"),
                Zend_Validate_Hostname::LOCAL_NAME_NOT_ALLOWED  =&gt; __("'%value%' appears to be a local network name but local network names are not allowed")
            )
        );
 
        $validatorEmail = new Zend_Validate_EmailAddress(Zend_Validate_Hostname::ALLOW_DNS, false, $validatorHostname);
        $validatorEmail-&gt;setMessages(
            array(
                Zend_Validate_EmailAddress::INVALID            =&gt; __("'%value%' is not a valid email address"),
                Zend_Validate_EmailAddress::INVALID_HOSTNAME   =&gt; __("'%hostname%' is not a valid hostname for email address '%value%'"),
                Zend_Validate_EmailAddress::INVALID_MX_RECORD  =&gt; __("'%hostname%' does not appear to have a valid MX record for the email address '%value%'"),
                Zend_Validate_EmailAddress::DOT_ATOM           =&gt; __("'%localPart%' not matched against dot-atom format"),
                Zend_Validate_EmailAddress::QUOTED_STRING      =&gt; __("'%localPart%' not matched against quoted-string format"),
                Zend_Validate_EmailAddress::INVALID_LOCAL_PART =&gt; __("'%localPart%' is not a valid local part for email address '%value%'")
            )
        );
        $email-&gt;addValidator($validatorNotEmpty, true)-&gt;setRequired(true)-&gt;setLabel('Email Address')
        -&gt;addFilter($filterTrim)
        -&gt;addValidator($validatorEmail);
        $form-&gt;addElement($email);
 
        $password = new Zend_Form_Element_Password('password');
        $password-&gt;addValidator($validatorNotEmpty, true)-&gt;setRequired(true)-&gt;setLabel('Password')
        -&gt;addValidator(new Zend_Validate_StringLength(3));
        $form-&gt;addElement($password);
 
        $password2 = new Zend_Form_Element_Password('password2');
        $password2-&gt;setLabel('Confirm Password');
        $form-&gt;addElement($password2);
 
        $gender = new Zend_Form_Element_Select('gender');
        $gender-&gt;setLabel('Gender')
        -&gt;addMultiOption('',' ')-&gt;addMultiOption('male',__('male'))-&gt;addMultiOption('female',__('female'));
        $form-&gt;addElement($gender);
 
        $realName = new Zend_Form_Element_Text('realname');
        $realName-&gt;setLabel('Real Name')-&gt;addFilter($filterTrim);
        $form-&gt;addElement($realName);
 
        $validatorNotEmptyAgreement = new Zend_Validate_NotEmpty();
        $validatorNotEmptyAgreement-&gt;setMessage(__('You have to accept our terms and conditions before you register'));
        $agreement = new Zend_Form_Element_Checkbox('agreement');
        $agreement-&gt;setLabel('I agree to terms and conditions')
            -&gt;addValidator($validatorNotEmptyAgreement, true)-&gt;setRequired(true);
        $form-&gt;addElement($agreement);
 
        $submit = new Zend_Form_Element_Submit('register');
        $submit-&gt;setLabel('Submit');
        $form-&gt;addElement($submit);
 
        return $form;
    }

Test the form. Everything should work, except the password confirmation field. We need a way to check if password2 is the same as password. So we will write our own validator, extending Zend_Validate_Abstract. We will name our validator My_Validate_PasswordConfirmation, and it will be located in ‘library/My/Validate/PasswordConfirmation.php‘ file.

&lt; ?php
require_once 'Zend/Validate/Abstract.php';   
 
class My_Validate_PasswordConfirmation extends Zend_Validate_Abstract
{
    const NOT_MATCH = 'passwordConfirmationNotMatch';
 
    protected $_messageTemplates = array(
        self::NOT_MATCH =&gt; "Passwords do not match"
    );
 
    protected $fieldToMatch = '';
 
    public function __construct($fieldToMatch)
    {
        $this-&gt;fieldToMatch = (string) $fieldToMatch;
    }
    public function isValid($value, $context = null)
    {
        $valueString = (string) $value;
        $this-&gt;_setValue($valueString);
 
        if (!isset($context[$this-&gt;fieldToMatch]) || $context[$this-&gt;fieldToMatch] !==  $valueString)
        {
            $this-&gt;_error(self::NOT_MATCH);
            return false;
        }
 
        return true;
    }
}

The constructor of the validator has one argument – the name of the field in the form, which value our validating field should match. The isValid() method uses the $context parameter, which is passed by the Zend_Form isValid() method and contains the whole $_POST array. So we just check if the value of our validating field matches the value of the field, given in the constructor. So back in our getRegistrationForm():

        $password2 = new Zend_Form_Element_Password('password2');
        $validatorPassword = new My_Validate_PasswordConfirmation('password');
        $validatorPassword-&gt;setMessage(__('Passwords do not match'));
        $password2-&gt;setLabel('Confirm Password')-&gt;addValidator($validatorPassword);
        $form-&gt;addElement($password2);

and now password confirmation works too.

Two intersting things remain to be done: birthday field and CAPTCHA.
First lets look at the birthday field. We can always use simple text field here, telling the user to enter a date in the format “YYYY-MM-DD”, then validate this with Zend_Validate_Date and hope that our users will understand what we mean with this “YYYY-MM-DD” :) I don’t like this. Another approach is to use some fancy date picker, which fills hidden field with our desired format. This is better. But what I want here is three selects – for day, month and year. But I want this 3 selects to work as one field in the form. So I can tell something like $date = new Zend_Form_Element_Date(‘date’). Well, I use ‘My_’ naming convention for my library code, se the name will be My_Form_Element_DateSelects. In getRegistrationForm(), after our gender field, lets add this:

$date = new My_Form_Element_DateSelects('birthdate');
        $validatorDate = new Zend_Validate_Date();
        $validatorDate-&gt;setMessages(
            array(
                Zend_Validate_Date::NOT_YYYY_MM_DD =&gt; __("'%value%' is not of the format YYYY-MM-DD"),
                Zend_Validate_Date::INVALID        =&gt; __("'%value%' does not appear to be a valid date"),
                Zend_Validate_Date::FALSEFORMAT    =&gt; __("'%value%' does not fit given date format")
            )
        );
        $date-&gt;setLabel('Birthdate')-&gt;addValidator($validatorDate);
        $date-&gt;setShowEmptyValues(true)-&gt;setStartEndYear(1900, date("Y")-7)-&gt;setReverseYears(true);
 
        $form-&gt;addElement($date);

Our custom field will support several options
- showEmpty: if true, will add empty options at the top of the select, so allowing the user not to select any date.
- startYear and endYear – just the range of the year select
- reverseYears – if true – the years will be printed in reverse order.

Here is our My_Form_Element_DateSelects class:
library/My/Form/Element/DateSelects.php

&lt; ?php
require_once 'Zend/Form/Element.php';
 
class My_Form_Element_DateSelects extends Zend_Form_Element_Xhtml
{
    /**
     * Use formSelect view helper by default
     * @var string
     */
    public $helper = 'formDateSelects';    
 
    /**
     * This array will hold options:
     * showEmpty - bool, if true will show and allow empty date
     * startYear, endYear - start and end year to show
     * reverseYears - if true - years will be print from most recent backwards
     *
     * Zend_Form_Decorator_ViewHelper will pass this array as argument to the
     * view helper, responsible for rendering this element
     *
     * @var array
     */
    public $options = array();  
 
    public function init()
    {
        $this-&gt;options['showEmpty'] = true;
        $this-&gt;options['startYear'] = 1900;
        $this-&gt;options['endYear'] = (int) date("Y");
        $this-&gt;options['reverseYears'] = false;
    }
 
    public function setShowEmptyValues($value)
    {
        $this-&gt;options['showEmpty'] = (bool) $value;
        return $this;
    }
 
    public function setStartEndYear($start = null, $end = null)
    {
        if ($start)
        {
            $this-&gt;options['startYear'] = (int) $start;
        }
 
        if ($end)
        {
            $this-&gt;options['endYear'] = (int) $end;
        }
        return $this;
    }
 
    public function setReverseYears($value)
    {
        $this-&gt;options['reverseYears'] = (bool) $value;
        return $this;
    }
 
    /**
     * We want to get the date from our auxiliary fields here
     *
     * @param mixed $value
     * @param mixed $context
     * @return boolean
     */
    public function isValid($value, $context = null)
    {
        $fieldName = $this-&gt;getName();
        $auxiliaryFieldsNames = $this-&gt;getDayMonthYearFieldNames($fieldName);
        if (isset($context[$auxiliaryFieldsNames['day']]) &amp;&amp; isset($context[$auxiliaryFieldsNames['month']])
                &amp;&amp; isset($context[$auxiliaryFieldsNames['year']]))
        {
            if ($context[$auxiliaryFieldsNames['year']] == '-'
                || $context[$auxiliaryFieldsNames['month']] == '-'
                || $context[$auxiliaryFieldsNames['day']] == '-')
            {
                $value = null;
            }
            else
            {
                $value = str_pad($context[$auxiliaryFieldsNames['year']], 4, '0', STR_PAD_LEFT) . '-'
                    . str_pad($context[$auxiliaryFieldsNames['month']], 2, '0', STR_PAD_LEFT) . '-'
                    . str_pad($context[$auxiliaryFieldsNames['day']], 2, '0', STR_PAD_LEFT);
            }
 
            $this-&gt;setValue($value);
        }
 
        return parent::isValid($value, $context);
    }
 
    /**
     * Makes day, month and year names from given element name. Special case is array notation.
     *
     * Given a value such as foo[bar][baz], the generated names will be
     * foo[bar][baz_day], foo[bar][baz_month] and foo[bar][baz_year]
     * I know it is bad design to have this function here and in the View Helper,
     * but I really can't think of other way
     *
     * @param  string $value
     * @return array
     */
    protected function getDayMonthYearFieldNames($value)
    {
        if (empty($value) || !is_string($value)) {
            return $value;
        }
 
        $ret = array(
                'day' =&gt; $value . '_day',
                'month' =&gt; $value . '_month',
                'year' =&gt; $value . '_year'
                );
 
        if (strstr($value, '['))
        {
            $endPos = strlen($value) - 1;
            if (']' != $value[$endPos]) {
                return $ret;
            }
 
            $start = strrpos($value, '[') + 1;
            $name = substr($value, $start, $endPos - $start);
            $arrayName = substr($value, 0, $start-1);
            $ret = array(
                    'day' =&gt; $arrayName . '[' . $name . '_day' . ']',
                    'month' =&gt; $arrayName . '[' . $name . '_month'  . ']',
                    'year' =&gt; $arrayName . '[' . $name . '_year' . ']'
                    );
        }
        return $ret;
    }
}

and the corresponding view helper for rendering it:
library/My/View/Helper/FormDateSelect.php

&lt; ?php
require_once 'Zend/View/Helper/FormElement.php';
 
class My_View_Helper_FormDateSelects extends Zend_View_Helper_FormElement
{
    public function formDateSelects($name, $value = null, $attribs = null,
    $options = null, $listsep = "\n")
    {
        $info = $this-&gt;_getInfo($name, $value, $attribs, $options, $listsep);
        extract($info); // name, id, value, attribs, options, listsep, disable
 
        // now start building the XHTML.
        $disabled = '';
        if (true === $disable) {
            $disabled = ' disabled="disabled"';
        }
 
        $elementNamesArray = $this-&gt;getDayMonthYearFieldNames($name);
        $valueDay = $valueMonth = $valueYear = null;
 
        if ($value !== null)
        {
            $valueExploded = explode('-', $value);
            if (!isset($valueExploded[2]))
            $value = null;
            else
            {
                $valueDay = (int) $valueExploded[2];
                $valueMonth = (int) $valueExploded[1];
                $valueYear = (int) $valueExploded[0];
            }
        }
 
        // Build the surrounding day element first.
        $xhtml = '
<select id="' . $this-&gt;view-&gt;escape($id . '_day') . '" name="' . $this-&gt;view-&gt;escape($elementNamesArray['day']) . '">_htmlAttribs($attribs)
        . "&gt;\n    ";
 
        // build the list of options
        $list = array();
        if ($options['showEmpty'])
        {
            $list[] = '<option value="-"> </option>';
        }
        for ($i = 1; $i &lt; = 31; $i++)
        {
            $list[] = '' . $i . '';
        }
 
        // add the options to the xhtml and close the select
        $xhtml .= implode("\n    ", $list) . "\n</select>
 
";
 
        // Build the month next
        $xhtml .= '
<select id="' . $this-&gt;view-&gt;escape($id . '_month') . '" name="' . $this-&gt;view-&gt;escape($elementNamesArray['month']) . '">_htmlAttribs($attribs)
        . "&gt;\n    ";
 
        // build the list of options
        $list = array();
        if ($options['showEmpty'])
        {
            $list[] = '<option value="-"> </option>';
        }
        for ($i = 1; $i &lt; = 12; $i++)
        {
            $list[] = '' . $i . '';
        }
 
        // add the options to the xhtml and close the select
        $xhtml .= implode("\n    ", $list) . "\n</select>
 
";
 
        // Build the years next
        $xhtml .= '
<select id="' . $this-&gt;view-&gt;escape($id . '_year') . '" name="' . $this-&gt;view-&gt;escape($elementNamesArray['year']) . '">_htmlAttribs($attribs)
        . "&gt;\n    ";
 
        // build the list of options
        $list = array();
        if ($options['showEmpty'])
        {
            $list[] = '<option value="-"> </option>';
        }
 
        if ($options['reverseYears'])
        {
            for ($i = $options['endYear']; $i &gt;= $options['startYear']; $i--)
            {
                $list[] = '<option selected="selected" value="' . $i . '">' . $i . '</option>';
            }
        }
        else
        {
            for ($i = $options['startYear']; $i &gt;= $options['endYear']; $i++)
            {
                $list[] = '<option selected="selected" value="' . $i . '">' . $i . '</option>';
            }
        }
 
        // add the options to the xhtml and close the select
        $xhtml .= implode("\n    ", $list) . "\n</select>
 
";
 
        return $xhtml;
    }
 
    /**
     * Makes day, month and year names from given element name. Special case is array notation.
     *
     * Given a value such as foo[bar][baz], the generated names will be
     * foo[bar][baz_day], foo[bar][baz_month] and foo[bar][baz_year]
     *
     * @param  string $value
     * @return array
     */
    protected function getDayMonthYearFieldNames($value)
    {
        if (empty($value) || !is_string($value)) {
            return $value;
        }
 
        $ret = array(
                'day' =&gt; $value . '_day',
                'month' =&gt; $value . '_month',
                'year' =&gt; $value . '_year'
                );
 
                if (strstr($value, '['))
                {
                    $endPos = strlen($value) - 1;
                    if (']' != $value[$endPos]) {
                        return $ret;
                    }
 
                    $start = strrpos($value, '[') + 1;
                    $name = substr($value, $start, $endPos - $start);
                    $arrayName = substr($value, 0, $start-1);
                    $ret = array(
                    'day' =&gt; $arrayName . '[' . $name . '_day' . ']',
                    'month' =&gt; $arrayName . '[' . $name . '_month'  . ']',
                    'year' =&gt; $arrayName . '[' . $name . '_year' . ']'
                    );
                }
                return $ret;
    }
}

what is interesting is the getDayMonthYearFieldNames() method, which constructs the names for the three “subfields” of our date field. If the name of the data field is array based – we make sure our subfields to have according names. We duplicate the function in both the view helper and the element class itself. May be it is better decision to make this function outside these classes and make it independant, so we avoid the code duplication, but I will go with this design just for now.
In My_Form_Element_DateSelects – isValid() method – we construct the “YYYY-MM-DD” format of the date, getting the auxiliary day, month and year values from the $context (we have all submitted values here). Then we just call parent::isValid() method with our new constructed value.
One last thing remains for this to work: adding the path to our view helper to the view object. This we will do in the bootstrap, so add there:

$view-&gt;addHelperPath('My/View/Helper', 'My_View_Helper_');

just after constructing the view object.

And finally – the CAPTCHA. We will generate the challange text in the user controller and will save it to session. We can show it again if not matched and take it from the session in our captcha controller, so we render the same challange text that we generated :) . The code is quite straightforward. Lets add new controller – CaptchaController.
application/modules/default/controllers/CaptchaController.php

&lt; ?php
 
class CaptchaController extends Zend_Controller_Action
{
    public function init()
    {
        $this-&gt;_helper-&gt;layout-&gt;disableLayout();
        $this-&gt;_helper-&gt;viewRenderer-&gt;setNoRender();
    }
 
    public function getAction()
    {
        $parameters = $this-&gt;_getAllParams();
 
        $width = 150;
        $height = 40; 
 
        $image = ImageCreate($width, $height);
 
        if (isset($parameters['namespace']) &amp;&amp; isset($parameters['captchaId']))
        {
            $session = new Zend_Session_Namespace($parameters['namespace']);
            $text = $session-&gt;{$parameters['captchaId']};
 
            if ($text)
            {
                $grey = ImageColorAllocate($image, 190, 190, 190);
                $black = ImageColorAllocate($image, 0, 0, 0);
                ImageString($image, 20, 50, 15, $text, $black);
            }
        }
 
        $this-&gt;_response-&gt;setHeader('Content-Type', 'image/gif');
        imagegif($image);
    }
}

In init() we disable the layout and tell the view renderer to not render.
We get as parameters the session namespace and the session key where the challange text is saved, then create new GD image, write the text in it, set content type in header to ‘image/gif’ and writing the gif image. Zend_Controller_Dispatcher_Standard which we use implicitly uses output buffering, so this gif image is captured as response body, and sent to the client from our bootstrap.php file.
To show this captcha image on our form we will use decorator, so we write
library/My/Form/Decorator/Captcha.php‘:

&lt; ?php
 
require_once 'Zend/Form/Decorator/Abstract.php';
 
/**
 * My_Form_Decorator_Captcha
 *
 * Shows CAPTCHA image
 *
 * Options that must be provided are:
 * - namespace: The id of the captcha to show (the key in the session namespace, where the value is saved)
 * - captchaId: The id of the captcha to show (the key in the session namespace, where the value is saved)
 * - tag: tag to use in decorator
 */
class My_Form_Decorator_Captcha extends Zend_Form_Decorator_Abstract
{
    /**
     * Default placement: append
     * @var string
     */
    protected $_placement = 'PREPEND';
 
    /**
     * HTML tag with which to surround image
     * @var string
     */
    protected $_tag;   
 
    /**
     * Set HTML tag with which to surround image
     *
     * @param  string $tag
     * @return My_Form_Decorator_Captcha
     */
    public function setTag($tag)
    {
        $this-&gt;_tag = (string) $tag;
        return $this;
    }
 
    /**
     * Get HTML tag, if any, with which to surround image
     *
     * @return void
     */
    public function getTag()
    {
        if (null === $this-&gt;_tag) {
            $tag = $this-&gt;getOption('tag');
            if (null !== $tag) {
                $this-&gt;removeOption('tag');
                $this-&gt;setTag($tag);
            }
            return $tag;
        }
 
        return $this-&gt;_tag;
    }
 
    /**
     * Render a captcha image
     *
     * @param  string $content
     * @return string
     */
    public function render($content)
    {
        $element = $this-&gt;getElement();
        $view    = $element-&gt;getView();
        if (null === $view) {
            return $content;
        }
 
        $tag       = $this-&gt;getTag();
        $placement = $this-&gt;getPlacement();
        $separator = $this-&gt;getSeparator();
 
        $namespace = $this-&gt;getOption('namespace');
        $captchaId = $this-&gt;getOption('captchaId');
 
        if (!$namespace || !$captchaId)
        {
            require_once ('Zend/Form/Decorator/Exception.php');
            $exception = new Zend_Form_Decorator_Exception('namespace or captchaId not set');
            throw $exception;
        }
 
        $image = '<img src="/captcha/get/'.$namespace.'/'.$captchaId.'" alt="CAPTCHA challange" />'; 
 
        if (null !== $tag) {
            require_once 'Zend/Form/Decorator/HtmlTag.php';
            $decorator = new Zend_Form_Decorator_HtmlTag();
            $decorator-&gt;setOptions(array('tag' =&gt; $tag));
            $image = $decorator-&gt;render($image);
        }
 
        switch ($placement) {
            case self::PREPEND:
                return $image . $separator . $content;
            case self::APPEND:
            default:
                return $content . $separator . $image;
        }
    }
}

As template for this decorator I have used Zend_Form_Decorator_Image, so there are snippets of code taken from there.
In order to use shorter URLs like /captcha/get/[namespace]/[captchaId], and not like /captcha/get/namespace/[namespace]/captchaId/[captchaId] we need to define custom route in our router, so in our bootstrap.php:

// define some routes (URLs)
$router = $frontController-&gt;getRouter();
$captchaRoute = new Zend_Controller_Router_Route('captcha/get/:namespace/:captchaId',
    array('controller'=&gt;'captcha', 'action'=&gt;'get')
    );
$router-&gt;addRoute('captcha', $captchaRoute);

and now add the captcha field to the form:
in getRegistrationForm():

if (!isset($this-&gt;session-&gt;passedRegisterCaptcha) || !$this-&gt;session-&gt;passedRegisterCaptcha)
        {
            //if we have set captcha in the session for this request - use it, else generate new one
            if (isset($this-&gt;session-&gt;registerCaptcha))
            {
                $captchaCode = $this-&gt;session-&gt;registerCaptcha;
            }
            else
            {
                $md5Hash = md5($_SERVER['REQUEST_TIME']);
                $captchaCode = substr($md5Hash, rand(0, 25), 5);
                $this-&gt;session-&gt;registerCaptcha = $captchaCode ;
            }
 
            $captcha = new Zend_Form_Element_Text('captcha');
            $captcha-&gt;setLabel('Enter the text')
                -&gt;addValidator(new Zend_Validate_Identical($captchaCode))
                -&gt;addValidator($validatorNotEmpty, true)-&gt;setRequired(true);
            $captchaDecorator = new My_Form_Decorator_Captcha();
            $captchaDecorator-&gt;setOption('namespace', 'User')-&gt;setOption('captchaId', 'registerCaptcha');
            $captchaDecorator-&gt;setTag('div');
            $captcha-&gt;addDecorator($captchaDecorator);
            $form-&gt;addElement($captcha);
        }

so here is the whole UserContoller.php:

&lt; ?
require_once 'Zend/Controller/Action.php';
 
class UserController extends Zend_Controller_Action
{
    /**
     * Session namespace object for user data
     *
     * @var Zend_Session_Namespace
     */
    public $session = null;
 
    public function init()
    {
        $this-&gt;session = new Zend_Session_Namespace('User');
        $this-&gt;view-&gt;translate = Zend_Registry::get('Zend_Translate');
    }
 
    public function indexAction()
    {
        $this-&gt;_forward('register');
    }
 
    public function loginAction()
    {
    }
 
    public function registerAction()
    {
        $this-&gt;view-&gt;registerForm = $this-&gt;getRegistrationForm();
        if ($this-&gt;getRequest()-&gt;isPost())
        {
            if ($this-&gt;view-&gt;registerForm-&gt;isValid($_POST))
            {
                $values = $this-&gt;view-&gt;registerForm-&gt;getValues();
                $this-&gt;view-&gt;values = $values;
            }
 
            //we want to know if we have passed the captcha, so won't show it again:
            if (!isset($this-&gt;session-&gt;passedRegisterCaptcha) || !$this-&gt;session-&gt;passedRegisterCaptcha)
            {
                $captchaField = $this-&gt;view-&gt;registerForm-&gt;captcha;
                if ($captchaField-&gt;isValid($captchaField-&gt;getValue()))
                {
                    $this-&gt;session-&gt;passedRegisterCaptcha = true;
                    $this-&gt;view-&gt;registerForm-&gt;removeElement('captcha');
                }
            }
 
        }
    }
 
    public function lostpasswordAction()
    {
 
    }
 
    public function activateAction()
    {
 
    }
 
    public function profileAction()
    {
 
    }
 
    private function getRegistrationForm()
    {
        $form = new Zend_Form();
 
        $form-&gt;setAction('/user/register')-&gt;setMethod('post')-&gt;setAttrib('id', 'register');
 
        $filterTrim = new Zend_Filter_StringTrim();
        $validatorNotEmpty = new Zend_Validate_NotEmpty();
        $validatorNotEmpty-&gt;setMessage(__('This field is required, you cannot leave it empty'));
 
        $username = new Zend_Form_Element_Text('username');
        $validatorAlnum = new Zend_Validate_Alnum();
        $validatorAlnum-&gt;setMessage(__('You can use only latin letters and numbers'));
        $validatorStringLength = new Zend_Validate_StringLength(3, 32);
        $validatorStringLength-&gt;setMessages(array(
            Zend_Validate_StringLength::TOO_SHORT =&gt; __('Your username have to be between 3 and 32 symbols long'),
            Zend_Validate_StringLength::TOO_LONG =&gt; __('Your username have to be between 3 and 32 symbols long'),
            )
        );
        $username-&gt;addValidator($validatorNotEmpty, true)-&gt;setRequired(true)-&gt;setLabel('Username')
        -&gt;addFilter($filterTrim)
        -&gt;addValidator($validatorAlnum)
        -&gt;addValidator($validatorStringLength);
        $form-&gt;addElement($username);
 
        /**
         * @todo Change this wired error messages to something more user friendly, or even use simple email regex matching validator
         */
        $email = new Zend_Form_Element_Text('email');
        $validatorHostname = new Zend_Validate_Hostname();
        $validatorHostname-&gt;setMessages(
            array(
                Zend_Validate_Hostname::IP_ADDRESS_NOT_ALLOWED  =&gt; __("'%value%' appears to be an IP address, but IP addresses are not allowed"),
                Zend_Validate_Hostname::UNKNOWN_TLD             =&gt; __("'%value%' appears to be a DNS hostname but cannot match TLD against known list"),
                Zend_Validate_Hostname::INVALID_DASH            =&gt; __("'%value%' appears to be a DNS hostname but contains a dash (-) in an invalid position"),
                Zend_Validate_Hostname::INVALID_HOSTNAME_SCHEMA =&gt; __("'%value%' appears to be a DNS hostname but cannot match against hostname schema for TLD '%tld%'"),
                Zend_Validate_Hostname::UNDECIPHERABLE_TLD      =&gt; __("'%value%' appears to be a DNS hostname but cannot extract TLD part"),
                Zend_Validate_Hostname::INVALID_HOSTNAME        =&gt; __("'%value%' does not match the expected structure for a DNS hostname"),
                Zend_Validate_Hostname::INVALID_LOCAL_NAME      =&gt; __("'%value%' does not appear to be a valid local network name"),
                Zend_Validate_Hostname::LOCAL_NAME_NOT_ALLOWED  =&gt; __("'%value%' appears to be a local network name but local network names are not allowed")
            )
        );
 
        $validatorEmail = new Zend_Validate_EmailAddress(Zend_Validate_Hostname::ALLOW_DNS, false, $validatorHostname);
        $validatorEmail-&gt;setMessages(
            array(
                Zend_Validate_EmailAddress::INVALID            =&gt; __("'%value%' is not a valid email address"),
                Zend_Validate_EmailAddress::INVALID_HOSTNAME   =&gt; __("'%hostname%' is not a valid hostname for email address '%value%'"),
                Zend_Validate_EmailAddress::INVALID_MX_RECORD  =&gt; __("'%hostname%' does not appear to have a valid MX record for the email address '%value%'"),
                Zend_Validate_EmailAddress::DOT_ATOM           =&gt; __("'%localPart%' not matched against dot-atom format"),
                Zend_Validate_EmailAddress::QUOTED_STRING      =&gt; __("'%localPart%' not matched against quoted-string format"),
                Zend_Validate_EmailAddress::INVALID_LOCAL_PART =&gt; __("'%localPart%' is not a valid local part for email address '%value%'")
            )
        );
        $email-&gt;addValidator($validatorNotEmpty, true)-&gt;setRequired(true)-&gt;setLabel('Email Address')
        -&gt;addFilter($filterTrim)
        -&gt;addValidator($validatorEmail);
        $form-&gt;addElement($email);
 
        $password = new Zend_Form_Element_Password('password');
        $password-&gt;addValidator($validatorNotEmpty, true)-&gt;setRequired(true)-&gt;setLabel('Password')
        -&gt;addValidator(new Zend_Validate_StringLength(3));
        $form-&gt;addElement($password);
 
        $password2 = new Zend_Form_Element_Password('password2');
        $validatorPassword = new My_Validate_PasswordConfirmation('password');
        $validatorPassword-&gt;setMessage(__('Passwords do not match'));
        $password2-&gt;setLabel('Confirm Password')-&gt;addValidator($validatorPassword);
        $form-&gt;addElement($password2);
 
        $gender = new Zend_Form_Element_Select('gender');
        $gender-&gt;setLabel('Gender')
        -&gt;addMultiOption('',' ')-&gt;addMultiOption('male',__('male'))-&gt;addMultiOption('female',__('female'));
        $form-&gt;addElement($gender);
 
        $date = new My_Form_Element_DateSelects('birthdate');
        $validatorDate = new Zend_Validate_Date();
        $validatorDate-&gt;setMessages(
            array(
                Zend_Validate_Date::NOT_YYYY_MM_DD =&gt; __("'%value%' is not of the format YYYY-MM-DD"),
                Zend_Validate_Date::INVALID        =&gt; __("'%value%' does not appear to be a valid date"),
                Zend_Validate_Date::FALSEFORMAT    =&gt; __("'%value%' does not fit given date format")
            )
        );
        $date-&gt;setLabel('Birthdate')-&gt;addValidator($validatorDate);
        $date-&gt;setShowEmptyValues(true)-&gt;setStartEndYear(1900, date("Y")-7)-&gt;setReverseYears(true);
 
        $form-&gt;addElement($date);
 
        $realName = new Zend_Form_Element_Text('realname');
        $realName-&gt;setLabel('Real Name')-&gt;addFilter($filterTrim);
        $form-&gt;addElement($realName);
 
        $validatorNotEmptyAgreement = new Zend_Validate_NotEmpty();
        $validatorNotEmptyAgreement-&gt;setMessage(__('You have to accept our terms and conditions before you register'));
        $agreement = new Zend_Form_Element_Checkbox('agreement');
        $agreement-&gt;setLabel('I agree to terms and conditions')
            -&gt;addValidator($validatorNotEmptyAgreement, true)-&gt;setRequired(true);
        $form-&gt;addElement($agreement);
 
        if (!isset($this-&gt;session-&gt;passedRegisterCaptcha) || !$this-&gt;session-&gt;passedRegisterCaptcha)
        {
            //if we have set captcha in the session for this request - use it, else generate new one
            if (isset($this-&gt;session-&gt;registerCaptcha))
            {
                $captchaCode = $this-&gt;session-&gt;registerCaptcha;
            }
            else
            {
                $md5Hash = md5($_SERVER['REQUEST_TIME']);
                $captchaCode = substr($md5Hash, rand(0, 25), 5);
                $this-&gt;session-&gt;registerCaptcha = $captchaCode ;
            }
 
            $captcha = new Zend_Form_Element_Text('captcha');
            $captcha-&gt;setLabel('Enter the text')
                -&gt;addValidator(new Zend_Validate_Identical($captchaCode))
                -&gt;addValidator($validatorNotEmpty, true)-&gt;setRequired(true);
            $captchaDecorator = new My_Form_Decorator_Captcha();
            $captchaDecorator-&gt;setOption('namespace', 'User')-&gt;setOption('captchaId', 'registerCaptcha');
            $captchaDecorator-&gt;setTag('div');
            $captcha-&gt;addDecorator($captchaDecorator);
            $form-&gt;addElement($captcha);
        }
 
        $submit = new Zend_Form_Element_Submit('register');
        $submit-&gt;setLabel('Submit');
        $form-&gt;addElement($submit);
 
        return $form;
    }
}

and the whole bootstrap.php:

&lt; ?php
 
// For our dev environment we will report all errors to the screen
error_reporting(E_ALL | E_STRICT);
ini_set('display_startup_errors', 1);
ini_set('display_errors', 1);
 
// Set our timezone
date_default_timezone_set('Europe/Sofia');   
 
// Add /library and /application directory to our include path
$siteRootDir = dirname($_SERVER['DOCUMENT_ROOT']);
 
set_include_path(
    $siteRootDir . '/library' . PATH_SEPARATOR
    . $siteRootDir . '/application' . PATH_SEPARATOR
    . get_include_path()
);
 
// Turn on autoloading, so we do not include each Zend Framework class
require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();
 
// Create registry object and setting it as the static instance in the Zend_Registry class
$registry = new Zend_Registry();
Zend_Registry::setInstance($registry);
 
// Load configuration file and store the data in the registry
$configuration = new Zend_Config_Ini($siteRootDir . '/configuration/config.ini', 'main');
Zend_Registry::set('configuration', $configuration);
 
// Construct the database adapter class, connect to the database and store the db object in the registry
$db = Zend_Db::factory($configuration-&gt;db);
$db-&gt;query("SET NAMES 'utf8'");
Zend_Registry::set('db', $db);
// set this adapter as default for use with Zend_Db_Table
Zend_Db_Table_Abstract::setDefaultAdapter($db);
 
// Now set session save handler to our custom class which saves the data in MySQL database
$sessionManager = new My_Session_Manager();
Zend_Session::setOptions(array(
    'gc_probability' =&gt; 1,
    'gc_divisor' =&gt; 5000
    ));
Zend_Session::setSaveHandler($sessionManager);
 
//we will always use session, so this is good place to create this and save it to the registry
$defSession = new Zend_Session_Namespace('Default', true);
Zend_Registry::set('defSession', $defSession);
 
// Setup translation adapter
// Check if language is set in session - if not - use english as default
$lang = 'en';
if (isset($defSession-&gt;lang))
{
    $lang = $defSession-&gt;lang;
}
$translate = new Zend_Translate('gettext', $siteRootDir . '/languages/'.$lang.'.mo', $lang);
Zend_Registry::set('Zend_Translate', $translate);
Zend_Form::setDefaultTranslator($translate);
 
// Setup the Front Controller, disable the error handler, set our controller directories
$frontController = Zend_Controller_Front::getInstance();
$frontController-&gt;throwExceptions(true);
$frontController-&gt;addModuleDirectory($siteRootDir . '/application/modules');
//we want the front controller to return the response, instead of emitting it automatically
$frontController-&gt;returnResponse(true);
 
// define some routes (URLs)
$router = $frontController-&gt;getRouter();
$captchaRoute = new Zend_Controller_Router_Route('captcha/get/:namespace/:captchaId',
    array('controller'=&gt;'captcha', 'action'=&gt;'get')
    );
$router-&gt;addRoute('captcha', $captchaRoute);
 
/*
 * We want to set the encoding to UTF-8, so we won't rely on the ViewRenderer action helper by default,
 * but will construct view object and deliver it to the ViewRenderer after setting some options.
 */
$view = new Zend_View(array('encoding'=&gt;'UTF-8'));
//$view-&gt;addHelperPath($siteRootDir . '/library/My/View/Helper', 'My_View_Helper');
$view-&gt;addHelperPath('My/View/Helper', 'My_View_Helper_');
$viewRendered = new Zend_Controller_Action_Helper_ViewRenderer($view);
Zend_Controller_Action_HelperBroker::addHelper($viewRendered);
 
// Now we initialize the Zend_Layout object with MVC support
Zend_Layout::startMvc(
    array(
        'layoutPath' =&gt; $siteRootDir . '/application/layouts',
        'layout' =&gt; 'main'
    )
);
 
// run the dispatch, get the response and send it to the client
$response = $frontController-&gt;dispatch();
$response-&gt;sendResponse();
 
function __($string)
{
    return $string;
}

A lot of new stuff we covered today, here is the part4.rar file with the current version of the project.

Share and Enjoy: These icons link to social bookmarking sites where readers can share and discover new web pages.
  • bodytext
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • Fark
  • Furl
  • Live
  • Ma.gnolia
  • Reddit
  • Spurl
  • Technorati

Building Sample Web Site Based On Zend Framework Series
Zend Framework

Comments (37)

Permalink