The method of the setup method
Load-time setup of application variables depends on the availability of the configuration options, but we often have the requirement of being able to defer these to later. But how do we do this?
A common requirement
Many applications set up variables on load-time:
package MyApp; use Dancer2; my $dsn = config->{'dsn'}; my $schema = MyApp::Schema->connect($dsn); ...
(If you're using the Dancer2::Plugin::Database, a similar thing will happen behind the scenes.
The problem we have with this is that it requires the information to
be available in the configuration (accessed via the config
keyword)
before loading the application.
use MyApp; # config() will be called
What happens when we want to set up the variables beforehand?
I can do that, you think to yourself. Here's the code:
use Dancer2; # for config() keyword use MyApp; config->{'dsn'} = $new_dsn; # too late, hot plate!
Nope. Not going to work. When you call use MyApp
, it will already
use the config
keyword to get the DSN.
Alright, so you decide to change the configuration before you load the second application. Let's try that.
use Dancer2; config->{'dsn'} = $new_dsn; use MyApp;
But that's not going to work, because use
is compile-time, not
run-time. The order is actually as following:
use Dancer2; # happens first config->{'dsn'} = $new_dsn; # happens last use MyApp; # happens right after "use Dancer2"
You might think you could get around this with BEGIN
by forcing the
changes to happen before it loads the application:
BEGIN { use Dancer2; config->{'dsn'} = $new_dsn; use MyApp; }
Sure. That will control the order of events by forcing the config
statement to get called at compile-time, and because it's one line before
the use MyApp
line, it will be called before it. Oh wait...
Now you've hit a major design issue with Dancer2. We no longer play
with globals because globals are evil. This means that as soon as you
called use Dancer2
, we created an application based on your package
name.
When you change the configuration (using the config
keyword), you
change the configuration of the first application built. When you called
use MyApp
, Dancer2 had created yet another application, solely for
the MyApp package. This is fully intended and is a major leading
design for Dancer2.
But how do we still handle this reasonable requirement?
Moving to run-time
Instead of trying to elevate the setup code to compile-time, why not just push the application setup to run-time instead?
What if we loaded the code and imported it in runtime?
package MyApp; use Dancer2; my $dsn; sub setup { $dsn = config->{'dsn'}; # do something with $dsn get '/' => sub {...}; }
Now we just call setup
method when we want to
start registering routes. It's a good start. Let's make it accept
parameters:
package MyApp; use Dancer2; my $dsn sub setup { my $opts = shift; $dsn = $opts->{'dsn'} || config->{'dsn'}; # do something with $dsn get '/' => sub {...}; }
Now we can call it from some other environment:
# mytest.t: ... use MyApp; MyApp->setup({ dsn => 'dbi:SQLite:dbname=:memory:' });
We can even set up our own schema object if we put that functionality
in our setup
method:
my $test_dsn = 'dbi:SQLite:dbname=t/corpus/test.sqlite'; my $schema = MyApp::Schema->connect($test_dsn); MyApp->setup({ schema => $schema }); $schema->deploy; ...
The future
Since this is such a useful bootstrapping pattern, we are still considering adding this as a built-in feature in Dancer2. While you can roll your own this easily, we could envision making this even easier.
Imagine having a bootstrap
method which could be run before the routes
are registered or when you actually call to_app
, with optional
bootstrap callbacks. It is possible and we might just blog about it
in the future.
Meanwhile, Dancer applications have just enough magic to do great things while still allowing you to make use of Perl to get around load-order and encapsulation issues.
Author
This article has been written by Sawyer X for the Perl Dancer Advent Calendar 2014.
Copyright
No copyright retained. Enjoy.
2014 // Sawyer X <xsawyerx@cpan.org>