Developers

This page currently covers the creation of custom plugins. Information on other development topics may be added depending on interest. If you’d like to see a particular topic discussed, feel free to e-mail us at team [at] phergie [dot] org.

Getting the Source Code

If you’d like to to install Phergie as a PEAR package, visit the Users page for instructions. If you’d prefer to work with the most up-to-date source code, version control for the Phergie project is hosted on GitHub. You can go to its project page for the address to clone the repository and Github Help for more information on how to do this if you’re unfamiliar with git.

API Documentation

As you read this page, you may find the API documentation hosted on this web site to be helpful. If you’d like to maintain your own local copy of the documentation, see the Users page for how to install a package from the Pirum server used to distribute Phergie. The API documentation is contained in the Phergie_Docs package.

Class Files

Each Phergie plugin is represented by a single class file. This file exists within the directory Phergie/Plugin and is named using a short name for the plugin, such as AutoJoin.php. Start by creating an appropriately named file for your plugin in this location.

Classes

Within this file, create a class with a name corresponding to the path to the class file. For example, the class name Phergie_Plugin_AutoJoin corresponds to the path Phergie/Plugin/AutoJoin.php. The class should extend Phergie_Plugin_Abstract.

Event Handlers

The IRC protocol is based around events. Appropriately, plugins function mainly by intercepting and responding to these events. If you open the file Phergie/Plugin/Abstract.php, you’ll notice that it contains a number of method stubs, or method declarations that don’t actually contain any code. These are called automatically by the Phergie core libraries when specific events occur.

If you don’t care about certain events, you don’t need to do anything; the appropriate method stubs for those events in Phergie_Plugin_Abstract are executed, causing no response to be issued for those events. If you do care about a particular event, you simply override the method stub in your own plugin class and include logic for the response you’d like it to issue.

Responding to Events

The bot essentially acts as an IRC client. In general, you will want it to respond to events by issuing IRC commands. If you open the file Phergie/Driver/Abstract.php, you’ll notice a lot of do*() methods. With the exception of doConnect(), which is called internally by the bot, all other do*() methods in this class can be called by plugins.

This doesn’t happen directly, though. The __call() implementation in Phergie_Plugin_Abstract is what actually intercepts calls to those do*() methods from your plugin. Data from these calls are stored in the $events property of the plugin, which references a central instance of Phergie_Event_Handler.

Once all plugins have had an opportunity to respond, the bot obtains the commands they’ve issued from that event handler object and passes them along to the driver to be sent back to the IRC server. The reasoning behind this approach of buffering commands is that it allows for things like plugins being able to add, remove, modify, or reorder commands before they go to the server.

Order of Events

IRC events aren’t the only type that can be intercepted. The bot adds several more that allow you to inject your own logic into various points of the event processing cycle. Below is a description of these events in the order in which they occur.

  1. onLoad() – Immediately after plugins are created, this event handler is called to give them an opportunity to check for runtime dependencies. In the event of a dependency not being satisfied, the fail() method defined in Phergie_Plugin_Abstract should be called.
  2. onConnect() – After all plugins are loaded, a connection to each IRC server is established. This event handler is executed immediately afterward. The new connection is available to plugins via their $connection property. This is a good handler to use for checking and acting on any optional dependencies, which may not have been loaded yet when onLoad() was executed for any given plugin.
  3. onTick() – A loop is executed as long as any connections to IRC servers are maintained to continuously poll them for new events. At the beginning of each loop iteration and before this polling takes place, this event handler is called. It’s handy for implementing logic to be executed on a timed interval.
  4. preEvent() – Executed when an event is received but before it is processed, regardless of what type of event it is.
  5. preDispatch() – Executed immediately after all plugins have executed their event handler for the specific type of event that has been received and before their commands to be sent in response have been dispatched to the driver.
  6. postDispatch() – Executed after commands sent by plugins in response to the received event have been dispatched to the driver.

Resources

There are two event-specific properties in Phergie_Plugin_Abstract, $connection and $event, that are automatically populated by internal code.

Consult their respective classes for more information about the data that these properties make available to you.

Additionally, there are three other properties that are not event-specific: $config, $plugins, and $events.

  • $config is an instance of Phergie_Config that represents the combined contents of any configuration files specified when the bot is executed. It implements the ArrayAccess interface and as such makes setting values accessible using array syntax. A method of the base plugin class, getConfig(), provides a convenient shorthand that also allows you to specify a default value for a setting when it isn’t assigned one in configuration files.
  • $plugins is an instance of Phergie_Plugin_Handler and provides a container for all loaded plugins. Plugins can be added, removed, retrieved, and detected. Because its class implements IteratorAggregate, $plugins can be used as the subject of a foreach loop to iterate over all the plugins it contains. Lastly, its __call() implementation allows method calls on $plugins to proxy that method call to each plugin.
  • $events is an instance of Phergie_Event_Handler. As previously described in the Responding to Events section, it provides a container for commands initiated by plugins. Like Phergie_Plugin_Handler, this class also implements IteratorAggregate, so the events it contains can be iterated. They can also be retrieved via its getEvents() method or replaced entirely with its replaceEvents() method. For most plugins, however, the __call() implementation provided by Phergie_Plugin_Abstract should be sufficient.

Dependencies

The code sample below shows how to indicate that your plugin depends on another plugin where the string 'Dependency' is intended to contain the short name of the plugin on which your plugin depends (i.e. ‘Dependency’ corresponds to the class Phergie_Plugin_Dependency in the file Phergie/Plugin/Dependency.php).

public function onLoad()
{
    // This forces the plugin to be loaded now.
    // An exception is thrown if it can't be loaded.
    $this->plugins->getPlugin('Dependency');
}

public function onConnect()
{
    // All plugins that will be loaded are loaded at this point.
    // This call checks to see if the dependency is among them.
    if ($this->plugins->hasPlugin('Dependency')) {
        // If you need to use the plugin instance for something, do this
        $plugin = $this->plugins->getPlugin('Dependency');
        // Implement any needed integration logic here
    }
}

Custom Events

One way to allow other plugins to integrate with yours is to have your plugin issue a call to a method prefixed with 'on' on its $plugins property. The plugin handler will internally iterate over all loaded plugins and call that method on each plugin if it’s defined.

A good example of this is the core Command plugin, which detects commands issued via IRC message events and informs other plugins by calling onCommand*() on $plugins where * is the command that was issued. The Join, Part, and Quit commands are examples of plugins that make use of this.

Best Practices

  • Avoid storing shorthand references to plugins in class or instance properties. This can cause unexpected results if plugins like the Reload plugin, which swaps out an old instance of a plugin for a new one in the plugin handler, are present. Instead, fetch the plugin from the handler each time it’s needed. If you want a shorthand reference to it, store it in a local variable in the method.
  • Avoid storing values of configuration settings in class or instance properties. This should only be done if you want to retain the value present when the bot is first loaded, regardless of whether any other plugin changes it later. Instead, retrieve the setting from the configuration object each time you need it.

Unit Testing

Unit tests for both core components and contributed plugins are written on the PHPUnit unit testing framework, specifically the 3.5.x branch. They can be found in the Tests directory of the git repository. This directory includes a PHPUnit XML configuration file, which enables you to simply execute the phpunit executable from that directory with no arguments to execute the entire test suite or the relative path to a specific PHP class file to execute.

Phergie plugin test cases extend the class Phergie_Plugin_TestCase, which provides a number of conveniences including those shown in the list below. The source code for this class is fairly readable and helps to increase familiarity with these features.

  • Automatic instantiation of the plugin to be tested in the test case class property $this->plugin – note this assumes that the test case class uses the same name as the plugin class with a Test postfix (ex: for a plugin class Phergie_Plugin_Name, the test case class would be named Phergie_Plugin_NameTest)
  • Automatic creation and injection of mock objects for all dependencies, which are fetched from separate methods defined in the parent class Phergie_TestCase that can be overridden in subclasses if needed
  • Easy creation of paths to files used by the plugin
  • Commonly used assertion methods for testing emission or non-emission of events and requirement or removal of plugins
  • Creation of expendable in-memory SQLite databases based on existing database files

Existing plugin test case classes serve as examples of how all this functionality works and how to write test case classes. Ideally, when writing a plugin test case class, Xdebug is used in conjunction with PHPUnit to generate code coverage reports to confirm that all plugin code is covered by the tests.