29 July 2011

Creating a custom Zend form validator

There is quite a bit of information around with a lot of different methods for creating a custom Zend validator across multiple framework versions, and when I first came to do this it took some time to put together all the scattered pieces of information. There is a lot of detail about how to actually write the validator code, but information on actually implementing the validator including where the files should go was harder to find. I thought I would bring all the details together in this short tutorial.

In a recent project I had the need to verify a users current password on a form used to update that users login credentials - this is the example custom validator code I will be showing here.

Under the application directory create a subdirectory called validate, and within this a new file called MatchPassword.php.

Inside this file put the following code:
<?php

class Application_Validate_MatchPassword extends Zend_Validate_Abstract
{
    const NOT_MATCH = 'notMatch';

    protected $_messageTemplates = array(
        self::NOT_MATCH => "Incorrect password entered"
    );

    public function isValid($value)
    {
        $authadapter = new Zend_Auth_Adapter_DbTable(Zend_Db_Table::getDefaultAdapter());

        $authadapter->setTableName('users')
            ->setIdentityColumn('username')
            ->setCredentialColumn('password')
            ->setCredentialTreatment('MD5(CONCAT(salt,?))');

        $authadapter->setIdentity($_POST['username']);
        $authadapter->setCredential($value);

        $auth = Zend_Auth::getInstance();
        $result = $auth->authenticate($authadapter);
        if ($result->isValid())
        {
            return true;
        }
        
        $this->_error(self::NOT_MATCH);
        return false;
    }
}
First we declare the class name and it must extend Zend_Validate_Abstract. The class name can be anything, but Application_Validate_MatchPassword is logical because of the file location and the function of the custom validator.

Next we declare the variable to contain the failed validation error message and assign the desired text to it within the $_messageTemplates array.

Custom validators only support two methods, isValid() and getMessages(), for this validator we will only be using isValid(), but if you are interested to read more about the two methods with example custom validator code, then refer to the Zend manual here.

To get the validator to run, you just need to call the isValid() method on the form object from within whatever controller you use to instantiate the form, for instance:
$form = new Application_Form_SomeForm();
$request = $this->getRequest();
if ($form->isValid($request->getPost()))
{
    // the form passed validation so take some action here
}
You don't need any logic for if the form fails validation as the error messages will automatically be displayed next to the form element the validator has been added to.

So within the isValid() method above you can see we make use of Zend_Auth to allow us to verify the password given against the actual current user password. As we are validating against a password held in a database, we also use the Zend_Auth_Adapter_DbTable class.

Firstly we instantiate Zend_Auth_Adapter_DbTable passing it the default database connection details pulled from our projects application.ini file:
$authadapter = new Zend_Auth_Adapter_DbTable(Zend_Db_Table::getDefaultAdapter());
Next we tell it which database table we want to validate against (in this case users), followed by which column contains the username, and then password data:
$authadapter->setTableName('users')
    ->setIdentityColumn('username')
    ->setCredentialColumn('password')
Finally we give it some details about how the password data is stored. If this line is emitted, then it would mean you would have to be storing passwords in plain text in your database (a bad idea!). In this case when the password is stored, a random salt value is created which is concatenated with the plain text password, and an MD5 hash is then created out of the result. So we need to tell Zend_Auth_Adapter_DbTable that this is what has happened so that it can perform the same operation when the custom validator runs.

The ? represents the password entered into the form element the custom validator is added to, and salt represents the contents of the salt column in the database table we are using for the record we are validating against. The contents of this column is the random salt value which is concatenated with the plain text password, and was also saved when the record was created:
    ->setCredentialTreatment('MD5(CONCAT(salt,?))');
Finally we tell Zend_Auth_Adapter_DbTable what data we want to authenticate with. The form this custom validator is added to also has an element username, and in the table users, the column username is primary key, so we pass the the post data username to the setIdentity() method. The variable $value (as defined in the isValid() function declaration) is the contents of the form field to which this custom validator is applied when the form gets submitted. As we have already told Zend_Auth_Adapter_DbTable that we want to authenticate against the column password when we call the setCredentialColumn() method, we need to tell it what data we need to validate against the contents of this column. To do this we pass the setCredential() method the $value variable.
$authadapter->setIdentity($_POST['username']);
$authadapter->setCredential($value);
Now that Zend_Auth_Adapter_DbTable has all the details it needs, we can instantiate an instance of Zend_Auth:
$auth = Zend_Auth::getInstance();
And then store the results of an authentication against the populated Zend_Auth_Adapter_DbTable instance:
$result = $auth->authenticate($authadapter);
If the password authentication succeeds, then we return true to indicate validation has passed:
if ($result->isValid())
{
    return true;
}
Otherwise we set the element error message to NOT_MATCH as defined at the top of the class, and return false to indicate validation has not passed (in which case that error message will be displayed next to the relevant form element):
$this->_error(self::NOT_MATCH);
return false;
Now we have the custom validator written and in the right place, we just need to add it to our form. This is actually very easy, we need to declare the presence of an additional path to search for form validators under at the top of the form class and within the init() function:
<?php

class Application_Form_SomeForm extends Zend_Form
{
    public function init()
    {
        $this->addElementPrefixPath('Application_Validate', APPLICATION_PATH . '/validate/', 'validate');
        // other form code
    }
}
And then within the relevant form element, add in the validator:
class Application_Form_SomeForm extends Zend_Form
{
    public function init()
    {
        $this->addElementPrefixPath('Application_Validate', APPLICATION_PATH . '/validate/', 'validate');

        // other form code

        $this->addElement('password', 'currentpassword', array(
            'label' => 'Enter current user password:',
            'required' => true,
            'validators' => array(
                'MatchPassword'
            )
        ));

        // other form code

    }
}
Now when you call the isValid() method on the form object instantiated from your controller, the custom validator will run against the element that declares it. Remember you also need a username element in your form for it to work.

IMPORTANT:

Because Zend_Auth is a singleton there can only be one instance of the class. So if you are using it anywhere else in your site (to for instance login users), then this method will interfere with the stored user credentials in the object. In this case, you should use the following code instead for MatchPassword.php:
<?php

class Application_Validate_MatchPassword extends Zend_Validate_Abstract
{
    const NOT_MATCH = 'notMatch';

    protected $_messageTemplates = array(
        self::NOT_MATCH => "Incorrect password entered"
    );

    public function isValid($value)
    {
        $mapper = new Application_Model_UsersMapper();
        $user = new Application_Model_Users();
        $mapper->find($_POST['username'], $user);

        if ($user->getPassword() === md5($user->getSalt() . $value))
        {
            return true;
        }
        
        $this->_error(self::NOT_MATCH);
        return false;
    }
}
This performs exactly the same operation as before, but won't interfere with your Zend_Auth credentials. You may have noticed that this method refers to two model classes, Application_Model_UsersMapper and Application_Model_Users. You will need these for the validation class to work correctly so create application/models/Users.php and application/models/UsersMapper.php (you can of course call these something else). In Users.php put the following code:
<?php

class Application_Model_Users
{
    protected $_username;
    protected $_password;
    protected $_email;
    protected $_active;
    protected $_lastaccess;
    protected $_salt;
 
    public function __construct(array $options = null)
    {
        if (is_array($options))
        {
            $this->setOptions($options);
        }
    }
 
    public function __set($name, $value)
    {
        $method = 'set' . $name;
        if (('mapper' == $name) || !method_exists($this, $method))
        {
            throw new Exception('Invalid users property');
        }
        $this->$method($value);
    }
 
    public function __get($name)
    {
        $method = 'get' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid users property');
        }
        return $this->$method();
    }
 
    public function setOptions(array $options)
    {
        $methods = get_class_methods($this);
        foreach ($options as $key => $value)
        {
            $method = 'set' . ucfirst($key);
            if (in_array($method, $methods))
            {
                $this->$method($value);
            }
        }
        return $this;
    }
 
    public function setUsername($text)
    {
        $this->_username = (string) $text;
        return $this;
    }
 
    public function getUsername()
    {
        return $this->_username;
    }
 
    public function setPassword($text)
    {
        $this->_password = (string) $text;
        return $this;
    }
 
    public function getPassword()
    {
        return $this->_password;
    }
 
    public function setEmail($email)
    {
        $this->_email = (string) $email;
        return $this;
    }
 
    public function getEmail()
    {
        return $this->_email;
    }
 
    public function setActive($active)
    {
        $this->_active = (int) $active;
        return $this;
    }
 
    public function getActive()
    {
        return $this->_active;
    }
 
    public function setLastaccess($lastaccess)
    {
        $this->_lastaccess = (string) $lastaccess;
        return $this;
    }
 
    public function getLastaccess()
    {
        return $this->_lastaccess;
    }
 
    public function setSalt($salt)
    {
        $this->_salt = (string) $salt;
        return $this;
    }
 
    public function getSalt()
    {
        return $this->_salt;
    }
}
I'm not going to go into much detail about this class, but when instantiated it sets up an object you can populate with information from the database via the mapper you are about to create. You should create a protected variable relating to each columnn in the database table that holds user credentials. In my case, I have the columns, username, password, email, active, lastaccess and salt with username being primary key. For each of the variables you define you also need to create a get and set method as above. Finally, if you pass the class an array when you instantiate it, the object will be populated with those values, so you might pass something like the following:
$data = array('username' => 'yourusername',
            'password' => 'yourpassword',
            'email' => 'youremail',
            'active' => 'youractive')
So, now we need to mapper class to retrieve the database information, inside UsersMapper.php put the following code:
<?php

class Application_Model_UsersMapper
{
    protected $_dbTable;
 
    public function getDbTable()
    {
        if (null === $this->_dbTable) {
            $this->setDbTable('Application_Model_DbTable_Users');
        }
        return $this->_dbTable;
    }
 
    public function find($username, Application_Model_Users $user)
    {
        $result = $this->getDbTable()->find($username);
        if (0 == count($result)) {
            return;
        }
        $row = $result->current();
        $user->setUsername($row->username)
            ->setPassword($row->password)
            ->setSalt($row->salt)
            ->setEmail($row->email)
            ->setActive($row->active)
            ->setLastaccess($row->lastaccess);

    }
}
This class retrieves an entry from the database using the find() method of Zend_Db_Table_Abstract and populates the Application_Model_Users object you pass it with any returned data. There is one more class you need for this to work, being Application_Model_DbTable_Users thats referred to in the mapper. Create the file application/models/DbTable/Users.php and put the following content inside:
<?php

class Application_Model_DbTable_Users extends Zend_Db_Table_Abstract
{
    protected $_name = 'users';
    protected $_primary = 'username';
}
This class is just used to define the name of the database table to use, and the column defined as primary key.

So overall a bit more complex than using Zend_Auth, but does allow you to create the validator without interfering with stored user credentials in the object.

12 July 2011

Troubleshooting Magento cron

Getting cron to work correctly in Magento seems to be the source of headaches for a lot of people, if that's you then hopefully following this through will allow you to get it working.

The first thing to do is to make sure the server is actually executing a cron job. From a terminal log on to your server as the user you want to setup the cron job under:
ssh username@server
Find out what jobs are currently scheduled in crontab:
crontab -l
If there is no output, you have no cron jobs scheduled, otherwise you may see something like the following:
*/15 * * * * /command/to/run.sh
If any scheduled cron jobs are set to run either cron.php or cron.sh in your Magento install directory, then crontab should already be configured properly. If you had no output, you need to add a job. If you are not familiar with cron syntax, you might want to have a look here, otherwise you can just go ahead and run:
crontab -e
This will launch a text editor so add your job onto a new line but don't save and quit just yet. At the top of the file add the following mailto line (if it's not already there) so that cron will email any output from the commands to the specified email address:
MAILTO=some@emailaddress.com
If everything goes to plan, you may not get any output from crontab when the job runs, so to make sure we do, edit the cron command to point at some location that doesn't exist, and change it to run every 5 minutes, so you will end up with something like this:
MAILTO=some@emailaddress.com
*/5 * * * * /some/fake/location.sh
Save the file and within 5 minutes you should receive an email from cron showing that the job has run. This proves the server is running the cron job, so if you don't get an email crontab may not be correctly configured and this should be looked at.

Generally I see people recommend kicking off Magento's cron with one of the following:
*/5 2 * * * /usr/bin/php -f /path/to/magento/install/cron.php
*/5 2 * * * /path/to/magento/install/cron.sh
Both of these can potentially be problematic however - cron.sh may need editing depending on your servers default shell, and using PHP CLI to call cron.php can fail to correctly schedule jobs if the user executing the cron job does not have sufficient privileges. Another method and the one that I prefer is to use wget:
*/5 2 * * * wget -q -O /dev/null http://server/cron.php
Not only does this bypass potential shell problems with cron.sh, it also ensures the script is executed as the correct user.

Add the wget cron job (but tailored to your needs) into your crontab file which should still be open, save and close.

Assuming the cron job is running correctly and you received the email to prove that then you can start looking at Magento itself. I'm not going to go into how you configure a module to use cron, or where to find cron configurations in an already existing module as if you have come across this page you have probably already got that information. Instead I will cover how to troubleshoot Magento cron jobs that do not appear to be running.

The key to finding out whether or not Magento has scheduled cron jobs to run is the cron_schedule table. Any jobs which have been queued up by cron will be shown in here with time and current status details.

From your terminal window (hopefully you still have it open) log in to MySQL and switch to the Magento database:
mysql -u database_user -p
mysql> use your_database
If you are unsure of your database credentials, they will be stored in app/etc/local.xml under your Magento install.

You can then see what content is in the cron_schedule table:
mysql> SELECT * FROM cron_schedule;
In the table that's printed out, you will a job_code column which should be fairly self explanatory as to what the job relates to, and a few columns detailing when the job was added and run.

You will also see different text under the status column such as pending, running and success, the table will also be purged intermittently. If everything is working as it should be then you hopefully will only see a status of success, apart from if a job is actually running at the time in which case the status may be different but should change to success the next time cron is run. However if the status for a job is pending and it is not a recent one (perhaps a couple of days old or more), this could indicate a problem with that cron job completing as the status here will only be set to success when the cron job runs through without issue. It's also worth noting that a problematic cron job can stop other jobs from running.

Hopefully this has helped you to get your cron jobs running correctly, or if not at least track down where the problem lies.

If you have tracked down a problematic cron job, you can clear the job from the cron schedule table by running:
mysql> DELETE FROM cron_schedule WHERE status='pending';
Then, for further testing you might want to temporarily change the cron scheduling for the module in question to just ahead of the current time and run your crontab job from the command line to further investigate the problem.
wget -q -O /dev/null http://server/cron.php
Obviously remember to change back the cron scheduling for the module once finished.