Migrating your Dancer plugin to Dancer 2, the smooth way

Now that Dancer 2 has come a long way and is stable enough to run applications, it’s time to start focusing on migrating the ecosystem. By ecosystem, I mean all the plugins and engines (template, logger, session) that live on the CPAN.

But we want to be as smooth as possible with our fellow users and of course, with plugin developers. So we want a plugin to be able to run under Dancer 1 or 2, with as minimal code tweaks as possible in the plugin’s code. All this in order to allow a smooth transition process towards Dancer 2.

My last patches sent to both Dancer 1 and 2 allows plugin authors to adapt their code so that the plugin can be agnostic of the version of Dancer behind them.

I thought everything was in place to allow a plugin to run smoothly with Dancer 1 and 2, but David – who was working on porting Dancer::Plugin::Database to Dancer 2 – asked me about the hooks.

With Dancer 1, all the hooks are managed by a singleton so registering a hook and executing it is as easy as

Dancer::Factory::Hook->instance
  ->install_hooks("some_hook_for_my_plugin");

...

Dancer::Factory::Hook->instance
  ->execute_hooks("some_hook_for_my_plugin");

Yes it’s simple, but the reason why it’s simple is because all the hooks are shared among all the components of the application. Which is one of the design limitation we wanted to solve with Dancer 2.

In Dancer 2, it’s way much better, we use massively Moo and its roles system in order to do as much code-reuse as possible and allow a clean and extensible design.

In 2, a plugin is a role that consumes the DSL role (to extend it) which itself consumes the “Hookable” role (which provides hook features).

When an application uses a plugin, its DSL (of the application) gets extended by consuming the plugin’s DSL.

Also, in Dancer 2, we have some kind of a namespace for hooks in order to do automatic resolution of hooks ownership (I won’t go into details about that part, it’s out of the scope of this post). Anyway, keep in mind a hook has a “real name” and a “user name” in Dancer 2.

The “user name” (which is called an alias) is the one you’re used to see in Dancer 1, like “before_template”.

To declare its hooks, a plugin must implement a method named “supported_hooks” which should return the list of hooks (real names) that it claims to support.

The plugin is also able to define user names with the method “hook_aliases”.

That’s the DSL of the plugin that holds the hooks, not a singleton living in a shared space accessible for the whole process. Way much cleaner, but of course, a bit more complex for the developer.

Let’s take an example of a dummy plugin that sets its own hook, in Dancer 1:

package Dancer::Plugin::Something;
use Dancer::Plugin;

# claim the hook we support
Dancer::Factory::Hook->instance
  ->install_hooks("somehook");

...

register some_keyword => sub {
    my $self = shift;
    ...

    Dancer::Factory::Hook->instance
      ->execute_hooks("somehook");
};

...

Now, Without my last patch, a plugin author who wanted to implement that very same plugin in Dancer 2 would have had to do like so:

package Dancer::Plugin::Something;
use Dancer::Plugin;

# claim the hook we support
sub supported_hooks { "plugin.somehting.some_hook" }
sub hook_aliases    { "somehook" => "plugin.somehting.some_hook" }

register some_keyword => sub {
    my $self = shift;
    ...

    $self->execute_hooks('somehook');
};

...

As you can see, we are far from what is needed in Dancer 1. I let you imagine the code needed to maintain compatibility with both Dancer versions…

To solve this issue, I’ve added two new keywords in the Dancer::Plugin’s export table, register_hook and execute_hooks which encapsulate for you the nasty bits exposed above.

I’ve implemented them in both Dancer 1 and Dancer 2 so a plugin author can now do the following:

package Dancer::Plugin::Something;
use Dancer::Plugin;

# claim the hook we support
register_hook "somehook";

register some_keyword => sub {
    execute_hooks 'somehook';
};

...

And voila, it works with both version!

Thanks David for poking me about that!
Note:
The patch for Dancer 2 has been pushed to Github, the one for Dancer 1 as well, and a new release is expected soon to be pushed to CPAN.