Phil Brown's Web Development Blog

Zend Framework and Doctrine Part 1

I’ve begun to investigate the Doctrine ORM library and how to integrate it into Zend Framework applications. I figure this post and any subsequent ones on the topic can grow into some sort of discovery series.

So, my first post is going to be a simple one – bootstrapping with Doctrine 1.2.

I figured the most robust and consistent way to get Doctrine into my applications was to create an application resource plugin so without further delay, here it is.

// library/Tolerable/Application/Resource/Doctrine.php
 
class Tolerable_Application_Resource_Doctrine extends Zend_Application_Resource_ResourceAbstract
{
    /**
     * @var Doctrine_Manager
     */
    protected $_manager;
 
    /**
     * @var Doctrine_Cache_Interface
     */
    protected $_cacheDriver;
 
    /**
     * @var string|array
     */
    protected $_cache;
 
    /**
     * @var array
     */
    protected $_connections = array();
 
    /**
     * Set cache options
     * 
     * @param string|array $cache
     * @return Tolerable_Application_Resource_Doctrine
     */
    public function setCache($cache)
    {
        $this->_cache = $cache;
        return $this;
    }
 
    /**
     * @return string|array
     */
    public function getCache()
    {
        return $this->_cache;
    }
 
    /**
     * Set connection configuration
     * 
     * @param array $connections
     * @return Tolerable_Application_Resource_Doctrine
     */
    public function setConnections(array $connections)
    {
        $this->_connections = $connections;
        return $this;
    }
 
    /**
     * Initialise and return Doctrine_Manager instance
     * 
     * @return Doctrine_Manager
     */
    public function getManager()
    {
        if (null === $this->_manager) {
            $manager = Doctrine_Manager::getInstance();
            $manager->setAttribute(Doctrine_Core::ATTR_MODEL_LOADING,
                                   Doctrine_Core::MODEL_LOADING_CONSERVATIVE);
            $manager->setAttribute(Doctrine_Core::ATTR_VALIDATE,
                                   Doctrine_Core::VALIDATE_ALL);
            $manager->setAttribute(Doctrine_Core::ATTR_AUTO_ACCESSOR_OVERRIDE, true);
            $manager->setAttribute(Doctrine_Core::ATTR_AUTOLOAD_TABLE_CLASSES, true);
            $manager->setAttribute(Doctrine_Core::ATTR_USE_DQL_CALLBACKS, true);
            $manager->setAttribute(Doctrine_Core::ATTR_AUTO_FREE_QUERY_OBJECTS, true);
 
            if ($cacheDriver = $this->_getCacheDriver()) {
                $manager->setAttribute(Doctrine_Core::ATTR_QUERY_CACHE, $cacheDriver);
            }
 
            $this->_manager = $manager;
        }
        return $this->_manager;
    }
 
    /**
     * Initialise and return query cache driver if configured
     * 
     * @return Doctrine_Cache_Interface|null
     */
    protected function _getCacheDriver()
    {
        if (null === $this->_cacheDriver) {
            $options = $this->getCache();
            if (empty($options)) {
                return null;
            }
 
            if (is_array($options)) {
                if (!array_key_exists('driver', $options)) {
                    throw new Zend_Application_Resource_Exception('Missing Doctrine cache driver');
                }
                $driver = $options['driver'];
                unset($options['driver']);
            } else {
                $driver = (string) $options;
                $options = array();
            }
 
            if (!class_exists($driver)) {
                Zend_Loader::loadClass($driver);
            }
 
            $this->_cacheDriver = new $driver($options);
        }
        return $this->_cacheDriver;
    }
 
    /**
     * Initialise any configured connections
     * 
     * @return void
     */
    protected function _initConnections()
    {
        foreach ($this->_connections as $name => $options) {
            if (!is_array($options)) {
                $options = array('dsn' => $options);
            }
 
            if (array_key_exists('charset', $options)) {
                $charset = $options['charset'];
                unset($options['charset']);
            } else {
                $charset = 'utf8';
            }
 
            // Attempt to find "dsn" or "connection_string" keys
            // falling back to an array of connection options
            if (array_key_exists('dsn', $options)) {
                $adapter = $options['dsn'];
            } else if (array_key_exists('connection_string', $options)) {
                $adapter = $options['connection_string'];
            } else {
                $adapter = $options;
            }
 
            $connection = $this->getManager()->openConnection($adapter, $name);
            $connection->setAttribute(Doctrine_Core::ATTR_USE_NATIVE_ENUM, true);
            $connection->setCharset($charset);
        }
    }
 
    /**
     * Initialise and return Doctrine manager
     * 
     * @return Doctrine_Manager
     */
    public function init()
    {
        $this->_initConnections();
        return $this->getManager();
    }
}

This class provides a reusable component to use in any ZF 1.8+ application. To enable the plugin, add the following to your application.ini file.

; Add the Doctrine and Tolerable namespaces to the autoloader
autoloaderNamespaces[] = "Doctrine_"
autoloaderNamespaces[] = "Tolerable_"
 
; Add our custom application resource plugin path to the plugin loader
pluginPaths.Tolerable_Application_Resource = "Tolerable/Application/Resource"

Configure the Doctrine query cache (if desired)

; Simple examples, no driver options
resources.doctrine.cache = "Doctrine_Cache_Apc"
 
; or with options
resources.doctrine.cache.driver = "Doctrine_Cache_Apc"
resources.doctrine.cache.option = ...
resources.doctrine.cache.option = ...
resources.doctrine.cache.option = ...

See here for drivers and options.

Configure the connections

; Simple connection strings
resources.doctrine.connections[] = "mysql://user1:pass@localhost/db1"
resources.doctrine.connections[] = "mysql://user2:pass@localhost/db2"
 
; or specify connection name and options
resources.doctrine.connections.default.dsn     = "mysql://user1:pass@localhost/db1"
resources.doctrine.connections.default.charset = "utf8"
 
; or specify Doctrine connection parameters
resources.doctrine.connections.default.scheme = "mysql"
resources.doctrine.connections.default.user   = "user1"
resources.doctrine.connections.default.pass   = "pass"
resources.doctrine.connections.default.host   = "localhost"
resources.doctrine.connections.default.path   = "db1"

This is assuming that your custom (Tolerable in these examples) and Doctrine libraries are under your application’s “library” path. If not, simply add them to the application’s include path configuration, for example

includePaths.library = APPLICATION_PATH "/../library"
includePaths.doctrine = "/path/to/doctrine/lib"
includePaths.custom = "/path/to/custom/library"

A lot of the code in the plugin has been taken from Matthew Weier O’Phinney’s blog post on Autoloading Doctrine and Doctrine entities from Zend Framework.

2009-11-19 Update

Some extra inspiration came from Juozas Kaziukenas’ post on Zend Framework and Doctrine. Part 2 as well as the twitter-verse and now the plugin handles multiple connections and caching.

Overriding Zend form element default decorators, for good!

I’m sure everybody who has used Zend_Form has, at one time or another, wished they could change the default form element decorators in one fell swoop, simply and efficiently.

Up until now, I’ve been using one of two methods to override the default “definition list” style; set each element’s decorator scheme on instantiation or, use the form’s “setElementDecorators” method to reset the scheme for nominated elements. No longer!

Using just a few simple classes, I’ll show you how to permanently alter the default decorator scheme, thus simplifying your code and making it ultimately more flexible.

For the following example, I’ll assume a decorator scheme like the following

form
  fieldset
    ol
      li
        label
        element
      li
        button

Lets start by extending Zend_Form_Element. I’d also like to take this opportunity to introduce my new “Tolerable” library.

// library/Tolerable/Form/Element.php
 
class Tolerable_Form_Element extends Zend_Form_Element
{
    /**
     * Load default decorators
     *
     * @return void
     */
    public function loadDefaultDecorators()
    {
        if ($this->loadDefaultDecoratorsIsDisabled()) {
            return;
        }
 
        $decorators = $this->getDecorators();
        if (empty($decorators)) {
            $this->addDecorator('ViewHelper')
                 ->addDecorator('Errors')
                 ->addDecorator('Description', array('tag' => 'p',
                                                     'class' => 'description'))
                 ->addDecorator('Label', array('requiredSuffix' => ' *'))
                 ->addDecorator('HtmlTag', array('tag' => 'li',
                                                 'id'  => $this->getName() . '-element'));
        }
    }
}

Don’t forget to add your library namespace to the auto loader

; application/configs/application.ini
 
autoloaderNamespaces.1 = "Tolerable_"

Zend_Form_Element_Submit also needs some treatment

// library/Tolerable/Form/Element/Submit.php
 
class Tolerable_Form_Element_Submit extends Zend_Form_Element_Submit
{
    /**
     * Default decorators
     *
     * Uses only 'Submit' and 'li' decorators by default.
     * 
     * @return void
     */
    public function loadDefaultDecorators()
    {
        if ($this->loadDefaultDecoratorsIsDisabled()) {
            return;
        }
 
        $decorators = $this->getDecorators();
        if (empty($decorators)) {
            $this->addDecorator('Tooltip')
                 ->addDecorator('ViewHelper')
                 ->addDecorator('HtmlTag', array('tag' => 'li'));
        }
    }
}

We’ll also need to provide our own display group class to support the desired scheme

// library/Tolerable/Form/DisplayGroup.php
 
class Tolerable_Form_DisplayGroup extends Zend_Form_DisplayGroup
{
    /**
     * Load default decorators
     * 
     * @return void
     */
    public function loadDefaultDecorators()
    {
        if ($this->loadDefaultDecoratorsIsDisabled()) {
            return;
        }
 
        $decorators = $this->getDecorators();
        if (empty($decorators)) {
            $this->addDecorator('Description', array('tag'   => 'li',
                                                     'class' => 'group_description'))
                 ->addDecorator('FormElements')
                 ->addDecorator('HtmlTag', array('tag' => 'ol'))
                 ->addDecorator('Fieldset');
        }
    }
}

as well as a custom form class

// library/Tolerable/Form.php
 
class Tolerable_Form extends Zend_Form
{
    /**
     * Default display group class
     * @var string
     */
    protected $_defaultDisplayGroupClass = 'Tolerable_Form_DisplayGroup';
 
    /**
     * Constructor
     *
     * Add custom prefix path before parent constructor
     *
     * @param mixed $options
     * @return void
     */
    public function __construct($options = null)
    {
        $this->addPrefixPath('Tolerable_Form', 'Tolerable/Form');
        parent::__construct($options);
    }
 
    /**
     * Load the default decorators
     *
     * @return void
     */
    public function loadDefaultDecorators()
    {
        if ($this->loadDefaultDecoratorsIsDisabled()) {
            return;
        }
 
        $decorators = $this->getDecorators();
        if (empty($decorators)) {
            $this->addDecorator('FormElements')
                 ->addDecorator('Form');
        }
    }
}

Now that we’ve laid the way for our custom decorator scheme, there’s only one item remaining and I can’t say that I’m proud but hey, it works.

All front facing (HTML) Zend form elements extend the empty, abstract class Zend_Form_Element_Xhtml. We’re going to override this class in its current form. This method works well if the Zend Framework is not in the application’s “library” which should have a higher “include path” setting than the framework itself. Otherwise, you’ll just have to override the existing Zend Framework file.

// library/Zend/Form/Element/Xhtml.php
 
abstract class Zend_Form_Element_Xhtml extends Tolerable_Form_Element
{
}

That’s it. Get ready to start writing leaner, meaner forms

$form = new Tolerable_Form;
$form->addElement('text', 'foo', array(
    'label'    => 'Foo',
    'required' => true
))->addElement('textarea', 'bar', array(
    'label'    => 'Bar'
))->addElement('submit', 'submit_btn', array(
    'label'    => 'Submit'
))->addDisplayGroup(
    array('foo', 'bar', 'submit_btn'),
    'baz',
    array('legend' => 'My Form')
);

Roundcube 0.3-stable on PHPsuExec hosts

I’ve been using Roundcube as my webmail client since the early alpha days and was happy to see the recent release of 0.3-stable. I did have to tweak some PHP configuration items so, for the benefit of other Roundcube users, here’s how to get it up and running on a PHPsuExec enabled host.

As outlined in a previous post, I created my custom configuration file under /home/user/etc/php.d/roundcube/custom.ini using the php_flag and php_value properties from the Roundcube .htaccess file.

display_errors              = Off
log_errors                  = On
upload_max_filesize         = 5M
post_max_size               = 6M
memory_limit                = 64M
 
zlib.output_compression     = Off
magic_quotes_gpc            = 0
zend.ze1_compatibility_mode = 0
suhosin.session.encrypt     = Off
 
session.auto_start          = 0
session.gc_maxlifetime      = 21600
session.gc_divisor          = 500
session.gc_probability      = 1
 
mbstring.func_overload      = 0

This file is then merged into the server configuration file as /home/user/etc/php.d/roundcube/php.ini.

Then you just need to edit the Roundcube .htaccess file and add

SetEnv PHPRC /home/user/etc/php.d/roundcube

PHP suExec and custom php.ini files

Lately, I’ve noticed more and more shared web hosts making the switch to running PHP under suExec. The benefits of this are:

  • PHP scripts run as the owning user
  • No more file system permission juggling
  • Scripts are generally sandboxed to the owner’s home directory

Unfortunately, this poses a problem for any custom PHP configuration changes as the usual .htaccess php_flag and php_value properties are no longer supported. The recommended method of setting a custom configuration is to place a php.ini file under each directory where required.

This method works fine for any properties set in the custom php.ini file however there appears to be one devastating omission. Server configuration properties do not cascade into the custom file. What this means is that for any property not set in your custom file, PHP will revert to the PHP default.

For example, say your host has enabled PDO (as any decent host should). The instant you introduce a custom php.ini file, PDO will be lost. Sure, you could just include the relevant extension_dir and extension properties but consider this example (from my host)

extension_dir = "/usr/local/lib/php/extensions/no-debug-non-zts-20060613"

That property is awfully specific and your custom entry wouldn’t fare too well in the event of an upgrade.

In a perfect world, the host would have compiled PHP with the --with-config-file-scan-dir=/some/dir option that allows an environmentally settable directory to be scanned for additional config files. Unfortunately, this is rarely the case.

My solution is a little hacky and hardly perfect but it certainly gets the job done for me.

1. Create your custom.ini file with the options you want. Mine is /home/user/etc/php.d/custom.ini and looks like this

register_globals = Off
magic_quotes_gpc = Off

Do not name this file php.ini.

2. Locate your host’s php.ini file. This information is available from phpinfo(). In the below examples it is /usr/local/lib/php.ini

3. Add a cron job to create a full php.ini file in a location of your choosing (/home/user/etc/php.d) that is the concatenation of the server php.ini file and your custom.ini file. The below example is set to run hourly with any errors displayed on stdout which should be mailed to the crontab owner.

0 * * * * /usr/bin/test -f /usr/local/lib/php.ini && /bin/cat /usr/local/lib/php.ini /home/user/etc/php.d/custom.ini > /home/user/etc/php.d/php.ini || echo '/usr/local/lib/php.ini NOT FOUND'

The reason for the cron job is to keep your full, custom php.ini file in sync with any server changes. The job will also inform you in the unlikely event that the host php.ini file is moved.

If you’ve got shell access, you can create this file immediately by running

cat /usr/local/lib/php.ini /home/user/etc/php.d/custom.ini > /home/user/etc/php.d/php.ini

otherwise, wait for the hour to come around in which case your cron job should have done the work for you.

4. Add the following to your web application’s .htaccess file

SetEnv PHPRC /home/user/etc/php.d

The PHPRC environment variable informs any PHP scripts of the location of the preferred php.ini file.

Enjoy.

Editor themes for Eclipse PDT – Obsidian

My PHP IDE of choice is Eclipse with the PDT plugin.

The thing is, I always feel a little snow blind after a day’s coding, staring at that white screen all day. So much so that I’ve undertaken the arduous task of tweaking my colour preferences to match the Obsidian theme found in the more recent versions of Notepad++. Why Eclipse can’t take some lead from Notepad++ and Aptana and make this process much easier, I don’t know but here’s the end result.

PHP file with Obsidian theme
PHP file with Obsidian theme

Mixed PHP and HTML file with Obsidian theme
Mixed PHP and HTML file with Obsidian theme

I’ve also created some preference files so you can play along at home. Please note that whilst I’ve made sure the preference files only contain colour data, I’ve only done minor testing but so far, everything has gone well. My setup is Eclipse Galileo with PDT 2.1 however I don’t think there’s any version specific stuff in the files.

To install the colour preferences:

  1. Download one of the below packages and extract the contents to a temporary directory
  2. Fire up Eclipse
  3. Backup your current preferences by going to File -> Export -> General -> Preferences. Follow the instructions from there to create a preferences file. Make sure you choose “Export all”.
  4. Go to File -> Import -> General -> Preferences and select the Obsidian/eclipse.epf file. Click “Finish”

I’ve also included some Aptana colourisation files for CSS and JavaScript however I haven’t yet created preferences for other Aptana editors.

Enjoy and keep watching for more themes.

Obsidian.tar.gz
Obsidian.zip

Extending Zend Framework Application Resource Plugins

With the arrival of Zend Framework version 1.8 came the application resource plugins. These nifty little classes help bootstrap your application’s resources (views, layouts, database connections, etc). The default behaviour is suitable for most needs however, occasionally, you’re going to want to perform some extra functionality.

Take for example the following use case – the standard DB resource plugin instantiates a database connection based on supplied parameters and registers it with Zend_Db_Table_Abstract::setDefaultAdapter(). What if you want to register the connection in the Zend_Registry as well? You could create a bootstrap method (_initRegistry() for example) and pull the adapter out of Zend_Db_Table_Abstract or you could simply extend the resource plugin.

// library/My/Application/Resource/Db.php
 
class My_Application_Resource_Db extends Zend_Application_Resource_Db
{
    public function init()
    {
        if (null !== ($db = $this->getDbAdapter())) {
            Zend_Registry::set('dbAdapter', $db);
        }
        return parent::init();
    }
}

This class simply adds the created database adapter to the registry before continuing on with the default behaviour.

To let the application know about your custom plugin, you simply add the plugin path to your application.ini file, eg

includePaths.library = APPLICATION_PATH "/../library"
pluginPaths.My_Application_Resource = "My/Application/Resource"

More reading is available here – http://framework.zend.com/manual/en/zend.application.theory-of-operation.html

Further examples here – http://framework.zend.com/manual/en/zend.application.examples.html

Boxen – a jQuery iframe plugin

As mentioned in a previous post, I’m a big fan of displaying small, configuration style screens in Lightbox style windows. If you’re not familiar with Lightbox, check it out here.

The thing with configuration screens is they usually have a little more to do than simply display an image or other static content. In fact, most of the ones I’ve written require some sort of AJAX functionality. Using an iframe is perfect for these scenarios as they operate as a self contained window, complete with any custom JavaScript.

I began using Cody Lindley’s ThickBox and more recently DOMWindow to load my iframes dynamically. Whist both of these plugins are great, I found they lacked the ability to truly style the boxes and extending them to add callbacks and extra features was not possible without editing the core JavaScript. I was after a simple yet expandable solution to dynamically open an iframe in a Lightbox style window.

Boxen

The boxen plugin allows you to bind a click event to any element that will open a Lightbox style iframe.

Example

    $('.boxen').boxen();
    $('#boxen_google').boxen('http://www.google.com/');
    $('#boxen_search').boxen('http://www.google.com/search', {
        urlParams: {
            q: 'More Cowbell'
        }
    });

Documentation

boxen([url or options], [options])

Arguments

url or options (optional) – String, Options

If the first parameter is a string, it is used as the URL for the iframe source. Otherwise, it is treated as the options map.

options (optional) – Options

urlParams: {},
showTitleBar: true,
showCloseButton: true,
title: null,
titleAttribute: 'title',
closeButtonText: null,
width: 600,
height: 500,
url: null,
urlAttribute: 'href',
overlayOpacity: 0.8,
overlayColor: null,
modal: false,
postOpen: function(contentAreaElement) {},
postClose: function() {}

Demo

Boxen Demo

Download

UPDATE 2009-03-20

Boxen now has the ability to open an empty canvas. Simply cancel out any URL by setting the url or urlAttribute options to null. See the demo page for a nifty Google Maps example.

UPDATE 2009-04-06

Boxen 1.2 is now IE6 compatible. Due to the lack of position: fixed, the experience may be a little jerky / flickery when the window is resized and occasionally the transparent overlay may be a few pixels too long or too short however I think this is a case of “near enough is good enough”.

Unfortunately, I’ve had to alter the Boxen “id” attribute naming convention and remove the leading underscore to get it to work in IE6. If you’ve used any custom CSS rules, you’ll need to edit them accordingly.

Also fixed in this version:

  • BIG iFrame implementation for overlaying IE6 form controls
  • Safari iFrame ID caching bug
  • Opera opacity issue

I’ve only tested this using a STRICT doctype and encourage everyone else to do so too.

UPDATE 2009-10-08

Boxen 1.3 contains the following updates:

  • Fixed “frameBorder” case sensitivity (thanks Caleb)
  • Added more streamlined way to anonymously open a Boxen window
    $.Boxen.open(null, {
        url:'http://www.google.com/',
        title:'Boxen window title'
    });
  • Now released under the MIT license

UPDATE 2009-11-30

Boxen 1.4 contains the following updates

  • Added “overlayColor” option
  • Stylesheet example now includes a “spinner” loading animation (courtesy of http://www.ajaxload.info/). Provided your iFrame / content specifies a background, the spinner will not be visible once the content loads

Internet Explorer Application Compatibility VPC Images under VirtualBox

With the release of Internet Explorer version 8, Microsoft have added yet another browser into the mix for we web developers to support.

Testing on different versions of Internet Explorer has always presented some challenges. Unlike some other browsers, it’s not possible to truly run various versions side-by-side. Some applications like Tredosoft’s Multiple IEs go some way to providing this functionality however problems still manage to present themselves.

The easiest way to really test is to use virtual machines created with specific versions of Internet Explorer running under Windows XP or Vista. Microsoft have kindly donated free, downloadable disk images to run under their Virtual PC software.

Unfortunately for some, Virtual PC only runs under Windows. I prefer Sun’s alternative so here are some instructions for installing the Internet Explorer Application Compatibility VPC Images under VirtualBox:

  1. Go here and download the VPC disk image(s) of your choice.
  2. Extract the VHD file using your favourite archive manager. You should probably read the associated documentation and EULA.
  3. Start VirtualBox and hit “New”.
  4. Choose the appropriate operating system and give your new VM a name and some RAM.
  5. On the hard disk screen, click “Existing” to open the Virtual Media Manager.
  6. Click “Add” and locate the extracted VHD file.
  7. Select the new disk image and click “Ok”, “Next” then “Finish”
  8. Start up the VM and press F8 as it boots to get into the Windows startup menu. If you don’t make it here on the first try, just wait for the BSOD then try again.
  9. Start Windows in Safe Mode. It may take a little while for your peripherals to start responding, just be patient.
  10. Ignore any new hardware wizards and open up a command prompt. Run
    sc config Processor start= disabled
    Now reboot.
  11. Again, ignore any new hardware wizards and select Devices -> Install Guest Additions. Follow the installation prompts and reboot.
  12. Once more, ignore the hardware wizards and open a command prompt. The guest additions ISO should still be mounted at D:. Run
    D:\VBoxWindowsAdditions-x86 /extract /D=C:\Drivers
  13. Now go through the new hardware wizards. For the Ethernet controller, point the wizard at C:\Drivers\x86\Network\AMD

That should be it. I’ve tested this using the Windows XP images and all has gone well.

Unfortunately, it looks like Microsoft have used the same disk UUID for each image which means VirtualBox will only allow one image at a time into the Virtual Media Manager. Hopefully this bug will be fixed shortly which should clear things up.

UPDATE

If you see a dialog on startup looking for cmBatt.sys and you don’t have a Windows CD handy, just disable the unknown battery device from Device Manager.

UPDATE 2009-05-21

Looks like the aforementioned UUID bug may be addressed in the next VBox release (2.2.3). All I can say is “thanks VirtualBox team, love your work”.

Changing layouts with Zend ContextSwitch

Ever since discovering Zend Framework’s ContextSwitch and AjaxContext action helpers, I’ve been hooked. What’s not to love about writing one controller action and having the request dictate the response format.

Whilst this generally does the job for XML, JSON and AJAX HTML snippets where any layout is disabled, sometimes you just need to wrap that content in something.

I often find myself writing small configuration screens that work nicely in modal style dialogs as opposed to a devoted web page. Generally I’ll render these screens as inline frames using my favourite JavaScript library and DOM Window (a ThickBox successor). The problem with iframes is they require a complete HTML document. ContextSwitch just wasn’t cutting it as I needed some kind of layout but obviously not the full blown, menus and all default.

The solution was to implement my own custom context, complete with a post-processing callback function to alter the layout script. Here’s how I’ve done it.

First, I added a static method to my Bootstrap class to add a suffix to the layout script name should a context be present.

public static function setLayoutContext()
{
    $layout = Zend_Layout::getMvcInstance();
    if (null !== $layout && $layout->isEnabled()) {
        $context = Zend_Controller_Action_HelperBroker::getStaticHelper('ContextSwitch')->getCurrentContext();
        if (null !== $context) {
            $layout->setLayout($layout->getLayout() . '.' . $context);
        }
    }
}

For example, given the default layout script layout.phtml and context iframe, this method will set the layout script to layout.iframe.phtml

Now I just need to add the context to the action helper. Again, in my Bootstrap class.

public static function setupActionHelpers()
{
    // This method is called during application bootstrapping
 
    // Add "iframe" context
    $iframeContext = array(
        'suffix'    => 'iframe',
        'callbacks' => array(
            'post' => array(__CLASS__, 'setLayoutContext')
        )
    );
 
    $contextSwitch = Zend_Controller_Action_HelperBroker::getStaticHelper('ContextSwitch');
 
    $contextSwitch->addContext('iframe', $iframeContext);
}

Ok, that’s all the preparation work out of the way. To add this context to a controller action, you simply, well, add it.

class MyController extends Zend_Controller_Action
{
    public function init()
    {
        $this->_helper->contextSwitch->addActionContext('config', 'iframe')
                                     ->setAutoDisableLayout(false)
                                     ->initContext();
    }
 
    public function configAction()
    {
        // Controller tasks here
    }
}

All that’s left to do is ensure your links to the context enabled controller action include the correct format parameter, for example /my/config/format/iframe or /my/config?format=iframe.

One thing I’ve noticed is that the ContextSwitch helper should be configured after your view and layout are initialised.

Thanks to Jeff Carouth for the inspiration for this post.

jQuery AJAX Autocomplete with JSON data

I recently had a requirement to implement an auto-complete input box in a dynamic form. Since I was already using jQuery, I had a look at the available plugins.

After sorting through a few, I decided on this one from Jörn Zaefferer. The only problem was, I wanted to use the wonderfully simple Zend Framework AjaxContext Action Helper to produce JSON data as my auto-complete data source. I like JSON as it’s easy to produce and easy to deal with in disparate systems (such as PHP and JavaScript). Unfortunately, Jörn’s plugin is built to work with a simple, formatted ASCII list delimited by newlines. Fortunately, he also put a lot of work into this plugin, he just hasn’t gotten around to documenting it all yet.

For this post, I’ll assume my AJAX request is for some user data out of LDAP (or a similar data store) and resembles this

{"users":[
    {"uid":"123","displayName":"User 123","mail":"123@example.com"},
    {"uid":"456","displayName":"User 456","mail":"456@example.com"},
    {"uid":"789","displayName":"User 789","mail":"789@example.com"},
]}

The secret to extending the Autocomplete plugin is the ability to overwrite certain core functions, primarily parse. The internal version of this function simply loops over each line of the returned data and “parses” it into an array of objects, each containing the following attributes:

  • data – the entire entry
  • value – the default display value
  • result – the data to populate the input element on selection

You can overwrite this by passing your own parse function as part of the options object to autocomplete. Mine also includes a custom formatItem function for displaying each entry.

var acOptions = {
    minChars: 3,
    max: 100,
    dataType: 'json', // this parameter is currently unused
    extraParams: {
        format: 'json' // pass the required context to the Zend Controller
    },
    parse: function(data) {
        var parsed = [];
        data = data.users;
 
        for (var i = 0; i < data.length; i++) {
            parsed[parsed.length] = {
                data: data[i],
                value: data[i].displayName,
                result: data[i].displayName
            };
        }
 
        return parsed;
    },
    formatItem: function(item) {
        return item.displayName + ' (' + item.mail + ')';
    }
};

The autocomplete plugin will now accept and parse JSON data.

Another thing I wanted to do with this page was to show one value but use another. In this case, I only want to display the user names and email addresses and use the UID without displaying it or putting it into the form field. This is where having each item available as a JSON object comes in very handy.

For this trick, I’ll add a hidden input field and place the UID into that when an item is selected. Here’s my form field

<input type="text" id="user_id" name="user_id">

and here’s the JavaScript

// autocomplete options as above
 
jQuery(document).ready(function($) {
    $('#user_id')
        .autocomplete('/path/to/ajax/data/source', acOptions)
        .attr('name', 'display_name')
        .after('<input type="hidden" name="user_id" id="ac_result">')
        .result(function(e, data) {
            $('#ac_result').val(data.uid);
        });
});

The reason I changed the name attribute of the original input and gave it to the new hidden one is so I can still submit the form normally and work with the submitted “user_id” value.

The Autocomplete result function receives the selected item’s data property when an item is selected. With the above configuration, this is the actual JSON object