Category: Zend_Form


I just managed to skip all the rather horrible and complex looking Magento form config by getting Zend_Form working in the controllers instead. (I reserve the right to retract what I said just there about Magento forms, seeing as I haven’t actually tried it that way yet!)

Anyway, as you know, CSRF (cross site request forgery) tokens are pretty important to secure up your forms, and Zend_Form_Element_Hash does a fine job of taking care of this, however the problem I got was the old “session already started” message. What I did was create my own Form_Element_Hash. It’s a copy paste job of the original Zend class, only it uses the Magento session. Here it is:

<?php /**  * Created by PhpStorm.  * User: delboy1978uk  * Date: 03/06/15  * Time: 15:30  */ class Del_Form_Element_Hash extends Zend_Form_Element_Xhtml {     /**      * Use formHidden view helper by default      * @var string      */     public $helper = 'formHidden';     /**      * Actual hash used.      *      * @var mixed      */     protected $_hash;     /**      * Salt for CSRF token      * @var string      */     protected $_salt = '54lt4ndP3pp3r';     /**      * @var Zend_Session_Namespace      */     protected $_session;     /**      * TTL for CSRF token      * @var int      */     protected $_timeout = 300;     /**      * Constructor      *      * Creates session namespace for CSRF token, and adds validator for CSRF      * token.      *      * @param array|string|Zend_Config $spec      * @param null $options      * @throws Zend_Form_Exception      */     public function __construct($spec, $options = null)     {         parent::__construct($spec, $options);         $this->setAllowEmpty(false)
            ->setRequired(true)
            ->initCsrfValidator();
    }

    /**
     * Set session object
     *
     * @param  Mage_Core_Model_Session $session
     * @return Del_Form_Element_Hash
     */
    public function setSession($session)
    {
        $this->_session = $session;
        return $this;
    }

    /**
     * Get session object
     *
     * Instantiate session object if none currently exists
     *
     * @return Mage_Core_Model_Session
     */
    public function getSession()
    {
        if (null === $this->_session)
        {
            $this->_session = Mage::getSingleton('core/session');
        }
        return $this->_session;
    }

    /**
     * Initialize CSRF validator
     *
     * Creates Session namespace, and initializes CSRF token in session.
     * Additionally, adds validator for validating CSRF token.
     *
     * @return Del_Form_Element_Hash
     */
    public function initCsrfValidator()
    {
        $session = $this->getSession();
        $key = $session->getData('csrf');
        if (isset($key))
        {
            $rightHash = $session->getData('csrf');
        }
        else
        {
            $rightHash = null;
        }

        $this->addValidator('Identical', true, array($rightHash));
        return $this;
    }

    /**
     * Salt for CSRF token
     *
     * @param  string $salt
     * @return Del_Form_Element_Hash
     */
    public function setSalt($salt)
    {
        $this->_salt = (string) $salt;
        return $this;
    }

    /**
     * Retrieve salt for CSRF token
     *
     * @return string
     */
    public function getSalt()
    {
        return $this->_salt;
    }

    /**
     * Retrieve CSRF token
     *
     * If no CSRF token currently exists, generates one.
     *
     * @return string
     */
    public function getHash()
    {
        if (null === $this->_hash) {
            $this->_generateHash();
        }
        return $this->_hash;
    }

    /**
     * Get session namespace for CSRF token
     *
     * Generates a session namespace based on salt, element name, and class.
     *
     * @return string
     */
    public function getSessionName()
    {
        return __CLASS__ . '_' . $this->getSalt() . '_' . $this->getName();
    }

    /**
     * Set timeout for CSRF session token
     *
     * @param  int $ttl
     * @return Del_Form_Element_Hash
     */
    public function setTimeout($ttl)
    {
        $this->_timeout = (int) $ttl;
        return $this;
    }

    /**
     * Get CSRF session token timeout
     *
     * @return int
     */
    public function getTimeout()
    {
        return $this->_timeout;
    }

    /**
     * Override getLabel() to always be empty
     *
     * @return null
     */
    public function getLabel()
    {
        return null;
    }

    /**
     * Initialize CSRF token in session
     *
     * @return void
     */
    public function initCsrfToken()
    {
        $session = $this->getSession();
        $session->setData('csrf',$this->getHash());
    }

    /**
     * Render CSRF token in form
     *
     * @param  Zend_View_Interface $view
     * @return string
     */
    public function render(Zend_View_Interface $view = null)
    {
        $this->initCsrfToken();
        return parent::render($view);
    }

    /**
     * Generate CSRF token
     *
     * Generates CSRF token and stores both in {@link $_hash} and element
     * value.
     *
     * @return void
     */
    protected function _generateHash()
    {
        $this->_hash = md5(
            mt_rand(1,1000000)
            .  $this->getSalt()
            .  $this->getName()
            .  mt_rand(1,1000000)
        );
        $this->setValue($this->_hash);
    }

}

As we all know, Magento is built using some of the Zend Framework library. However, that doesn’t mean you can go straight into a controller and start programming like you would ZF. However, preparing your Zend_Form in order to work in Magento is pretty straigtforward.

Magento’s controllers do not have a view! So your form will fail to render! To fix this, we override the __toString() method:

/**
 *  Overridden because Magento doesnt have the zend view
 */
public function __toString()
{
    return  $this->render(new Zend_View());
}

Now your form will work in your magento controller.  If you are new to Magento like I am, you’ll also need to know how to instantiate the form, send the form to the view, and how to display it once we are in the view! In the controller:

public function indexAction()
{
    $this->loadLayout();
 
 // new My_Form_Class(); also works here, but this is how Magento does it, from the xml config.
 $this->form = Mage::getModel('madskull_feedback/form'); 

 if($this->getRequest()->isPost())
 {
   $data = $this->getRequest()->getPost();
   if($this->form->isValid($data))
   {
     // get sanitised data
     $data = $this->form->getValues();
     $this->sendFeedbackEmail($data);
     $this->_redirect('*/*/thanks');
   }
   else
   {
     $this->form->populate($data);
   }
  }

  $this->getLayout()->getBlock('madskull_feedback.form')->setData('form', $this->form);
  $this->renderLayout();
}

On the .phtml file, it’s a piece of cake:

echo  $this->getData('form');

There you go! Full Zend_Form goodness! Have fun!

Getting this error? Set your form to  enctype=”multipart/form-data”. Job done.

Sometimes, you’ll want the same form in more than one place in your application, however occasionally, that means your forms look terrible! However you can specify a different view script to use (I put mine in a forms subdirectory):

$this->addDecorators(array(
    array('ViewScript', array('viewScript' => 'view_folder/different-looking-form.phtml'))
));

And the bit that confdused me for a while, accessing the form in that view is strangle called $this->element:

<?= $this->element; ?>

Of course, as you may know, you don’t have to render the form like that, which is kind of the point of this:

<?php
$form = $this->element;
$supply = $form->getSupply();
$fueltype = $form->getFueltype();

?>

<form id = "<?=$form->getAttrib('id') ?>"
    name = "<?=$form->getName() ?>"
    method = "<?=$form->getMethod(); ?>"
    enctype = "<?=$form->getEnctype(); ?>"
    >
    <dl class="zend_form">

        <dt><label>Supply / Meter</label></dt>
        <dd><?=$supply->getMeterNumber() ?></dd>

        <dt><label>Fuel Type</label></dt>
        <dd><?=$fueltype->getFueltype() ?></dd>

        <dt><label>Description</label></dt>
        <dd><?=$supply->getDescription() ?></dd>

    <?php
    foreach ($form->getElements() as $element)
    {
        echo $element;
    }
    ?>
    </dl>
</form>

Sorted! No need to encase your form in more divs and mess around with CSS! 🙂

Here’s a poser for you. Open up a controller and its view and try this:

Controller:

$this->view->utf8 = 'Si Señor!';
 $this->view->html = 'Le Cr&eacute;pe';
$btn1 = new Zend_Form_Element_Button('btn1');
 $btn1->setLabel($this->view->utf8);
$btn2 = new Zend_Form_Element_Button('btn2');
 $btn2->setLabel($this->view->html);
$this->view->btn1 = $btn1;
 $this->view->btn2 = $btn2;

View:

<?php echo 'utf-8: '.$this->utf8.'<br />'
 'html: '.$this->html.'<br />'
 'btn1: '.$this->btn1.'<br />'
 'btn2: '.$this->btn2; ?>

Output:

utf8: Si Se�or!
html: Le Crépe
btn1: ()
btn2: (Le Cr&eacute;pe)

the (buttons) render, but:
the first one is blank (kind of expected as the utf8 variable doesn’t display either)
the 2nd one displays a label, and the html variable worked as we knew it would, however the &acute; entity is displaying in the label.

what does this mean?
does it mean that! the Zend_Form_Element_Button has doubly encoded it?
it must have sent Cr&amp;eacute;pe to the browser!

This is busting my chops! Send it unencoded and it fails to render the label, and send it encoded and it gets encoded twice 😐

Screen Shot 2013-02-06 at 22.02.22

UPDATE: You’ve checked character encoding in your code, mac textedit, the db, the templates, the form decorators,  the form filters, the forums, irc, but guess what? you forgot the IDE.

If you ever need to get your Zend_Form_Element_File to be attached to your Zend_Mail, here’s how it’s done!

$mail = new Zend_Mail();
$at = new Zend_Mime_Part(file_get_contents($form->upload->getFileName()));
            $at->type           = $form->upload->getMimeType();
            $at->disposition = Zend_Mime::DISPOSITION_ATTACHMENT;
            $at->encoding    = Zend_Mime::ENCODING_BASE64;
            $at->filename    = basename($form->upload->getFileName());
            $mail->addAttachment($at);

I’m loving discovering all these other parts of Zend Framework that I haven’t looked at yet. Today it was setting the locale of the application, and dishing out content specific to that region. Okay, lets get cracking.

In the root of the application, create /languages

And in there, create files like messages.es.php & messages.fr.php and messages.en_US.php. For English, as well as having en_GB and en_US, we should also have just en as a fallback. In these files, you put your translations in a big ass array:

<?php
return array(
  'nav-home'             => 'Inicio',
  'nav-services'         => 'Servicios',
  'nav-catalogue'         => 'Catálogo',
  'nav-contact'         => 'Contacto',
  'welcome'         => 'Bienvenido',
  'form-name'         => 'Nombre:',
  'form-email-address' => 'email:',
  'form-message'       => 'Mensaje:',
  'form-verification'  => 'verificación:',
  'form-send-message'  => 'Enviar',
  'form-title'         => 'Contacto',
);

Then you initialise the locale stuff in your bootstrap. If it can’t detect the locale from your browser then it will fall back to your default locale of choice :

protected function _initLocale()
{
    try 
    {
      $locale = new Zend_Locale('browser');
    } 
    catch (Zend_Locale_Exception $e) 
    {
      $locale = new Zend_Locale('en_GB');
    }
    $registry = Zend_Registry::getInstance();
    $registry->set('Zend_Locale', $locale);
}

To test the different locales, we can edit the bootstrap replacing the word browser for fr or es or en_US etc. But before we do that, a couple more things. First, initialise the translate stuff in the bootstrap too.

protected function _initTranslate()
{
    $translate = new Zend_Translate('array',
                  APPLICATION_PATH . '/../languages/',
                  null,
                  array('scan' => Zend_Translate::LOCALE_FILENAME,
                        'disableNotices' => 1));
    $registry = Zend_Registry::getInstance();
    $registry->set('Zend_Translate', $translate);
}

In the controller actions where you want anything to happen you get it from the Zend_Registry:

    $registry = Zend_Registry::getInstance();
    $this->view->locale = $registry->get('Zend_Locale');

Finally, you need to edit the nav links and the form field labels like so:

    <a href="blah"><?php echo $this->translate('nav-catalogue');?></a>

And the form:

    $email->setLabel('form-email-address');

Zend_Form is groovy enough to check for translations automatically, so that’s it really! A lot of fuss taken care of quite simply!

Another point is that now we have a locale, we can get locale specific versions of things like dates, currency, and number formatting. As an example:

echo $this->escape(
    Zend_Locale_Format::toNumber
    (
        $this->product->price,
        array
        ( 
            'locale' => $this->locale,
            'precision' => 2
        ) 
    )
);

And for a date:

    <?php echo $this->escape($this->productdate->get(Zend_Date::DATE_FULL)); ?>

As you can see this is easy and fun! So it would be awesome to have the ability to switch between them, yes? So lets make a locale controller:

class LocaleController extends Zend_Controller_Action

{

public function setAction()
 {
    // if supported locale, add to session
    if (Zend_Validate::is($this->getRequest()->getParam('locale'), 'InArray',
        array('haystack' => array('en_US', 'en_GB', 'de_DE', 'fr_FR')))) 
    {
      $session = new Zend_Session_Namespace('ttb.l10n');
      $session->locale = $this->getRequest()->getParam('locale');
    }
    // redirect to requesting URL
    $url = $this->getRequest()->getServer('HTTP_REFERER');
    $this->_redirect($url);
 } 
}

edit the _initLocale method in the bootstrap

  protected function _initLocale()
  {
     $session = new Zend_Session_Namespace('ttb.l10n');
     if ($session->locale) 
     {
        $locale = new Zend_Locale($session->locale);
     }
     if ($locale === null) 
     {
        try 
        {
            $locale = new Zend_Locale('browser');
        } 
        catch (Zend_Locale_Exception $e) 
        {
          $locale = new Zend_Locale('en_GB');
        }
     }
     $registry = Zend_Registry::getInstance();
     $registry->set('Zend_Locale', $locale);
  }

And then make some links in your layout to swap locales:

<a href="/locale/set/fr_FR"><img src="/img/locale/france-flag.png" />

Awesome! switching locales is a dawdle! Now to get on with my Spanish lessons some more! Adios!

Zend_Form is awesome. Zend_Validate and Zend_Filter are awesome. The Error decorators are also awesome, but sometimes a little too awesome.

I mean, when a user leaves an email field blank, and I had a NotEmpty and EmailAddress validator, I get two error messages!

  • Value is required and can't be empty
  • '' is no valid email address in the basic format local-part@hostname

Really I’d rather it just said:

  • Please enter a valid email address

Because some users, lets face it, are idiots! Nice idiots, but idiots all the same lol!

To stop this sort of thing happening, you can say in your form element:

$email = new Zend_Form_Element_Text('email');
 $email->setRequired(true)
 ->addFilter('StripTags')
 ->addFilter('StringTrim')
 ->addValidator('NotEmpty',true)
 ->addValidator('EmailAddress')
 ->setLabel('Email')
 ->setErrorMessages(array('Please enter a real email address'));

The key thing here is the true value in the NotEmpty validator. It breaks the chain of validators upon failure, stopping subsequent validators from checking and adding its error message too.
Without the true, but with the custom error message, you would get:

  • Please enter a valid email address
  • Please enter a valid email address

This should help save headaches!