Extending the Expression Engine channel module

Posted on in expressionengine

Overview

If you’ve been using Expression Engine for a while, you’ll know that the Channel Module is the heart of creating site templates. It comes with a long list of parameters allowing you to control which entries are displayed. Sometimes though, you just need a little bit more.

At that point, templates can become messy. {if}{if:else} tags creep in with {embeds} between them, PHP gets enabled and performance can become a problem.

And if you ever asked the question “can PHP be enabled on input AND output”, you wouldn’t be alone.

NOTE: This article assumes you’re using EE2 and have read and understood the plugin development documentation.

A real-world example

I’ve done this so many times for so many different reasons it’s hard to pick a single example, but here is a typical template requirement that gives us problems with a standard {exp:channel:entries} tag.

  1. We want to filter entries to multiple categories based on a URL segment, but those categories must be validated by a set of rules
  2. We need PHP in the template to apply some layout formatting to the entries

Let’s enable output PHP in the template to clear off point 2. It’s the best solution as it keeps the layout logic all in one place. So now we can’t have input PHP, how do we solve point 1?

We could inject the categories directly into the {channel:entries} tag by having a URL structure like:

http://example.com/group/template/1|2|3

the template tag looks a bit like this

{exp:channel:entries category="{segment_3}"}

That’s going to work a treat, but it has potential disadvantages. What if you want to exclude certain categories and how to stop the user injecting them? What if you hate that URL structure and want to improve it? What if you want to include other categories based on user selections? There are endless reasons why simply passing on user input is not desirable.

Let’s assume we’ve decided that we want custom URLs such as

http://example.com/group/template/list1 and
http://example.com/group/template/list2

The final URL segment will tell us which categories should be loaded.

A plugin to solve the problem

We going to make our own plugin here that gives us a new Template tag to replace {exp:channel:entries}. We start with a basic plugin structure with all the required elements and a couple more.

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); $plugin_info = array( 'pi_name' => 'My_entries', 'pi_version' => '1.0' ); require_once(PATH_MOD.'channel/mod.channel.php'); class My_entries extends Channel { /** * Constructor function */ function __construct() { // run standard Channel module constructor parent::Channel(); } }

There are two important lines in there:

require_once(PATH_MOD.'channel/mod.channel.php'); class My_entries extends Channel {

This means that all the code we’re writing is in addition to everything the channel module provides. So now we can change our template code to {exp:my_entries:entries} and it will behave exactly like {exp:channel:entries}

Now it’s time to insert our extra functionality. First off will be to establish what the allowed categories are. For the purposes of this example I’m going to hard-code them. In reality, they would likely be looked-up from the database based on certain rules. We’ll insert our options just after the start of the class like so:

class My_entries extends Channel { // possible category sets we will allow // In a real example these would be looked-up dynamically private $cat_groups = array( 'list1' => '1|2|3', 'list2' => '4|5|6', 'list3' => '7|8|9' ); // our default categories // (also likely to be looked-up dynamically) private $default_cats = '1|2|3';

We’ve now got the possible URL segments defined as ‘list1′, ‘list2′ and ‘list3′ along with their corresponding categories – in the format the entries tag expects.

Finally we need to add a function to do all our work

// ----------------------------------------------------------------- /** * entries * * Adjust channel:entries parameters before it runs * * @return string */ public function entries() { // set our default categories $categories = $this->default_cats; // establish the group the user has requested $group = $this->EE->uri->segment(3); // if it's valid, override our defaults if(FALSE != $group && isset($this->cat_groups[$group])) { $categories = $this->cat_groups[$group]; } // set the template params $this->EE->TMPL->tagparams['category'] = $categories; // return to main channel module code return parent::entries(); }

Not too much going on here. The name of the function – entries – relates to how it will be called from the template. It could be called almost anything but I like to stick with ‘entries’ for consistency.

Next we set the $categories variable to our defaults and then override it if – and only if – a valid selection has been passed via segment 3 of the URL.

The key line is

$this->EE->TMPL->tagparams['category'] = $categories;

tagparams relate to the long list of parameters you can set for the channel module. You can set as many of them as you like using a plugin like this.

The final line sends us back to the main channel module to carry on the work.

The full plugin will look like this:

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); $plugin_info = array( 'pi_name' => 'My_entries', 'pi_version' => '1.0' ); require_once(PATH_MOD.'channel/mod.channel.php'); class My_entries extends Channel { // possible category sets we will allow // In a real example these would be looked-up dynamically private $cat_groups = array( 'list1' => '1|2|3', 'list2' => '4|5|6', 'list3' => '7|8|9' ); // our default categories // (also likely to be looked-up dynamically) private $default_cats = '1|2|3'; // ----------------------------------------------------------------- /** * Constructor function */ function __construct() { // run standard Channel module constructor parent::Channel(); // if we were looking up categories dynamically, we might do it here } // ----------------------------------------------------------------- /** * entries * * Adjust channel:entries parameters before it runs * * @return string */ public function entries() { // set our default categories $categories = $this->default_cats; // etablish the group the user has requested $group = $this->EE->uri->segment(3); // if it's valid, override our defaults if(FALSE != $group && isset($this->cat_groups[$group])) { $categories = $this->cat_groups[$group]; } // set the template params $this->EE->TMPL->tagparams['category'] = $categories; // return to main channel module code return parent::entries(); } }

This then gets saved as /system/expressionengine/third_party/my_entries/pi.my_entries.php

your template code may look like this

{exp:my_entries:entries channel="channel_name"} .. your markup ... {/exp:my_entries:entries}

So what have we ended up with? First off we have a very small plugin. It will run quickly and is easy to maintain because there’s hardly anything to it. If something changes in Expression Engine and the plugin becomes incompatible (it happens a lot), it can be fixed in no time. You can’t always say the same of some complex third-party systems.

We also have very simple template markup. By extending the channel module rather than adding a separate plugin (and so an extra template tag) we’ve also reduced the workload on the template engine (and every little helps).

Manipulating output

It’s not just the tagparams you can set using this method. You can also manipulate the template data before and/or after the main Channel module has run. Here are some very basic pointers. All of this code would replace the line:

return parent::entries();

Manipulation before Channel:entries

// manipulate before channel:entries $tagdata = $this->EE->TMPL->tagdata; // do something to $tagdata here // send back to main module $this->EE->TMPL->tagdata = $tagdata; return parent::entries();

Manipulation after Channel:entries

I had a client recently who had a design that needed ampersands displayed in a different font throughout the site. Painful to manage something like that when adding content so I came up with this:

// manipulate after channel:entries $output = parent::entries(); // do something to $output here $search = array(' & ', ' &amp; '); $replace = array(' & ', ' &amp; '); $output = str_replace($search, $replace, $output); return $output;

The (non-semantic) <b> tag was then styled using CSS.

What would YOU do?

The sky’s the limit here. In terms of tag parameters, anything you might want to set yourself from the template parameters can be accomplished. In terms of manipulation of template data or output, anything you can do with PHP can be accomplished.

Some examples of things I’ve done by extending the channel module include:

  • Setting the entry_id parameter based on user data / member group / url segments etc
  • Setting the status parameter based on user data / member group / url segments etc
  • Setting date ranges for entries based on all sorts of things
  • Creating custom conditional variables for use in templates, avoiding the use of resource-hungry advanced conditionals or embeds
  • Repeating the same channel:entries tag multiple times with slightly different parameters

I’d love to hear what you’re doing.

Comments