jeremykendall.net

Dynamically Adding Elements to Zend_Form

UPDATE: This post is now more than 4 ½ years old and the information is woefully out of date. I do not plan to update the code for any newer versions of Zend Framework.

There have been some requests on the Zend Framework mailing lists for information on how to dynamically add elements to Zend_Form.  This is something that I’ve been looking into myself, and I’d like to share what I’ve come up with.

Please note that this code is a proof of concept / request for peer review detailing the work that I’ve done to date, and not an example of what I might consider the best way to address this use case.  Special thanks go to Cory Wiles who helped me think things through when I first started giving this a go.

First, let’s do a high level walk through of what the code is going to do, then take a look at the code, and wrap up with a live example.

High Level Overview

The form in this example extends Zend_Form and consists of a hidden element that stores an ID, a single text element, buttons for adding and removing dynamic elements, and a submit button.  The add and remove buttons are used to trigger a jQuery script that adds and removes dynamic elements. The jQuery script uses the value of the hidden ID element to set element order and make the dynamic element names and IDs unique.

The form class consists of the standard init() method for building the form and two custom methods for dealing with dynamic elements.  There is a preValidation() method, called after the form is submitted but before it is validated, that searches the submitted form data for dynamically added fields.  If any new fields are found, the addNewField() method takes care of adding the new fields to the form.

jQuery is used to request the new form element from the form’s Controller via Ajax, utilizing the AjaxContext action helper. jQuery is also used to find the most recently added dynamic element, allowing for easy removal of dynamically added elements from the form.

The action controller contains the action that displays the form, and it also has a newfieldAction() that utilizes the AjaxContext to return markup for new fields.

The Zend_Form Subclass

Let’s start with the code for the form.  The most important item here is that each form element has its order property set.  You can see the huge jump in the order between the “name” element and the  “addElement” button.  This gap occurs so the dynamic elements can be placed exactly where I want them and so they’ll maintain their position in the form once they’ve been added to the form object.

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
public function init() {

    $this->addElement('hidden', 'id', array(
    'value' => 1
    ));

    $this->addElement('text', 'name', array(
    'required' => true,
    'label'    => 'Name',
    'order'    => 2,
    ));

    $this->addElement('button', 'addElement', array(
    'label' => 'Add',
    'order' => 91
    ));

    $this->addElement('button', 'removeElement', array(
    'label' => 'Remove',
    'order' => 92
    ));

    // Submit
    $this->addElement('submit', 'submit', array(
    'label' => 'Submit',
    'order' => 93
    ));
}

Action Controller

The action that displays the form is straightforward.  If you’ve ever done any work with Zend_Form, I’m sure you recognize what’s going on here.  The only thing to note is the $form->preValidation() method.  That’s where the magic happens.  We’ll get to that in a bit.

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
/**
 * Shows the dynamic form demonstration page
 */
public function dynamicFormElementsAction() {

    $form = new Code_Form_Dynamic();

    // Form has not been submitted - pass to view and return
    if (!$this->getRequest()->isPost()) {
    $this->view->form = $form;
    return;
    }

    // Form has been submitted - run data through preValidation()
    $form->preValidation($_POST);

    // If the form doesn't validate, pass to view and return
    if (!$form->isValid($_POST)) {
    $this->view->form = $form;
    return;
    }

    // Form is valid
    $this->view->form = $form;
}

Next comes the controller’s newfieldAction().  This action utilizes the AjaxContext action helper to pass the new field’s markup back to the form view.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * Ajax action that returns the dynamic form field
 */
public function newfieldAction() {

    $ajaxContext = $this->_helper->getHelper('AjaxContext');
    $ajaxContext->addActionContext('newfield', 'html')->initContext();

    $id = $this->_getParam('id', null);

    $element = new Zend_Form_Element_Text("newName$id");
    $element->setRequired(true)->setLabel('Name');

    $this->view->field = $element->__toString();
}

jQuery

The jQuery script is also fairly straightforward.  I attach event listeners to the “Add” and “Remove” buttons that call the ajaxAddField and removeField methods respectively.

The ajaxAddField method makes a post request to the newfieldAction using jQuery’s .ajax method, passing in the current value of the hidden ID element.  On success, the new element’s markup is added to the form, and the ID is incremented and stored in the hidden ID element.

The removeField method finds the last element in the page with the class dynamic, removes it, then decrements the current ID and stores the new value in the hidden ID element.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<script type="text/javascript">

$(document).ready(function() {

    $("#addElement").click(
        function() {
            ajaxAddField();
        }
    );

    $("#removeElement").click(
        function() {
            removeField();
        }
    );
    }
);

// Get value of id - integer appended to dynamic form field names and ids
var id = $("#id").val();

// Retrieve new element's html from controller
function ajaxAddField() {
    $.ajax(
    {
        type: "POST",
        url: "<?=$this->url(array('action' => 'newfield', 'format' => 'html'));?>",
        data: "id=" + id,
        success: function(newElement) {

        // Insert new element before the Add button
        $("#addElement-label").before(newElement);

        // Increment and store id
        $("#id").val(++id);
        }
    }
    );
}

function removeField() {

    // Get the last used id
    var lastId = $("#id").val() - 1;

    // Build the attribute search string.  This will match the last added  dt and dd elements.  
    // Specifically, it matches any element where the id begins with 'newName<int>-'.
    searchString = '*[id^=newName' + lastId + '-]';

    // Remove the elements that match the search string.
    $(searchString).remove()

    // Decrement and store id
    $("#id").val(--id);
}
</script>

Zend_Form: preValidation() and addNewField()

Now on to the fun stuff.  All of the code up to this point is present to support what happens in the form’s preValidation() method.  Remember that preValidation() is called after the form has been submitted but before the form is validated.  preValidation() searches through the submitted form’s data for new fields.  If it finds any new fields, it calls addNewField() and adds the new fields to the form object.  By adding the new form fields to the form object before validation, any filters and validators attached to the new fields will be run as if those fields had always existed in the form object.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
 * After post, pre validation hook
 * 
 * Finds all fields where name includes 'newName' and uses addNewField to add
 * them to the form object
 * 
 * @param array $data $_GET or $_POST
 */
public function preValidation(array $data) {

  // array_filter callback
  function findFields($field) {
    // return field names that include 'newName'
    if (strpos($field, 'newName') !== false) {
      return $field;
    }
  }

  // Search $data for dynamically added fields using findFields callback
  $newFields = array_filter(array_keys($data), 'findFields');

  foreach ($newFields as $fieldName) {
    // strip the id number off of the field name and use it to set new order
    $order = ltrim($fieldName, 'newName') + 2;
    $this->addNewField($fieldName, $data[$fieldName], $order);
  }
}

/**
 * Adds new fields to form
 *
 * @param string $name
 * @param string $value
 * @param int    $order
 */
public function addNewField($name, $value, $order) {

  $this->addElement('text', $name, array(
    'required'       => true,
    'label'          => 'Name',
    'value'          => $value,
    'order'          => $order
  ));
}

Summary

The ability to dynamically add form fields to Zend_Form is a feature I’d really like to see added to Zend_Form.  If I were talented enough, I might attempt to make a formal proposal myself.  In the meantime, what I’ve come up with can perhaps serve as a starting point for adding very simple elements to very simple forms.

Thanks again to Cory Wiles for helping me work out some of the kinks during the planning phase. Any mistakes, bad practices, or egregious coding errors are the result of my implementation, not his insight and suggestions.

Request for Comments / Peer Review

If you’ve made it this far, I’m grateful to you for hanging in with me.  If you have suggestions for improvements to the code, an implementation of your own, or if you see mistakes I’ve made or poor practices that I’ve employed, I’d appreciate your input.  Thank you in advance for taking the time to discuss this concept with myself and with the ZF community at large.

Full Controller, Form, and View Code

If you’re interested in the complete code for the controller, form, and views, I’ve posted them over at pastebin. Follow the links below to view / grab the code.

Further reading:

Conditional Form Validation With Zend_Form

A question from ‘ronny stalker’ in the Zend_Form_Element_Multi - Tips and Tricks comments:

I need to do different validations for field A depending on the value of field B and (possibly depending on a variable that is not in the form at all - C ).

in this kind of logic:

1
2
3
4
5
6
7
8
9
10
11
12
if (B ==1)
{
validator_B(A);
}
elseif (C)
{
validator_C(A);
}
else
{
validator_Default(A);
}

I understand that validators get a secondary argument called $context - which can be used to check values of other fields, but how can a validator get knowledge of other variables in the environment?

While this post may not answer ronny’s question exactly, hopefully it will give him a good starting point to get over the hump.

If other, please explain - Conditional Validation Using $context

Many forms have a set of radio buttons, or sometimes a select element, where a user can choose from one of several options.  Sometimes “other"  will be one of those options, with a corresponding "If other, please explain” text field placed directly after.  If “other” is selected, then the accompanying text field is usually required.  Since there’s not a standard Zend Validate validator for this scenario, I’ve written a custom validator that seems to do the trick.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
<?php
/**
 * Kendall Extensions
 * 
 * @category Kendall
 * @package  Kendall_Validate
 * @author   Jeremy Kendall 
 */

/**
 * @see Zend_Validate_Abstract
 */
require_once 'Zend/Validate/Abstract.php';

/**
 * Requires field presence based on provided value of radio element.  
 * 
 * Example would be radio element with Yes, No, Other option, followed by an "If 
 * other, please explain" text area.
 * 
 * IMPORTANT: For this validator to work, allowEmpty must be set to false on 
 * the child element being validated.
 * 
 * From Zend Framework Documentation 15.3: "By default, when an 
 * element is required, a flag, 'allowEmpty', is also true. This means that if 
 * a value evaluating to empty is passed to isValid(), the validators will be 
 * skipped. You can toggle this flag using the accessor setAllowEmpty($flag); 
 * when the flag is false, then if a value is passed, the validators will still 
 * run."
 * 
 * @uses     Zend_Validate_Abstract
 * @category Kendall
 * @package  Kendall_Validate
 * @author   Jeremy Kendall 
 */
class Kendall_Validate_FieldDepends extends Zend_Validate_Abstract {

    /**
     * Validation failure message key for when the value of the parent field is an empty string
     */
    const KEY_NOT_FOUND  = 'keyNotFound';

    /**
     * Validation failure message key for when the value is an empty string
     */
    const KEY_IS_EMPTY   = 'keyIsEmpty';

    /**
     * Validation failure message template definitions
     *
     * @var array
     */
    protected $_messageTemplates = array(
        self::KEY_NOT_FOUND  => 'Parent field does not exist in form input',
        self::KEY_IS_EMPTY   => 'Based on your answer above, this field is required',
    );

    /**
     * Key to test against
     *
     * @var string|array
     */
    protected $_contextKey;

    /**
     * String to test for
     *
     * @var string
     */
    protected $_testValue;

    /**
     * FieldDepends constructor
     *
     * @param string $contextKey Name of parent field to test against
     * @param string $testValue Value of multi option that, if selected, child field required
     */
    public function __construct($contextKey, $testValue = null) {
        $this->setTestValue($testValue);
        $this->setContextKey($contextKey);
    }

    /**
     * Defined by Zend_Validate_Interface
     *
     * Wrapper around doValid()
     *
     * @param  string $value
     * @param  array  $context
     * @return boolean
     */
    public function isValid($value, $context = null) {

        $contextKey = $this->getContextKey();

        // If context key is an array, doValid for each context key
        if (is_array($contextKey)) {
            foreach ($contextKey as $ck) {
                $this->setContextKey($ck);
                if(!$this->doValid($value, $context)) {
                    return false;
                }
            }
        } else {
            if(!$this->doValid($value, $context)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if dependant field value is not empty when parent field value
     * indicates that the dependant field is required
     *
     * @param  string $value
     * @param  array  $context
     * @return boolean
     */
    public function doValid($value, $context = null) {
        $testValue  = $this->getTestValue();
        $contextKey = $this->getContextKey();
        $value      = (string) $value;
        $this->_setValue($value);

        if ((null === $context) || !is_array($context) || !array_key_exists($contextKey, $context)) {
            $this->_error(self::KEY_NOT_FOUND);
            return false;
        }

        if (is_array($context[$contextKey])) {
            $parentField = $context[$contextKey][0];
        } else {
            $parentField = $context[$contextKey];
        }

        if ($testValue) {
            if ($testValue == ($parentField) && empty($value)) {
                $this->_error(self::KEY_IS_EMPTY);
                return false;
            }
        } else {
            if (!empty($parentField) && empty($value)) {
                $this->_error(self::KEY_IS_EMPTY);
                return false;
            }
        }

        return true;
    }

    /**
     * @return string
     */
    protected function getContextKey() {
        return $this->_contextKey;
    }

    /**
     * @param string $contextKey
     */
    protected function setContextKey($contextKey) {
        $this->_contextKey = $contextKey;
    }

    /**
     * @return string
     */
    protected function getTestValue () {
        return $this->_testValue;
    }

    /**
     * @param string $testValue
     */
    protected function setTestValue ($testValue) {
        $this->_testValue = $testValue;
    }
}

The validator above is essentially a conditional NotEmpty validator.  It checks the value of a parent field to see if a child field should be required.  IMPORTANT:  allowEmpty must be set to false on the child field.

Here’s an example of how to use the validator.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Parent element
$this->addElement('radio', 'flavor', array(
    'required'     => true,
    'label'        => 'Choose a flavor',
    'multiOptions' => array('Vanilla' => 'Vanilla', 'Chocolate' => 'Chocolate', 'Other' => 'Other')
));

// Child element. IMPORTANT: allowEmpty must be set to false!
$this->addElement('text', 'flavorOther', array(
    'allowEmpty' => false,
    'label'      => 'If Other, provide flavor here',
    'validators' => array(new Kendall_Validate_FieldDepends('flavor', 'Other')),
));

Again, please note that allowEmpty has been set to false on the child field.  This is necessary to run the FieldDepends validator even when the “If other …” element is empty.

While I’m sure there’s plenty of room for refactoring, the above code has served me well.

Adding Validators After Submission but Before Validation

Expanding on the example above, what if it became necessary to add additional validators to the “If other …” field?  Because the “If other …” field has allowEmpty set to false, and because an empty value is sometimes a valid value, it is not possible to add additional validators that will run only if the field is not empty.  The additional validators will run regardless of the value of the “If other …” element, throwing errors when the element is empty.  Additional validators will have to be added somewhere else.

In order to work around this issue, I added a custom method called preValidation() to my form class.

1
2
3
4
5
6
7
8
public function preValidation($data) {

    if (!empty($data['flavorOther'])) {
    $this->flavorOther->addValidator(new FlavorOther_Validator());
    }

    return $data;
}

The preValidation() method is called after submission but before validation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$form = new Flavor_Form();

if (!$this->getRequest()->isPost()) {
    // Display form
    $this->view->form = $form;
    return;
}

$data = $form->preValidation($_POST);

if (!$form->isValid($data)) {
    // Failed validation, redisplay form with values and errors
    $this->view->form = $form;
    return;
}


// Passed validation

While the preValidation() code above adds validation depending on the state of an element in the form, it would be trivial to add validation to the form based on any number of conditions, including conditions that exist as a result of business rules rather than the form’s input.

Wrapping Up

Writing custom validators for the Zend Framework makes server side validation of unique validation scenarios a breeze.  I have yet to encounter a non-standard validation scenario where I haven’t been able to address it by writing a custom validator.  With the ability to extend Zend Form with a couple of helpful custom methods, adding additional validation after form submission becomes trivial.

Have you ever had to write any custom validators?  Any suggestions on improving the code above?  Jump down to the comments and let us know!

UPDATED to add code comments to the validator implementation example. Thanks to reader Neil for the suggestion.

Zebra Tables With jQuery

I’ve used the classic A List Apart Zebra Tables technique to stripe my tables for years.  It’s always worked well, and I never really considered updating the technique until last week.  I’ve been making heavy use of the jQuery library lately, and I really disliked including another external js file whenever I wanted to stripe a table, so I thought I’d see if someone had come up with a jQuery friendly table striping technique.  It took about 10 seconds on Google to find what I was looking for, and the solution was so simple and elegant that I wanted to kick myself for not thinking of it, er, myself.

For the whole scoop, head over and read the tutorial.  If you’re like me, you want to get right to the point, so here goes.

The idea is to use jQuery to select alternate rows from your table and apply a css class to them. In the example below, the table has a class of ‘striped’ and jQuery adds the class ‘alt’ to the even rows.

$(document).ready(function(){
    $(".striped tr:even").addClass("alt");
});

Whip up a little css that adds a background color to .alt and you’re done. Not bad, huh?

The tutorial author also included an example of jQuery code that allows for a nice hover effect when you mouse over the table rows.  I wasn’t as interested in that, so you’ll have to head over there for the scoop.

Zend_Form_Element_Multi - Tips and Tricks

I’m responsible for creating a lot of forms at my day job. It seems that any project I get involved in requires at least one form. The Zend Form component has made my life a lot easier. After putting together more forms than I can count, I’ve picked up a couple of tricks that I’d like to share. Here are some for the Zend_Form_Element_Multi elements.

As noted in the API documentation, Zend_Form_Element_Multi is the base class for multi-option form elements. Its direct descendants are the Zend Form Select, Radio, and MultiCheckbox elements. Adding options to these elements is possible using the addMultiOptions method. Most of what I want to cover is about retrieving, creating, and adding options, with a short detour into validation.

Using array_combine

Sometimes you want the displayed element options to be the same options returned by the form (as opposed to displaying a string while returning an id). Perhaps you’ll be sending the value(s) along in an email or storing them as strings in a database. While you can create an associative array with matching keys and values, the process quickly becomes tedious with an array of any appreciable size. Why not use array_combine to make life easier?

$options = array('Vanilla', 'Chocolate', 'Strawberry', 'Cookies and Cream', 'Chocolate Chip');
$options = array_combine($options, $options);

Using array_merge

array_merge is helpful when you’d like to add an item to your options array that isn’t already a part of the options array. For example, I frequently add a “Please make a selection” option to my select elements. Extending the above example, I might choose to add the new option like this:

$options = array_merge(array('Please select a flavor'), $options);

One word of caution: array_merge will reindex numerically indexed arrays. array_merge should never be used in a situation where the original array needs to be preserved, such as a numerically indexed array of id and value pairs pulled from a database. In those cases, I use the + operator.

// $options is an array of database ids and flavor descriptions
$options = array('Select a flavor') + $options;

Retrieving options using Zend_Db

I frequently retrieve options from a database, using the record id as the array’s index and a related string as the array’s value. There are a lot of ways retrieve options using Zend_Db, but my favorite is the fetchPairs method.

The fetchPairs() method returns data in an array of key-value pairs, as an associative array with a single entry per row. The key of this associative array is taken from the first column returned by the SELECT query. The value is taken from the second column returned by the SELECT query. Any other columns returned by the query are discarded.

Here’s what that might look like.

$select = 'SELECT flavor_id, flavor FROM flavors';
$options = $db->fetchPairs($select);

I especially enjoy using this method with Zend_Db_Table, using custom table class methods to retrieve my options. My table class usually looks like this:

class Flavors extends Zend_Db_Table_Abstract {

  protected $_name = 'flavors';

  public function getFlavorOptions() {

    $select = $this->select()->from($this, array('flavor_id', 'flavor'));
    $result = $this->getAdapter()->fetchPairs($select);

    return $result;
  }
}

Grabbing your options now becomes ridiculously simple.

$flavors = new Flavors();
$flavorOptions = $flavors->getFlavorOptions();

Validation with Zend_Validate_InArray

Zend_Validate_InArray is the default validator for Multi elements, but the InArray validator can be a little tricky to implement properly. Below are the two gotchas that I’ve run into.

Let’s say you’ve got a select element in your form. In order to force the user to select an option, you’ve added a “Please Select” option to the beginning of your options array. If you use the default InArray validation against the full list of options, “Please Select” becomes a valid selection, and you may end up stuck with a lot of bad form submissions. In order to get around this, I make sure to pass the original array of options to the validator, and use array_merge or the + operator to add the “Please select” option before adding the options to the element.

// Create list of flavor options
$flavorOptions = array('Vanilla', 'Chocolate', 'Strawberry', 'Cookies and Cream', 'Chocolate Chip');
$flavorOptions = array_combine($flavorOptions, $flavorOptions);

// Add "Select a flavor" option
$flavorMultiOptions = array_merge(array('Select a flavor'), $flavorOptions);

// Add flavor options to flavor select element
$form->flavor->addMultiOptions($flavorMultiOptions);

// Add validation, validating against original $flavorOptions array
$form->flavor->addValidator(new Zend_Validate_InArray($flavorOptions));

The second gotcha has to do with option arrays where the keys and values don’t match. InArray tests the element’s selected value against the values of the options array, but what you really want to do is test the element’s selected value against the keys of the options array. The trick is to use PHP’s array_keys function.

// Get flavor ids and descriptions from database
$flavors = new Flavors();
$flavorOptions = $flavors->getFlavorOptions();

// Add "Select a flavor" option, preserving original array with the + operator
$flavorMultiOptions = array('Select a flavor') + $flavorOptions;

// Add flavor options to flavor select element
$form->flavor->addMultiOptions($flavorMultiOptions);

// Add validation, validating against array keys of the original $flavorOptions array
$form->flavor->addValidator(new Zend_Validate_InArray(array_keys($flavorOptions)));

Do you have any Zend_Form tips or tricks that you’d like to share? Have I made any egregious errors above that need to be corrected? Hit the comments and let me know.

Dynamic Content on Static Pages Using Zend Json and jQuery

The Problem

One of the most highly trafficked sites on the intranet where I work is the cafeteria’s web site.  Employees from all across the enterprise visit daily to see what’s on the menu.  Maintaining the menu pages on the cafeteria site has been a constant challenge for our content folks.

As the bulk of our intranet is still mostly static (PHP isn’t even installed on our main intranet box), our content people have to work really hard to keep the menu up-to-date.  The process consists of the cafeteria folks emailing an updated menu over to our content people and our content folks manually updating the menu’s html.  Since the cafeteria is open seven days a week, that means our content folks have to manage eight separate pages: the cafeteria homepage featuring the current day’s menu in a side bar and separate menu pages for each day of the week.

The Solution, Almost

The solution to this issue began with a simple Java CRUD app, allowing the cafeteria employees to manage the menu themselves.  The “Menu Builder” application spits out an XML document for consumption on the cafeteria website.  During the development process, I extended Zend_Http_Client and wrote a simple index page to parse the XML for display.  While the PHP solution for displaying the menu was nice, clean, and simple, it turned out to be completely useless.

As I mentioned above, the majority of our intranet is still running on a server without PHP installed.  We’re working to move away from that server, but the process is slow and painful.  Without the ability to use PHP to serve the daily menu, we had to come up with something else.  AJAX was the obvious choice, but how to deal with the fact that we’d have to make cross-domain requests in order to retrieve the application’s XML output?

Cross-Domain JSON with jQuery

After a little digging around, one of my co-workers turned me on to jQuery and JSONP.  As of version 1.2, jQuery provides native support for JSONP, a method of retrieving JSON data across domains.  The implementation can be found in jQuery.getJSON.

Once the decision was made to utilize jQuery’s getJSON, I had to come up with a way to convert the menu’s XML output to JSON.  Since the Menu Builder app was already spitting out XML, there was no way I was going to add a module to the application to support the new JSON requirement.  I needed a simple way to convert XML to JSON, and I didn’t want to spend any time rolling my own solution.

Zend Framework to the Rescue

Since we’re already using Zend Framework on our intranet, I decided to look to Zend Json and see if it could do the job.  Sure enough, there’s a static fromXml() method that takes care of the conversion beautifully.

After that, it was fairly quick work to create some JavaScript functions to parse the returned JSON and display it on the cafeteria website.

[Side note: As of ZF 1.6, the Dojo JavaScript library is integrated with Zend Framework.  Well before that integration occurred, we made the decision that jQuery would be our library of choice.  That’s why I’m using jQuery for this solution rather than Dojo.]

The Code

First is an example of the XML returned from the Menu Builder application.

<menu>
  <timestamp>Thu Oct 16 13:18:50 CDT 2008</timestamp>
  <day>Thursday</day>
  <date>October 16, 2008</date>
  <meal descr="Breakfast Feature">
    <category descr="None (will not display to users)">
      <item>
        <itemdescr>French Toast</itemdescr>
        <itemprice>0.45</itemprice>
      </item>
      <item>
        <itemdescr>Biscuit</itemdescr>
        <itemprice>0.25</itemprice>
      </item>
    </category>
  </meal>
  <meal descr="Lunch">
    <category descr="Grab & Go">
      <item>
        <itemdescr>Garden Vegetables</itemdescr>
        <itemprice>0.89</itemprice>
      </item>
    </category>
    <category descr="Papa J's Pizza by the Slice">
      <item>
        <itemdescr>Sausage Egg & Cheese Biscuit</itemdescr>
        <itemprice>2.09</itemprice>
      </item>
    </category>
  </meal>
</menu>

Running the above XML through Zend_Json::fromXml() provided me with the JSON representation I needed.

{
    "menu": {
        "timeStamp": "Thu Oct 16 13:18:50 CDT 2008",
        "day": "Thursday",
        "date": "October 16, 2008",
        "meal": [
            {
                "@attributes": {
                    "descr": "Breakfast Feature"
                },
                "category": {
                    "@attributes": {
                        "descr": "None (will not display to users)"
                    },
                    "item": [
                        {
                            "itemdescr": "French Toast",
                            "itemprice": "0.45"
                        },
                        {
                            "itemdescr": "Biscuit",
                            "itemprice": "0.25"
                        }
                    ]
                }
            },
            {
                "@attributes": {
                    "descr": "Lunch"
                },
                "category": [
                    {
                        "@attributes": {
                            "descr": "Grab & Go"
                        },
                        "item": {
                            "itemdescr": "Garden Vegetables",
                            "itemprice": "0.89"
                        }
                    },
                    {
                        "@attributes": {
                            "descr": "Papa J's Pizza by the Slice"
                        },
                        "item": {
                            "itemdescr": "Sausage Egg & Cheese Biscuit",
                            "itemprice": "2.09"
                        }
                    }
                ]
            }
        ]
    }
}

As you can see in the samples above, menu items belong to categories and categories belong to meals. Both meals and categories are described in XML attributes rather than in a description node.

Next is the PHP script that I wrote to get the menu XML, convert it to JSON, and echo it out to the jQuery.getJSON call.

// Include required files
require_once dirname(dirname(dirname(__FILE__))).'/cafeteria/lib/base.php';

// Set service uri
$uri  = $cafeteriaConfig->menu->serviceuri;

// Valid days of the week
$validDays = array('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday');

// Get day from querystring, validate
if (isset($_GET['day'])) {

  $getDay = $_GET['day'];

  if (in_array($getDay, $validDays)) {
    $day = $getDay;
    $uri = $uri . '?day=' . $day;
  }

}

// Get xml from cafeteria service
$xml = file_get_contents($uri);

// Encode returned xml as JSON, do not ignore XML attributes
$json = Zend_Json::fromXml(trim($xml), false);

// Return response, wrapping response in the requested callback
// @see http://remysharp.com/2007/10/08/what-is-jsonp/
// @see http://docs.jquery.com/Release:jQuery_1.2/Ajax
echo $_GET['callback'] . '(' . $json . ')';

There are a couple of important things to note in the script above.

First, we keep a lot of code in a base.php file. This includes Zend_Loader::registerAutoload() and the creation of a Zend_Config object, among other things, allowing for much DRYer code.

Second, you’ll notice that I’m using the second, optional parameter for fromXml().  Set to false, fromXml() will return a representation of the XML attributes present in the input.  fromXml() ignores XML attributes by default.

Finally, you can see how the callback name and parentheses are wrapped around the returned JSON before outputting anything back to jQuery.getJSON.  This is required.  Without it, your cross-domain request won’t return anything at all.

The next bit of code is the JavaScript that parses the returned JSON and outputs the full menu to the web.  Notice that there are two output functions.  This is to allow for two different styles of menu tables: one for the cafeteria homepage (sidebar), and one for the daily menu page (full sized).

// Parses JSON representation of cafeteria menu and returns Menu table
// Requires jQuery

var menu = '';

// Builds sidebar menu for cafeteria home page
function outputSidebarMenu(json) {
    var day = json.menu.day;
    var heading = '<h4><a href="' + menuLink + '?requestedDay=' + day.toLowerCase() + '">' + day + "'s Menu</a></h4>\n";
    menu = '<table id="side_datatable">' + getMeals(json.menu.meal) + '</table>';
    return timeStampComment(json) + heading + menu;
}

// Builds table for daily menu page
function outputDailyMenu(json) {
    var day = json.menu.day;
    var date = json.menu.date;
    var meal = json.menu.meal;
    var heading = '<h4 style="text-align:right; font-weight:normal;"><span style="float:left; font-weight:bold;">' + day + '\'s Menu</span>' + date + "</h4>\n";
    menu = '<table width="400" class="datatable">' + "\n" + getMeals(meal) + "</table>\n";
    return timeStampComment(json) + heading + menu;
}

// Get menu meals
function getMeals(meals) {
    var mealList = '';
    if (meals != null) {
        if (meals instanceof Array) {
            $.each(meals, function(i,meal) {
                mealList += buildMealRow(meal);
                mealList += getCategories(meal.category);
            });
        } else {
            mealList += buildMealRow(meals);
            mealList += getCategories(meals.category);
        }
    }
    return mealList;
}

// Get meal's categories
function getCategories(categories) {
    var catList = '';
    if (categories != null) {
        // Is categories an array?
        if (categories instanceof Array) {
            $.each(categories, function(i, category) {
                catList += buildCategoryRow(category);
                catList += getItems(category.item);
            });  
        } else {
            catList += buildCategoryRow(categories);
            catList += getItems(categories.item);
        }
    }
    return catList;
}

// Get category's items
function getItems(items) {
    var itemList = '';
    // Is items an array?
    if (items instanceof Array) {
        $.each(items, function(i, item) {
            itemList += buildItemRow(item);
        });  
    } else {
        itemList += buildItemRow(items);
    }
    return itemList;
}

// Build individual meal's table row
function buildMealRow(meal) {
    var descr = meal['@attributes'].descr;
    var mealRow = '<tr><th colspan="2">' + descr + "</th></tr>\n";
    return mealRow;
}

// Build individual category's table row
function buildCategoryRow(category) {
    var catRow = '';
    if (category != null) {
        var descr = category['@attributes'].descr;
        // Do not display the 'None' category
        if (descr.search(/None/) == -1) {
            catRow = '<tr class="title"><td colspan="2">' + descr + "</td></tr>\n";
        }
    }
    return catRow;
}

// Build individual item's table row
function buildItemRow(item) {
    var itemRow = '';
    if (item != null) { 
        var itemDescr = item.itemdescr;
        var itemPrice = item.itemprice;
        itemRow = '<tr><td>' + itemDescr + '</td><td>$' + itemPrice + "</td></tr>\n";
    }
    return itemRow;
}

// Update Cafeteria Daily Menu page title with "[day]'s Menu"
function updatePageTitle(json) {
    // Get day to use in page title
    var titleDay = json.menu.day;
    // Append day to page title
    document.title = document.title + ' | ' + titleDay + "'s Menu";
}

// Output html comment with note on when menu was last cached for troubleshooting
// and debugging
function timeStampComment(json) {
      var day = json.menu.day;
      var cacheTimeStamp = json.menu.timeStamp;
      var timeStampComment = "\n\n";
      return timeStampComment;
}

There’s one big gotcha in the code above.  Notice how I’m setting the descr variable in both the buildMenuRow() and the buildCategoryRow() functions. If you don’t use array notation with @attributes ([‘@attributes’]), this code will fail in IE.

Finally, let’s take a look at how I displayed the menu on the cafeteria home page and on the daily menu page.

This first snippet displays the sidebar menu on the cafeteria homepage. Since the homepage always displays the current day’s menu, the only parameter that I’m passing with the querystring is the required callback.

// URL to menu display page
    var menuLink = 'http://www.example.com/cafeteria/dailyMenu.shtml';

    $(document).ready(function() {

       // Get JSON representation of cafeteria menu for today
        $.getJSON("http://www.cross-domain-example.com/service/cafeteria/json.php?callback=cafeteriaMenu", function(json) {
            // Build html menu table
            var content = outputSidebarMenu(json);
            // Add table to .sidebarMenu div
            $(".sidebarMenu").html(content);
        });
    });

The last snippet displays the full sized menu on the daily menu page. Since I only want a single daily menu page, rather than a page for each day of the week, I had to have some way to grab a “requestedDay” parameter from the querystring. The jQuery Query String Object plugin solved that problem perfectly.

// Menu display requires jQuery and jQuery querystring plugin (jQuery.query.js)
    $(document).ready(function() {

          // JSON service URL
        var jsonUrl = 'http://www.cross-domain-example.com/service/cafeteria/json.php?callback=?';

        // Get requested day from QueryString
        // @see http://plugins.jquery.com/project/query-object
        var reqDay = $.query.get('requestedDay');
        jsonUrl = jsonUrl + '&day;=' + reqDay.toLowerCase();

        // Get JSON representation of cafeteria menu for today            
        $.getJSON(jsonUrl, function(json) {

            // Add "[day]'s Menu" to page title
            updatePageTitle(json);

            // Build html menu table
            var content = outputDailyMenu(json);

            // Add table to #menuBuilder div
            $("#menuBuilder").html(content);
        });

    });

Wrapping Up

When you’re in a situation where you can’t use PHP (or your programming language of choice) to display dynamic content, AJAX can be an excellent solution.  With the right tools, good documentation, and a little time spent on Google, it’s relatively simple to overcome the limitations of your environment and deliver rich, up-to-date content to your users.  Combining Zend Framework, the jQuery JavaScript Library, and JSONP made for a nice, lightweight solution to the cafeteria menu problem, saving my clients and co-workers a lot of time and effort they can now expend elsewhere.

My Contribution to the History Meme

From my work machine:

jkendall@ventura:~$ uname -a
Linux ventura 2.6.22-14-generic #1 SMP Tue Feb 12 07:42:25 UTC 2008 i686 GNU/Linux
jkendall@ventura:~$ history | awk '{a[$2]++}END{for(i in a){print a[i] " " i}}' | sort -rn | head
83 exit
70 clear
58 cd
47 ls
44 ps
35 svn
32 tail
32 jgrep
23 kill
11 sshdev

jgrep is a bash alias for 'grep --color -r -n --exclude=\*.svn\*', while sshdev is a bash alias that gets me to the dev box at work without having to type too much.

See more examples here, here, here, and here.

Dell 1420n Ubuntu Wireless Issue Resolved

A few months ago I picked up a Dell 1420n loaded with Ubuntu 7.10. I’ve really enjoyed the laptop, but I’ve experienced ongoing issues with my wireless connectivity. The laptop will connect to my wireless router without any problems. Staying connected to the wireless router was the problem.

After using the laptop for anywhere from 5 minutes to 36 hours, the wireless connection would drop. The only way to reconnect was to reboot the laptop. That got old fast, but being a new Linux user I had no idea how to troubleshoot the issue. I finally Googled the problem and, via this thread on the excellent Ubuntu Forums, I found the DellLinuxWiki and the answer to my problem.

The issue has something to do with the ipw3945 wireless module. The DellLinuxWiki entry suggests using the network module iwl3945 instead, and provides simple step-by-step instructions as to how to disable the ipw3945 module and how to enable the iwl3945 module.

The resolution works like a charm, and I haven’t had a wireless connectivity issue since.

Zend Studio for Eclipse Leap Year FTP Bug

I found a bug in Zend Studio for Eclipse the other day. I created a new QA subdomain for one of my web project. When I tried to view the new directory in Zend Studio for Eclipse, it wouldn’t show up. Nothing that I did in Zend Studio would make it show up.

It drove me nuts until I found a clue in this discussion on the Zend Forums. Zend Forum member adlorenz posted the clue, saying

… it seems like bug concerns only files/directories that have leap day date of last modification

Turns out that any directory or file created or last modified on Feb 29 of this year won’t show up in FTP Remote Systems connections in Zend Studio for Eclipse. I logged into my account, touched the directories and files that were dated Feb 29 ($ touch dirname), and I could finally see the directories and files in the Remote Systems Explorer.

Image Upload Issues With Internet Explorer and Image/pjpeg

I’m working on a project that uses Zend_Gdata, specifically Zend_Gdata_Photos, to manage photos in Google’s Picasa Web Albums. When the application went into its testing phase, the image upload functionality began to throw exceptions. While I could upload photos to Picasa without issue, my business partner John couldn’t upload any photos at all. The application kept throwing the following error:

1
Error Message: Exception Message: Expected response code 200, got 400

Since the app worked fine for me, the issue didn’t seem to be with the code. After some frustrating, unproductive troubleshooting, I finally built a file upload mockup and ran the $_FILES array through Zend_Debug::dump(). I discovered that while Firefox reports the MIME type of a jpg as “image/jpeg”, Internet Explorer reports the MIME type as “image/pjpeg.” Thanks a lot, Redmond.

As it turned out, the problem occurred when I tried to set the upload content type to “image/pjpeg”:

1
2
$fd = $this->_service->newMediaFileSource($photo["tmp_name"]);
$fd->setContentType($photo["type"]);

I resolved the issue by adding a test for the pjpeg MIME type before setting the content type. The new code looks like this:

1
2
3
4
/** Resolves issue where IE changes jpg MIME type to image/pjpeg, breaking upload */
$photo["type"] = ($photo["type"] === "image/pjpeg") ? "image/jpeg" : $photo["type"];
$fd = $this->_service->newMediaFileSource($photo["tmp_name"]);
$fd->setContentType($photo["type"]);

The code snippets above come from an image upload helper method that I built (borrowing heavily from Cory Wiles) to make working with Zend_Gdata_Photos a little easier.

If you’re having similar problems uploading images, regardless of the method that you’re using, check that MIME type and see if that’s not the issue.