Sat 07 May 2011

More Power to ExpressionEngine URLs

Please Excuse the Tone. :(

I wrote this when I was younger and, arguably, an asshole (pardon my French). There may still be technical content of note in here, so I'm keeping it up, but please ignore the harsh and unnecessary tone.

When playing "contract engineer", you sometimes have to jump in and work with a less than ideal codebase. This was the case on a recent project I helped out on; the codebase is an install of ExpressionEngine 2 (EE2), a publishing system (CMS) developed by the fine people at EllisLab, favored by web designers all over the place. While I personally find it too limiting for my tastes (I suspect this is due to my doing less design based work these days), I can understand why people choose to work with it - highly sensible defaults with a pleasing overall control panel design that you can sell to customers. We can all agree that not reinventing the wheel is a good thing.

That said, I would be lying if I didn't note that there are a few things about EE2 that bug me. I'll write about them each in-depth in their own articles; the focus of this one is on the somewhat limiting URL structure that EE2 enforces on you, as well as how to get around this and obtain a much higher degree of flexibility while still re-using your same EE2 essentials (templates, session handling, etc).

The Scenario to Fix

The way that EE2 handles URL routing is pretty simple, and works for a large majority of use cases. The short and sweet of it is this:

http://example.com/index.php/\ template_group/template/

That url will render a template named "template" that resides inside "template_group", taking care of appropriate contextual data and such. Let's imagine, though, that for SEO-related purposes you want a little more dynamism in that url - the template_group should act as more of a controller, where it can be re-used based on a given data set. What to do about this...

Wait! EE2 is CodeIgniter!

This is where things get interesting. EE2 is actually built on top of CodeIgniter, an open source PHP framework maintained by EllisLab. It's similar to Ruby on Rails in many regards.

That said, if you're new to web development and reading this, please go learn to use a real framework. Learning PHP (and associated frameworks) first will only set you up for hardships later.

Now, since we have a framework, we have to ask ourselves... why doesn't EE2's setup look like a CodeIgniter setup? Well, EE2 swaps some functionality into the CI build it runs on, so things are a bit different. This is done (presumably) to maintain some level of backwards compatibility with older ExpressionEngine installations.

Exposing the Underlying Components

The first thing we need to address is the fact that the CodeIgniter router functions are being overridden. If you open up the main index.php file used by EE2 and go to line 94-ish, you'll find something like the following:

<?php

// ...

/*
 * ---------------------------------------------------------------
 *  Disable all routing, send everything to the frontend
 * ---------------------------------------------------------------
 */
$routing['directory'] = '';
$routing['controller'] = 'ee';
$routing['function'] = 'index';

// ...

You're gonna want to just comment those lines out. What's basically going on there is that this is saying "hey, let's just have every request go through this controller and function", but we really don't want this. By commenting these out, the full routing capabilities of CodeIgniter return to us.

One thing to note here is that if our desired route isn't found, ExpressionEngine will still continue to work absolutely fine. This is due to a line in the config/routes.php file:

<?php

// ...
$route['default_controller'] = "ee/index";
$route['404_override'] = "ee/index";

// An example of a route we'll watch for
$route['example/(:any)'] = "example_controller/test/$1";

// ...

The default controller, if no route is found matching the one we've specified, is the EE controller, so nothing will break.

Controllers and Re-using Assets

So now that we've got a sane controller-based setup rolling, there's one more problem to tackle: layouts and/or views. Presumably all your view code is built to use the EE2 templating engine; it'd be insane to have to keep a separate set of view files around that are non-EE2 compatible, so let's see if we can't re-use this stuff.

A basic controller example is below:

<?php

if(!defined('BASEPATH')) exit('This cannot be hit directly.');

class Example_controller extends Controller {
    function __construct() {
        parent::Controller();
        
        /* Need to initialize the EE2 core for this stuff to work! */
        $this->core->_initialize_core();
        $this->EE = $this->core->EE;
        
        /* This is required to initialize template rendering */
        require APPPATH.'libraries/Template'.EXT;
    }
    
    function test($ext) {
        echo $ext;
    }
}

/* End of file */

Now, viewing "/example/12345" in your browser should bring up a page that simply prints "12345". The noteworthy pieces of this happen inside the construct method; there's a few pieces that we need to establish in there so we have a reference to the EE2 components.

Now, to use our template structures once more, we need to add in a little magic...

<?php
    
    // ...
    
    private function _render($template_group, $template, $opts = array()) {
        /* Create a new EE Template Instance */
        $this->EE->TMPL = new EE_Template();
        
        /* Run through the initial parsing phase, set output type */
        $this->EE->TMPL->fetch_and_parse($template_group, $template, FALSE);
        $this->EE->output->out_type = $this->EE->TMPL->template_type;
        
        /* Return source. If we were given opts to do template replacement, parse them in */
        if(count($opts) > 0) {
            $this->EE->output->set_output(
                $this->EE->TMPL->parse_variables(
                    $this->EE->TMPL->final_template, array($opts)
                )
            );
        } else {
            $this->EE->output->set_output($this->EE->TMPL->final_template);
        }
    }
    
    // ...

This render method should be added to the controller example above; it accepts three parameters - a template group, a template name, and an optional multi-dimensional array to use as a context for template rendering (i.e, your own tags). If the last argument confuses you, it's probably best to read the EE2 third_party documentation on parsing variables, as it's actually just using that API. There's really less black magic here than it looks like.

With that done, our final controller looks something like this...

<?php
    if(!defined('BASEPATH')) exit('This cannot be hit directly.');

    class Example_controller extends Controller {
        function __construct() {
            parent::Controller();
        
            /* Need to initialize the EE2 core for this stuff to work! */
            $this->core->_initialize_core();
            $this->EE = $this->core->EE;
            
            /* This is required to initialize template rendering */
            require APPPATH.'libraries/Template'.EXT;
        }

        private function _render($template_group, $template, $opts = array()) {
            /* Create a new EE Template Instance */
            $this->EE->TMPL = new EE_Template();
            
            /* Run through the initial parsing phase, set output type */
            $this->EE->TMPL->fetch_and_parse($template_group, $template, FALSE);
            $this->EE->output->out_type = $this->EE->TMPL->template_type;
            
            /* Return source. If we were given opts to do template replacement, parse them in */
            if(count($opts) > 0) {
                $this->EE->output->set_output(
                    $this->EE->TMPL->parse_variables(
                        $this->EE->TMPL->final_template, array($opts)
                    )
                );
            } else {
                $this->EE->output->set_output($this->EE->TMPL->final_template);
            }
        }

        function test($ext) {
            return $this->_render('my_template_group', 'my_template', array(
                'template_variable_one' => 'SuperBus',
                'repeatable_stuff' => array(
                    array('id' => 1, 'text' => 'This'),
                    array('id' => 2, 'text' => 'Will'),
                    array('id' => 3, 'text' => 'Be'),
                    array('id' => 4, 'text' => 'Repeatable'),
                )
            ));
        }
    }

    /* End of file */

Awesome! Now what?

Please go use a more reasonable programming language that enforces better practices. While you're at it, check out one of the best web frameworks around, conveniently written in said reasonable programming language.

Of course, if you're stuck using PHP, then make the most of it I suppose. If this article was useful to you, I'd love to hear so!

Ryan around the Web