The Horde Application Framework

Chuck Hagenbuch

The Horde Project

Summary

What is Horde?

Origins

IMP 1.0.0 also somehow contained hardcoded links to my old .ml.org dynamic domain, an empty faq.shtml (all of the HTML that wasn't embedded into PHP code was in .shtml files), and a src/ directory with .phps symlinks to all of the PHP files.

Back then (this was 1998), releasing something new and unknown on Freshmeat meant some people actually noticed it - there weren't nearly as many posts as there are now.

Poof!

Horde Today

The line count obviously isn't a measure of quality, but the 53-fold increase is impressive to me from a "what have I done?" standpoint. Also it doesn't count templates, CSS, docs, etc.

Why: Libraries

Why: Standards

Why: Backend Independence

Why: Applications

Why: Internationalization

Just looking at this slide title you can understand where "i18n" came from. It's not some standard with the number 18 - it's "i", followed by 18 other letters, followed by by "n". Same for l10n - localization has 10 letters between "l" and "n".

What do I get by?

Here are the basic things an application gets from Horde:

What makes a Horde application?

There is a skeleton application providing the basic common structure. But all you really need is:

That's all?

Technically, yes. But to take full advantage of Horde, you'll want:

registry.php

Here's an application's registry.php entry:

$this->applications['whups'] = array(
    'fileroot' => dirname(__FILE__) . '/../whups',
    'webroot' => $this->applications['horde']['webroot'] . '/whups',
    'name' => _("Tickets"),
    'status' => 'active',
    'provides' => 'tickets',
    'menu_parent' => 'devel',
);

The fileroot setting tells Horde where the application - in this case Whups - lives on the filesystem. The __FILE__ constant is the current file; dirname() strips off the filename and turns it into just a directory.

This means that the location is by default relative to registry.php - under the horde/ directory - which is almost always right. But it's configurable just in case.

The webroot setting tells Horde where the application lives relative to the webserver's document root. Usually this is predictable as well, but you might want to give an application its own domain - for example, http://cvs.php.net/, which is powered by Horde and Chora.

The name setting surprisingly sets the human-readable name of the application. The _() is an alias for gettext(), which translates the name into other languages.

The status setting tells Horde what kind of application this is. Possible statuses are:

  • inactive
  • hidden
  • notoolbar
  • heading
  • block
  • admin
  • active

The provides setting tells Horde if the application provides any APIs. In this case Whups provides the tickets API, which allows for adding and listing tickets. In turn, Horde knows that if it gets a request for a tickets/search method, it should pass it along to Whups.

The menu_parent setting is just for the sidebar - it tells Horde what item to make the application of a child of. This lets you customize the menu to your heart's content. This can be left out or set to null for top-level items.

How about something cool?

Let's add task alarms to the sidebar menu. Here's the plan:

Creating the Block

Horde Blocks are very easy to write. Just give them a name, a type, and fill in a few functions.

For tree blocks, which add things to Horde_Tree objects, all we need is a _buildTree() function.

    function _buildTree(&$tree, $indent = 0, $parent = null)
    {
        ...

Here's the full code:

<?php

 $task) {
            $differential = $task['due'] - $now;
            if ($differential >= -60 && $differential < 60) {
                $title = sprintf(_("%s is due now."), $task['name']);
            } elseif ($differential >= 60) {
                $title = sprintf(_("%s is due in %s"), $task['name'], Nag::secondsToString($differential));
            }

            $url = Util::addParameter(Horde::applicationUrl('view.php'),
                                      array('task' => $task['task_id'],
                                            'tasklist' => $task['tasklist_id']));
            $tree->addNode($parent . $taskId,
                           $parent,
                           $task['name'],
                           $indent + 1,
                           false,
                           array('icon' => 'alarm.png',
                                 'icondir' => $GLOBALS['registry']->getImageDir(),
                                 'title' => $title,
                                 'url' => $url));
        }

        if ($registry->get('url', $parent)) {
            $purl = $registry->get('url', $parent);
        } elseif ($registry->get('status', $parent) == 'heading' ||
                  !$registry->get('webroot')) {
            $purl = null;
        } else {
            $purl = Horde::url($registry->getInitialPage($parent));
        }
        $pnode_params = array('url' => $purl,
                              'icon' => $registry->get('icon', $parent),
                              'icondir' => '');

        $pnode_params = array('url' => $purl,
                              'icon' => $registry->get('icon', $parent),
                              'icondir' => '');
        $pnode_name = $registry->get('name', $parent);
        if ($alarms) {
            $pnode_name = '' . $pnode_name . '';
        }

        $tree->addNode($parent, $registry->get('menu_parent', $parent),
                       $pnode_name, $indent, false, $pnode_params);
    }

}

Adding it to registry.php

Tree blocks are easy to add into (or take out of) registry.php:

$this->applications['nag-alarms'] = array(
    'status' => 'block',
    'app' => 'nag',
    'blockname' => 'tree_alarms',
    'menu_parent' => 'nag',
);

That's it!

Thanks! Any Questions?

Resources