Day the Fifth: Export Hooks

Export Hooks

Top Close Open

The Badger::Exporter modules makes it easy to define export hooks for your modules. An export hook is a piece of code which gets run when the module is used with a particular option.

use Your::Module option => 'value';

Hello World

Top Close Open

Consider the following trivial module which defines a hello() exportable subroutine.

package Acme::Hello;
use base 'Badger::Exporter';
our $EXPORT_ANY = 'hello';

sub hello {
    print "Hello World!\n";
};

1;

The module can be used like this:

use Acme::Hello 'hello';
hello();                                # Hello World!

Salut le Monde!

Top Close Open

Now let's say you want to localise your module so that it can generate a welcome message in different languages. Here's version 2.

package Acme::Hello;
use base 'Badger::Exporter';

our $EXPORT_ANY = 'hello';
our $LANGUAGE   = 'english';
our $MESSAGES   = {
    czech     => 'Ahoj, svet!',
    bengali   => 'Shagatam Prithivi!',
    english   => 'Hello World!',
    french    => 'Salut le Monde!',
    german    => 'Hallo Welt!',
    hindi     => 'Shwagata Prithvi!',
    italian   => 'Ciao Mondo!',
    norwegian => 'Hallo Verden!',
    portugese => 'Ola mundo!',
    spanish   => '!Hola mundo!',
    swedish   => 'Hejsan värld!',
    turkish   => 'Merhaba Dünya!',
};

sub hello {
    print $MESSAGES->{ $LANGUAGE }, "\n";
};

sub language {
    my $class = shift;

    if (@_) {
        die "Sorry, I don't speak $_[0]" 
            unless $MESSAGES->{ $_[0] };
        $LANGUAGE = shift;
    }
    return $LANGUAGE;
}

1;

We now define our greeting messages in the $MESSAGES hash array and use the $LANGUAGE package variable as the index into it. We also define a language() class method which can be used to get or set the value for $LANGUAGE. This adds some error checking to ensure that the $LANGUAGE chosen is one that we have a message defined for.

use Acme::Hello 'hello';

hello();                                # Hello World!

Acme::Hello->language('french');
hello();                                # Salut le Monde!

Acme::Hello->language('spanish'); 
hello();                                # !Hola mundo!

Acme::Hello->language('japanese');      # Sorry I don't speak japanese

Adding an Export Hook

Top Close Open

We can streamline the selection of a language by providing an export hook for the Acme::Hello module. Export hooks are defined using the appropriately named $EXPORT_HOOKS package variable. We add the following to Acme::Hello.

our $EXPORT_HOOKS = {
    language  => sub {
        my ($class, $target, $symbol, $symbols) = @_;
        my $language = shift @$symbols || die "No language specified";
        $class->language($language);
    },
};

This defines a language export hook which we can use like this:

use Acme::Hello 'hello', language => 'french';

hello();                                # Salut le Monde!

The language option now triggers the export hook we defined above. The first argument passed to the handler is the class name of Acme::Class (although bear in mind that you might want to subclass this module at some point in the future, in which case $class will be the subclass name). The second argument is the package that the module is being imported into (main in this example). The third argument, $symbol contains the word language, and the final argument $symbols is a reference to a list of remaining symbols. In this case, the list contains just one item, 'french', which we remove (shift) from the list and store in the $language variable. The final step is to call the language() class method passing $language as an argument.

Of course, we could have just set $LANGUAGE directly at this point, but calling the language() method gives us the extra error checking that will alert the user to an invalid language selection.

Argument Count

Top Close Open

We can streamline our export hook definition a little by having Badger::Exporter provide us with the single argument we need instead of the $symbols list reference. Here the export hook is defined using a list reference. The first item is a subroutine reference as before, the second item is the number of additional arguments it expects. Our language hook requires just one.

our $EXPORT_HOOKS = {
    language  => [
        sub {
            my ($class, $target, $symbol, $language) = @_;
            $class->language($language);
        },
        1   # one argument
    ],
};

If you don't mind sacrificing a bit of readability then you can make that more compact:

our $EXPORT_HOOKS = {
    language => [ sub { $_[0]->language($_[3]) }, 1 ],
};

If you've got several class methods like language() that you want to expose using export hooks then you can handle them all using a single handler. The trick here is to use the third argument ($_[2]/$symbol) as the method name. Let's assume that we've added verbose() and polite() class methods that we want to hook up to the corresponding export options.

our $EXPORT_HOOKS = {
    language => [ \&_export_hook, 1 ],
    verbose  => [ \&_export_hook, 1 ],
    polite   => [ \&_export_hook, 1 ],
};

sub _export_hook {
    my ($class, $target, $symbol, $argument) = @_;
    $class->$symbol($argument);
}

# don't forget to define the verbose() and polite() class methods...

Now we can use the module like so:

use Acme::Hello
    'hello',
    language => 'german',   # calls Acme::Hello->language('german')
    verbose  => 1,          # calls Acme::Hello->verbose(1)
    polite   => 1;          # calls Acme::Hello->polite(1)

Catch-All Handler

Top Close Open

You can also define a catch-all handler which is called when the module is used with an unrecognised option. This is known as an export fail handler and is declared using the $EXPORT_FAIL package variable. The arguments are exactly the same as for regular export hooks.

our $EXPORT_FAIL = sub {
    my ($class, $target, $symbol, $symbols) = @_;
    print "$symbol is not a valid option for $class\n";
};

The handler will be called once for each unrecognised option. The $symbols argument is a reference to a list of remaining symbols and you can remove items from this list to use as arguments.

Fork Me on Github