The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

WWW::Mechanize::Plugin::Cookbook - how to write plugins for WWW::Mechanize::Pluggable

DESCRIPTION

This document describes what a WWW::Mechanize::Pluggable plugin is, how they work in connection with the base module, and gives examples of how one would design a new plugin.

    This cookbook addresses the current state of the Pluggable interface; future versions are expected to greatly streamline the process of creating plugins and hooks.

PLUGIN BASICS

A plugin is basically as specially-named package that is automatically loaded by a parent class. This document outlines the interface between WWW::Mechanize::Pluggable and its plugin classes.

Flow of Control

When WWW::Mechanize::Pluggable is loaded, it searches @INC for modules whose names begin with WWW::Mechanize::Plugin and calls import for the package, using the arguments supplied on WWW::Mechanize::Pluggable's own use line. This allows you to parameterize the plugins if you wish.

When a WWW::Mechanize::Pluggable object is instantiated, its new method calls each of the plugins' init method. Typically, init() exports methods back into the caller's namespace, and also calls pre_hook and post_hook to wrap any of WWW::Mechanize's methods it desires.

When a WWW::Mechanize method is called, WWW::Mechanize::Pluggable's AUTOLOAD takes control. It calls any pre-hooks that have been installed for the method; if any of them return a true value, the actual method call is skipped. WW::Mechanize::Pluggable then calls the method (if it should) using the same context in which the method was originally callled, saving the return value. The post-hooks are then called, and the return value from the method is returned to the original caller.

What you can do

Essentially, you now have complete control over what any method in the base class does. You can

  • alter the parameter list

  • process the call yourself

  • conditionally get involved, or not

  • post-process the results after the call

API TO WWW::MECHANIZE::PLUGGABLE

import

Called as import($class, %args).

This routine is optional; it is called when your plugin is loaded by WWW::Mechanize::Pluggable. You can use this to parameterize your plugin via arguments on the use statement.

It's recommended that you supply arguments as key-value pairs; this will make it possible for WWW::Mechanize::Pluggable to remove the "used-up" parameters from the c<use> line by returning the keys you want to have removed.

Here's a sample import method:

  sub import {
    my($class, %args) = @_;
    if defined(my $value = $args{'mine'}) {
      if (_is_appropriate($value)) {
        # do whatever ,,,
      }
    }
    return ("mine");
  }

This looks for the mine parameter on the use. It processes it as appropriate and returns the key so that WWW::Mechanize::Pluggable will delete it.

init

Called as init($pluggable).

The init method allows your plugin a chance to export subroutines and store information appropriate for its proper functioning in the parent WWW::Mechanize::Pluggable object. It also can be used to set up pre-hooks and post-hooks for methods.

    Note that at present it isn't possible to add hooks for methods installed by other plugins; a future release of this software may be able to do this.

Because other plugins will be doing the same thing, it's important to choose unique method names and field names. It's proabably a good idea to prefix field names with the name of your plugin, like _MyPlugin_data.

It's possible that we may change the interface in a future release of WWW::Mechanize::Pluggable to support "inside-out" objects (see http://www.windley.com/archives/2005/08/best_practices.shtml for an example).

Sample init function:

  sub init {
    my($parent_object, %args) = @_;
    $parent_object->{_myplugin_foo} = "my data";
    *{caller() . '::myplugin_method'} = \&my_implementation;
    $parent_object->pre_hook('get', sub { &my_prehook(@_) } );
    $parent_object->post_hook('get', sub { &my_prehook(@_) } );
    my @removed;
    if ($args{'my_arg'}) {
       # process my_arg
       push @removes, 'my_arg';
    }
    @removed;
  }

The anonymous subroutine wrapping the hook setup currently is necessary to prevent the hook from being called during its installation; this needs to be fixed. The anonymous subroutine works for the moment, and will work in future releases, so go head and use it for now.

Also note that we have the same kind of interface that we do in import; you can parameterize a particular plugin by putting the parameters (key=>value-style) on the new and then processing them in init, and deleting them after processing by returning the list of names.

pre_hook

Called as $parent_object-pre_hook('method_name", $subref)>.

Installs the referenced subroutine as a pre-hook for the named method. Currently, only WWW::Mechanize methods can be hooked; future releases may allow methods supplied by plugins to be hooked as well.

post_hook

Called as $parent_object-pre_hook('method_name", $subref)>.

Installs the referenced subroutine as a post-hook for the named method. Currently, only WWW::Mechanize methods can be hooked; future releases may allow methods supplied by plugins to be hooked as well.

YOUR CODE

Since pre-hooks and post-hooks are all about getting your code involved in things, this section details how all that works.

Prehooks and posthooks

Called as your_hook($pluggable, $internal_mech, @args).

This is the subroutine that you passed a reference to in the call to either pre_hook or post_hook. It can do anything you like; it has access to both the \WWW::Mechanize::Pluggable object and to the internal WWW::Mechanize object, as well as to the parameters with which the method was called.

If your code is a pre-hook, it can cause WWW::Mechanize::Pluggable to skip the method call altogether by returning a true value.

Sample pre-hook:

  sub my_prehook {
    my($pluggable, $mech, @args) = @_;
    
    # We'll assume that this is a hook for 'get'.
    if ($args[0] =~ /$selected_url/) {

      # alter the URL to what we want
      $args[0] =~ s/$what_we_dont_want/$what_we_do/;
    }
    # force another try with the altered URL.
    $pluggable->get(@args);

    # don't actually do the get with the old URL.
    return 'skip';
  }

We used this approach because the interface currently doesn't allow us to alter the parameter list; this is something we probably should do in the next release.

RECIPIES

Adding a new acessor

To avoid doing a lot extra monkey coding, Class::Accessor::Fast is highly recommended.

  package WWW::Mechanize::Plugin::MyPlugin;
  use base qw(Class::Accessor::Fast);
  __PACKAGE__->mk_accessors(qw(foo bar baz));

You can now use the newly-created accessors any way you like; often you'll use them to store data for other methods that are exported to WWW::Mechanize::Pluggable.

Adding a new method

This is done (for the moment) by using a construct like this:

  *{caller() . '::new_method'} = \&localsub;

This would call any subroutine or method call to new_method via the Mech::Pluggable object to be dispatched to localsub in this package.

Replacing a method

In init, install a pre_hook for the method which does something like this:

  sub init {
    pre_hook('desired_method', sub { \&substitute(@_) });

  sub substitute {
    my($pluggable, $mech, @_) = @_;
    # Do whatever you want;
    return "skip";
  }

Note the anonymous sub construct in the call to pre_hook. This is necessary because the construct \&substitute tries to call substitute() immediately, which we do not want.

We return "skip" as a mnemonic that a true value causes the real call to be skipped.

Retrying a method call

This is done with a prehook to count how many times we've tried to retry an action, and a posthook to

  • take whatever action is needed to set up for the retry

  • call back() on the Mech object

  • repeat the last action again on the Mech object

  • up the count of tries

The prehook is needed to keep the retry from going into an infinite loop.

CREDITS

The Perl Advent Calendar (http://www.perladvent.org/2004/6th/) for bringing Module::Pluggable to my attention.

Damian Conway, for showing us how to do things that Just Work.

Andy Lester, for WWW::Mechanize.