Building a Plugin: Replacing Magpie

0 comments

Writing add-ons for ExpressionEngine isn’t terribly difficult once you know what you’re doing. However, before you know what you’re doing it can be a frustrating tangle of guess and check. I vividly remember writing my first plugin, and then my first extension, trying to wrap my head around how hooks work. Then I tried my hand at writing a Multiple Site Manager compatible extension and there was more confusion there.

I want to walk you through writing a plugin, the best first step towards building ExpressionEngine add-ons. This tutorial will be a start-to-finish adventure, starting with the plugin skeleton and working our way to putting it on GitHub for everyone to download. I’ll explain my methods and my approach to building plugins, and by extension, software in general. Additionally, this particular plugin will be replacing the aging Magpie plugin, since Magpie is no longer being maintained.

Getting Started

The first thing we’ll need is an ExpressionEngine install. My recommendation is to create a new ‘sandbox’ on your local machine, but a remote server should work equally well.

Once you have the ExpressionEngine install all setup, work your way into the system/expressionengine/third_party/ directory and create the following items:

  • rss_reader/
  • rss_reader/pi.rss_reader.php
  • rss_reader/libraries/

Notice that the base folder and the PHP file have the same names, except that the PHP file has the prefix pi. to denote that it’s a plugin. We’re creating the libraries directory now, but we won’t be working with it immediately.

Open up, the pi.rss_reader.php file in your favorite text editor and add the following skeleton code:

<?php
if ( ! defined('BASEPATH')) exit('No direct script access allowed');

$plugin_info = array(
    
'pi_name'           => '',
    
'pi_version'        => '',
    
'pi_author'         => '',
    
'pi_author_url'     => '',
    
'pi_description'    => '',
    
'pi_usage'          => Rss_parser::usage()
);

Class 
Rss_parser {

    
public function __construct()
    
{
        $this
->EE   =&ampget_instance();
    
}

    
// -----------------------------------------------------------

    /**
     * Plugin Usage
        * 
        * @return void
     */
    
public function usage()
    
{
        ob_start
(); 
        
?>

        <?php
        $buffer 
ob_get_contents();
        
ob_end_clean(); 

        return 
$buffer;
    
}
}

/* End of file pi.rss_parser.php */
/* Location: ./system/expressionengine/third_party/rss_parser/
pi.rss_parser.php */ 

There’s a few things to take note of in this code:

  • The $plugin_info array at the top needs be filled out.
  • ‘pi_usage’ is pointing at the usage() method at the bottom and anything in that method will be shown on the Plugin’s page in the Control Panel.
  • There are only two included methods (__construct() and usage()), but that doesn’t mean you can’t add more.

With that done, you’re ready to go.

(Not) Reinventing the Wheel

With our skeleton code set, we’re ready to code, right? Not quite. Our next step is figuring out how we’re going to solve the problem. We’re replacing Magpie, a RSS parsing library that’s gone long in the tooth, but we still need the same functionality.

With that in mind, I’m going to use SimplePie, a fast and simple RSS parser built as a successor to Magpie. However, we won’t be downloading the version they offer on their site, we’ll need to go to their GitHub page and download the latest stable development version from there. That version has some compatibility fixes that we’ll need.

Taking a look at the current Magpie plugin, we see that all of the Magpie libraries were just copied and pasted into the plugin file, making it more of a pain to upgrade the library later. Instead of following suit, unzip the package and move the following files and directories to the libraries directory we created earlier:

  • LICENSE.txt
  • idn/
  • SimplePie/
  • SimplePie.php
  • SimplePieAutoloader.php

Later if the SimplePie library is upgraded, it’s just a matter of replacing a few files.

Documentation First

With the skeleton copied and the SimplePie library downloaded and added, we’re almost ready to start writing the code, but first we need to document. This is the step that most developers hate and save until later, something that must be done, but not enjoyed. However, writing documentation first is one of the best things you could ever do to your code. Instead of writing code and making decisions at the same time, you just program to the documentation.

I always start at the highest level and work my way down, and that means doing the following documentation before ever writing a line of code:

  1. Plugin usage() documentation
  2. Method DocBlocks
  3. Inline comments in the methods
usage() Documentation

The usage() documentation is there to give your customers an idea of how to use your plugin: what’s available to them, an example of how to use it, and any caveats. You can find it under:

Addons → Plugins →

I’ve always found that writing the usage() documentation is the best way to plan out how your plugin works. The usage() documentation should cover a few things:

  • The tag(s) available with this plugin
  • The parameters for any tags provided
  • The variables inside the tags separated into single tags and tag pairs
  • A solid example of the tag

So let’s think about how many tags we need and what they will look like. All plugins can have more than one tag (e.g. {exp:xhtml:full} and {exp:xhtml:light}), but they can also be simple and have only one tag (e.g. {exp:xml_do_not_encode}). You’ll notice that the XHTML plugin has three segments: exp, xhtml, and full. xhtml denotes the XHTML plugin—much like xml_do_not_encode does for the XML Encode plugin—and full and light denote methods within that plugin. If there is no third segment, then it’s assumed that the __construct() method should be called.

For our plugin, we only have one tag, so we only need the two segments and the __construct() method:

{exp:rss_parser} 

Next, we need to figure out what parameters we’ll take with this tag. We have a good idea of the functionality we need since we’re replacing existing functionality. We need to provide:

  • The ability to take a RSS feed and print information about the feed and the items in the feed
  • The ability to cache the RSS feed
  • The ability to limit the number of items shown
  • The ability to offset the items, so we don’t have to start with the first item in the feed

That means we need four parameters to meet those requirements and match the Magpie plugin’s features:

  • url: The URL of the feed to parse
  • cache: The amount of time in minutes to parse the RSS feed
  • limit: The number of items from the feed to show
  • offset: The number of items to ignore from the beginning of the feed

Then we need some way of actually displaying the information associated with the feed, so we’ll need to talk about variables, both single and pairs. First, there’s some information about the feed that we might want in a template:

  • feed_title
  • feed_link
  • feed_description
  • feed_copyright
  • feed_language
  • logo_url
  • logo_title
  • logo_link
  • logo_width
  • logo_height

So we have some information about the feed itself, and then any details about an optionally included logo. We then need to think about the items themselves. To begin with, items will have to be a tag pair since we’re dealing with multiple items. On top of that, each item can have multiple authors and categories, so those are tag pairs within tag pairs:

  • feed_items
    • item_title
    • item_link
    • item_authors
      • author_name
      • author_email
      • author_link
    • item_date
    • item_categories

      • category_name
    • item_description
    • item_content

Then we need an example, but nothing too complicated, but something folks can copy and paste to get them started is usually my goal:

{exp:rss_parser url='http://expressionengine.com/feeds/rss/full/' 
    
limit='10' refresh='720'}
    
<ul>
        
{items}
            
<li><a href='{link}'>{title}</a></li>
        
{/items}
    
</ul>
{/exp:rss_parser} 

The last thing worth including is a changelog, so your customers have a good idea of what’s changing with each new version.

With all of that in mind, here’s our final documentation:

RSS Parser
===========================

There is only one tag for the RSS Parser:

{exp:rss_parser url='http://expressionengine.com/feeds/rss/full/' 
    
offset='5' limit='10' refresh='720'}


Parameters
===========================

The tag has three parameters:

url The URL of the RSS or Atom feed.
limit Number of items to display from feed.
offset Skip a certain number of items in the display of the feed.
refresh How often to refresh the cache file in minutes.  The 
    plugin 
default is to refresh the cached file every three hours.


Single Variables
===========================

feed_title
feed_link
feed_copyright
feed_description
feed_language

Both RSS 2.0 
and Atom 1.0 feeds can have a 'feed logo'The following 
variables can be used to display the logo
:

logo_title
logo_url
logo_link
logo_width
logo_height


Pair Variables
===========================

There are three pair variables available{feed_items}
{item_categories}, and {item_authors}Both {item_categories} 
and {item_authors}are only available within {feed_items}.

{feed_items}
---------------------------

The {feed_items} variable contains all of the items found 
within the feed
:

item_title
item_link
item_dateuses standard ExpressionEngine date 
    formatting 
(e.g{date format='%F %d %Y'})
item_description
item_content

{item_authors}
---------------------------

The {item_authors} variable contains information about all of the 
authors of a particular item
Each author has three single 
variables associated with it
:

author_email
author_link
author_name

{item_categories}
---------------------------

The {item_categories} variable contains all of the categories that a 
feed item has been assigned
Each category has one useful variable:

category_name


Example
===========================

{exp:rss_parser url='http://expressionengine.com/feeds/rss/full/' 
    
limit='10' refresh='720'}
    
<ul>
        
{feed_items}
            
<li><a href='{item_link}'>{item_title}</a></li>
        
{/feed_items}
    
</ul>
{/exp:rss_parser}


Changelog
===========================

Version 1.0
---------------------------

Initial release and (mostlyfeature parity with Magpie plugin 
Method DocBlocks

With the usage defined, it’s only now that we start thinking about how the code will actually work. When programming, I always write a DocBlock comment for each and every method so when I look at it six months later I have a good idea of what it does, what arguments it needs, and what it returns. Here’s an example:

/**
 * Example DocBlock explaining the following method
 * 
 * @param param_type $param_name Description of $param_name
 * @return Description of what's returned
 */
public function function_name($param_name

What methods will we need? We have two methods that we already have that could use simple DocBlocks: __construct() and usage(). Beyond that we know we’ll be caching things and that involves creating a directory, so we might want to abstract that to a separate method:

/**
 * Check to make sure the cache exists and create it otherwise
 */
private function _check_cache() 

Beyond that, I’m not sure what methods we’ll need, but we can come back to that later.

Inline Comments

My final step of documentation is writing inline comments in each method describing the flow of the method. For example, in the _check_cache() method, I might have something like this:

// Check to see if rss_parser cache directory exists

// If it does not exist, try to create it

// If I can't create it, throw an error

// If I can create the directory, or if it already exists,
// then we're done 

In __construct() I’d write this:

// Fetch tag parameters and set defaults

// Bring in SimplePie

// Create the feed in SimplePie

// Establish the cache

// Check to see if the feed was initialized, if so, deal with the type

// Parse the variables
    // Get Feed Information
    // Get Logo Information
    // Get Feed Items
        // If there are no feed items return no_results
        // Make sure to obey the offset and limit parameters
        // Get Item Authors
        // Get Item Categories

// If the feed couldn't be fetched, put the error into the 
// Template log and return no_results 

Letting the Code Write Itself

Having written all of that documentation, now the only thing left to do is write code. Most of the hard decisions have been made and at this point, we’ll only be hindered here or there by errors that can be quickly remedied.

Something to keep in mind when writing code is that while we defined the methods and DocBlocks earlier, we aren’t limited to those few methods. In my case, while writing this plugin, I noticed that it made a lot of sense to create an additional method that handled retrieving all of the feed item information. That way the __construct() method is smaller and easier to maintain.

If you want to see the code, take a look at the source on GitHub. The gist of what I’m doing is getting all of the feed information using SimplePie and putting it into an associative array with additional arrays nested in it.

Using the Template Class

The only tricky part remaining is working with the Template Class and it’s not too bad. The array that we need to build looks something like this:

array(
    array(
        
'feed_title'    => 'Feed Title',
        
'feed_link'     => 'http://...',
        
[...]
        
'feed_items'    => array(
            array(
                
'item_title'    => 'Item Title',
                
'item_link'     => 'http://...',
                
[...]
            
),
            array(
                
[...]
            
)
        )
    )

The most confusing part about that array is that there seems to be an unnecessary array wrapped around the whole thing. However, that’s there because a plugin could in theory return multiple items, multiple feeds for example. In our case we’re only dealing with one feed, so we only have one array within our main array.

Once we have our array, it’s a matter of parsing the variables and giving it to the plugin so that it makes it to the front end. Parsing the variables is easy enough, pass the array along with the data within your tag to parse_variables():

$this->EE->TMPL->parse_variables(
    
$this->EE->TMPL->tagdata
    array(
$content)
); 

Note our use of the Template class variable $this->EE->TMPL->tagdata. That contains everything between the {exp:rss_parser} tags in the template.

The other part of the template class we’ll need to use is the no_results method. In the event that feed is empty or if there is an error when retrieving the feed, we want to show no_results:

$this->EE->TMPL->no_results(); 

That will look at the tagdata variable and parse the if no_results} conditional while removing everything else. Also, it’s worthwhile to log errors with the feed. I chose to log them to template debugging:

$this->EE->TMPL->log_item('RSS Parser Error: '.$feed->error()); 

log_item() is a method of the Template class and $feed->error() is a method from SimplePie. If there are any errors they will show up in template debugging at the bottom of the page if it’s enabled.

The last thing you’ll need to do is pass whatever you’re sending to the template to $this->return_data so ExpressionEngine knows what to put in between the tags:

if ($success)
{
    [
...]
    $this
->return_data $this->EE->TMPL->parse_variables(
        
$this->EE->TMPL->tagdata
        array(
$content)
    );
}
else
{
    [
...]
    $this
->return_data $this->EE->TMPL->no_results();

Download It

At this point, you should have a good idea of what it takes to write a plugin, at least the parts that work with ExpressionEngine. Everything in between on the other hand is up to you to fill out. Feel free to abstract more or less, document more (but not less), or change everything about this tutorial to write a completely different plugin. If you need help getting started with a plugin skeleton, check out pkg.io which is a great little service put together by Pascal Kriete and Greg Aker that creates a lot of the basic files for you.

If you want to download the plugin or just view the final source, check it out on GitHub and feel free to fork it and send a pull request if you want to make any changes.

Comments & Feedback

You must be logged in to comment on this blog post