LogoPhly, boy, phly
the weblog and site of Matthew Weier O'Phinney

Friday, February 8. 2008

Zend_Form Advanced Features

I've been working on for the past few weeks, and it's nearing release readiness. There are a number of features that Cal didn't cover in his DevZone coverage (in part because some of them weren't yet complete) that I'd like to showcase, including:

  • Internationalization
  • Element grouping for display and logistical purposes
  • Array support

This post will serve primarily as a high-level overview of some of these features; if you're looking for more in-depth coverage, please review the unit tests. :-)

Internationalization

When using form components in many libraries, internationalization (i18n) is often tricky to accomplish. There are many potential translation targets: labels, submit and reset buttons, and error messages all potentially need to be treated.

Zend_Form allows setting a translation object at both the element and form level, and also allows setting a default translation object for all forms and elements. I personally feel this latter is the most flexible; in most projects, you'll have a single set of translation files, so why not simply re-use the same object throughout?


// Create your translation object
$translate = new Zend_Translate(...);

// Set it as the default object for all forms and elements:
Zend_Form::setDefaultTranslator($translate);
 

What do you get for this?

  • Legends. If a fieldset legend has a translation available, it will be translated.
  • Labels. If the label you provide has a translation available, it will be used.
  • Buttons. Submit, reset, and regular form button values will be translated.
  • Error messages. Validation error messages will be translated, with any value substitutions as provided by Zend_Validate.

In other words, translation in Zend_Form is pretty well integrated.

Element Grouping

In Zend_Form, we distinguish between two types of element grouping: grouping for display purposes (DisplayGroups) and grouping for logistical purposes (Sub Forms)

With DisplayGroups, you're basically saying you want to visually or semantically group elements together on the page. Usually (and by default) this is done with fieldsets. DisplayGroups provide a simple mechanism for doing this. The elements remain children of the parent form object, but are rendered within the display group.

Other times, you want to group the elements logically. For instance, you might want to group a billing address separately from a shipping address. This grouping may be simple namespacing under array keys (I'll cover this more later), shared filters or decorators, or, in advanced use cases, separate pages of a multi-page form.

Zend_Form's answer to these situations are "Sub Forms". They are actually a subclass of Zend_Form, and the only real difference is the class and the default decorators used (by default, they render in a fieldset). Since they share the same functionality as a regular form, this means they can validate their elements, render themselves, etc. However, Zend_Form itself cannot iterate over or render a sub forms elements; only the sub form can do that.

One potentially powerful use case for sub forms is for multi-page forms. You could easily create a form consisting of several sub forms, and display a single sub form per page, persisting data in the session between form submissions; only when all pages have received their data would the parent form be valid, allowing you to finally pass the data to the model.

Form grouping at the display and logical level both are powerful tools, and this functionality is trivial with Zend_Form.

Array Support

Many developers like to namespace their form elements under nested arrays. This allows for groupings of related data, as well as having several groups with similar data on the same page. As an example, imagine a form showing both a shipping and a billing address:


<form action="/foo/bar" method="post">
    <fieldset>
        <legend>Shipping Address</legend>
        <dl>
            <dt>Address:</dt>
            <dd><input name="shipping[address]" type="text" value="" /></dd>
         
            <dt>City:</dt>
            <dd><input name="shipping[city]" type="text" value="" /></dd>
         
            <dt>Postal:</dt>
            <dd><input name="shipping[postal]" type="text" value="" /></dd>
        </dl>
    </fieldset>

    <fieldset>
        <legend>Billing Address</legend>
        <dl>
            <dt>Address:</dt>
            <dd><input name="billing[address]" type="text" value="" /></dd>
         
            <dt>City:</dt>
            <dd><input name="billing[city]" type="text" value="" /></dd>
         
            <dt>Postal:</dt>
            <dd><input name="billing[postal]" type="text" value="" /></dd>
        </dl>
    </fieldset>
</form>

PHP will receive two arrays from the submitted form, 'shipping' and 'billing'.

Zend_Form now allows this (as of today). To keep all existing features, and to allow elements and sub forms to stay de-coupled from their parent forms, you need to do a little configuration:


$shipping = new Zend_Form_SubForm('shipping');

// This next line tells the elements, validators, and decorators that they are
// part of an array; by default, the sub form name is used:
$shipping->setIsArray(true);

// This can also be accomplished by explicitly setting the array name:
$shipping->setElementsBelongTo('shipping');
 

The fun part is that this can be arbitrarily deep, by specifying the array key as it would appear in the form. So, as an example, if we wanted the entire form returned in the 'demographics' array, and 'shipping' and 'billing' were keys in that array, we could do the following:


// Set base key for entire form:
$form->setElementsBelongTo('demographics');

// Set subkey for shipping sub form:
$shipping->setElementsBelongTo('demographics[shipping]');

// Set subkey for billing sub form:
$billing->setElementsBelongTo('demographics[billing]');
 

When you set or retrieve values, or validate, these array keys are honored. What's more, since they are configurable, you can leave them out of your generic forms, and only set them in your concrete instances -- allowing re-use and re-purposing.

Conclusion

This post is mainly to serve as high-level overview of some of the more advanced features of Zend_Form. In the coming weeks, more thorough documentation will be present in the Zend Framework repository, allowing developers to understand the functionality in more depth. Hopefully I've whetted some people's appetites out there, and we'll get more of you experimenting with the current code base.

Update: fixed array notation HTML example to show separate billing and shipping addresses.

Posted by Matthew Weier O'Phinney in PHP at 22:56 | Comments (16) | Trackback (1)
Defined tags for this entry: mvc, php, zend framework
Related entries by tags:
Form Decorators Tutorial posted
Zend Framework Q&A Session
View Helpers Tutorial on DevZone
ZF Plugins Tutorial on DevZone
Action Helpers in ZF

Trackbacks
Trackback specific URI for this entry

Matthew O'Phinney's Blog: Zend_Form Advanced Features
In a new post to his blog today, Matthew O'Phinney dives ...
Weblog: PHPDeveloper.org
Tracked: Feb 11, 10:21

Comments
Display comments as (Linear | Threaded)

Great work. I am looking forward to getting rid of QuickForm when 1.5 is released. Zend_Form is turning out to be an excellent solution that is capable of handling a very diverse range of Form related problems.

I think you made a mistake in the HTML code for the "Array Access" section. Shouldn't one of the fieldsets be Billing and the other Shipping?
#1 Daniel Skinner (Link) on 2008-02-09 09:13 (Reply)
Glad you're liking what you see. I've traditionally stayed away from most form libraries as I've found them not configurable enough.

Thanks for pointing out the cut-and-paste error; I've now corrected it.
#1.1 Matthew Weier O'Phinney (Link) on 2008-02-09 13:51 (Reply)
Yes I agree. I am particularly looking forward to being able to set up an entire form with a Zend_Config object.
#1.2 Daniel Skinner (Link) on 2008-02-09 17:59 (Reply)
Thank you for this information. I found the sections about i18n and arrays particularly useful, as I was trying to hack this together myself.
#2 Robin Skoglund on 2008-02-11 06:03 (Reply)
Actually, I've got a follow-up question to the i18n part...

I'm wondering if I'm doing things wrong, or if the result I get is by design. In your post, it says:

* Error messages. Validation error messages will be translated, with any value substitutions as provided by Zend_Validate.

I have subclassed Zend_Form, and in the constructor I use $this->setTranslator($translate), then go on to make elements and add them to the form (without setting translator object for any of them). This all works fine, but on errors, value substitution dos not occur. The reason for this, as far as I've been able to backtrack, is that the translation itself occurs in the Errors decorator, at which point values have already been substituted by Zend_Validate_Abstract.

I've tried setting translator for elements too, but this results in no translating (also, the render method in Zend_Form sets translator to null in all elements if no translator is set in the form).

Is there any way to make the translation happen before value substitution?

I'm using Zend Framework 1.5.0 Preview Release.
#3 Robin Skoglund on 2008-02-11 13:58 (Reply)
Most of the i18n improvements have been made since the last Preview Release. If you give a nightly snapshot or current trunk a try, I think your results will be much better. :-)
#3.1 Matthew Weier O'Phinney (Link) on 2008-02-11 15:38 (Reply)
I tried the latest snapshot, but that didn't work either. It's almost implemented, but not quite. The isValid() method in Zend_Form_Element calls setTranslator() on all validators if the method exists, but setTranslator() is not a method in Zend_Validate_Abstract.

My fix:
- Added $_translator as a member in Zend_Validate_Abstract

- Added accessors setTranslator(Zend_Translate $translator = null) and getTranslator() to Zend_Validate_Abstract

- Modified the method _createMessage() in Zend_Validate_Abstract:
Changed the following line:
$message = $this->_messageTemplates[$messageKey];

to:
if (null !== $this->_translator) {
$message = $this->_translator->getAdapter()->translate($messageKey);
} else {
$message = $this->_messageTemplates[$messageKey];
}

- Removed translation part from the method render() in Zend_Form_Decorator_Errors


Now it works, but this hack is sub-optimal, so I'm looking forward to the stable release, and hope it will be fixed till then.
#3.1.1 Robin Skoglund on 2008-02-11 16:27 (Reply)
There's something wrong with your install, as the addition of setTranslator() to Zend_Validate_Abstract was necessary for me to even attempt these i18n features; please try checking out directly from subversion.

Also, if you continue having issues, please report to the fw-mvc list instead of my blog; there will be more people who can help you, including helping verify issues. :-)
#3.1.1.1 Matthew Weier O'Phinney (Link) on 2008-02-11 17:02 (Reply)
Okay, I checked out the latest trunk from subversion, and this helped a lot. Now, everything is translated in form elements and validators, but the error messages are still rendered without value substitution.

I followed your advice and posted the issue to the fw-mvc list, so I guess the discussion continues there from now on (this comment is for notifying other potentional googlers that the discussion has moved :-)).

http://www.nabble.com/I18n-problem-with-Zend_Form_Decorator_Errors-td15422825s16154.html
#3.1.1.1.1 Robin Skoglund on 2008-02-11 18:12 (Reply)
I have a question about using a radio button with a config object, I'm setting it up in my config like so:
elements.call_me.type = "radio"
elements.call_me.options.multiOptions.morning = "9:00-5:00"
elements.call_me.options.multiOptions.afternoon = "5:00-10:00"
elements.call_me.options.validators.alnum.validator = "alnum"
elements.call_me.options.validators.alnum.options.messages.stringEmpty = "..."
elements.call_me.options.validators.alnum.options.messages.notAlnum = "..."
elements.call_me.options.required = true
elements.call_me.options.label = "Call Me"

But when there are errors with other elements on the form neither of the radio buttons get selected when the form gets displayed again. All of the other elements on the form seem to work fine, any ideas?
#4 Rashime on 2008-02-15 22:49 (Reply)
Who do you guys have writing the error messages, robots? I really think they could use some humanization. I realize they can be overridden, but you'd think the default ones would be somewhat understandable. "value is empty, but a non-empty value is required". 90% of users are going to think "what the hell does that mean" when reading that. Also, I'm still a little on the fence about mixing all the logic into one component (rendering, validation, etc) Other than that... nice work! :-) I'm happy to finally see a form handler in the framework.
#5 Luke (Link) on 2008-04-07 14:17 (Reply)
For the line:
$shipping->setElementsInArray(true);

Zend_Form_SubForm does not contain a definition for setElementsInArray

Also, using setElementsBelongTo() does not seem to have an effect for elements in display groups, is there a way to make that work?
#6 Christopher on 2008-04-07 14:33 (Reply)
setElementsInArray() existed at the time I wrote this, which was before the API stabilized. I've updated it now to use setIsArray().

Re: Zend_Form_DisplayGroup::setElementsBelongTo(), this is a known bug that I will be fixing in an upcoming release.
#6.1 Matthew Weier O'Phinney (Link) on 2008-04-07 14:40 (Reply)
Oh no! I guess I will have to defer creating my multi page form.

Thanks for the prompt reply; your articles and responses have been remarkably helpful.
#6.1.1 Christopher on 2008-04-07 15:04 (Reply)
You can still create your multi-page forms, just be careful using display groups within sub forms.
#6.1.1.1 Matthew Weier O'Phinney (Link) on 2008-04-07 15:18 (Reply)
Does that mean don't use them at all or is there a workaround I can use?
#6.1.1.1.1 Christopher on 2008-04-21 17:16 (Reply)

Add Comment

Standard emoticons like :-) and ;-) are converted to images.
E-Mail addresses will not be displayed and will only be used for E-Mail notifications

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA

 
 
  • Home
  • Resume
  • Blog
  • Phly PEAR Channel
  • Contact Me
  • About this site

ZCE

Zend Education Advisory Board Member

Add to Technorati Favorites

Calendar

Back May '08 Forward
Mon Tue Wed Thu Fri Sat Sun
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  

Quicksearch

Links

  • PHLY - PHp LibrarY
  • Paul M. Jones
  • Mike Naberezny
  • Shahar Evron
  • Planet PHP
  • Zend Where I now work
  • Garden.org Where I once worked

Archives

May 2008
April 2008
March 2008
Recent...
Older...

Categories

XML Linux
XML Personal
XML Aikido
XML Family
XML Programming
XML Perl
XML PHP

All categories

Syndicate This Blog

XML RSS 0.91 feed
XML RSS 1.0 feed
XML RSS 2.0 feed
ATOM/XML ATOM 0.3 feed
ATOM/XML ATOM 1.0 feed
XML RSS 2.0 Comments

Show tagged entries

xml best practices
xml books
xml conferences
xml file_fortune
xml linux
xml mvc
xml pear
xml php
xml programming
xml zendcon
xml zend framework
© 2004 - present, Matthew Weier O'Phinney
matthew-web <at> weierophinney.net