Day 14 - Using Catalyst Models Externally and Multiple Configuration Files

Today we look at how to solve a common problem: accessing a Catalyst model from a batch script outside Catalyst while sharing some configuration settings such as the database connection string.

UPDATE: As of 2008, there is a new module which avoids the "kludgy" solution mentioned below: Config::JFDI.

Sample Application

This article uses an enhanced version of the ExtJS sample application written for the Catalyst Advent Calendar 2007 day 1 article and you may wish to read the code along with this article.

The app is accessible at http://www.dragonstaff.co.uk/extjs/home

The code can be viewed online at http://dev.catalystframework.org/repos/Catalyst/trunk/examples/ExtJS.

You can check out the code under Linux with

 $ svn co http://dev.catalystframework.org/repos/Catalyst/trunk/examples/ExtJS

Under Windows use TortoiseSVN http://tortoisesvn.tigris.org/ to check it out from the same URL.

Sort out dependencies

 $ perl Makefile.PL
 $ make

To run the app locally

 $ perl script/extjs_server.pl

The problem

If you write a simple Catalyst application, you can put the database connection right in the model class. E.g. in the sample ExtJS application in lib/ExtJS/Model/ExtJSModel.pm you might have:

 package ExtJS::Model::ExtJSModel;

 use strict;
 use base 'Catalyst::Model::DBIC::Schema';

 __PACKAGE__->config(
     schema_class => 'ExtJS::Schema',
     connect_info => [
         'dbi:SQLite:extjs.db',
         'myusername', 'mypassword',
         { AutoCommit => 1 },
     ],
 );

That just works.

However, say you are using live and test environments and they have different paths to the database, or different SQL usernames and passwords. Now you would rather hold the database connection information in a separate configuration file that you can change depending whether it's the live or test area.

Also, as well as a master application configuration file like conf/extjs.pl you may want a local site file like conf/extjs_local.pl that lets you override settings in your development area. Commonly you might want to turn on extra debug or turn off flags to stop your application doing things like sending emails. It's annoying debugging web user registration and receiving 100 emails a day from yourself.

The Solution

Change the main Catalyst configuration and the Model configuration to run from several separate configuration files. Split out the model database connection parameters.

Catalyst Configuration

To handle multiple configuration files we need to replace ConfigLoader with ConfigLoader::Multi in our main Catalyst perl module and let it do the hard work for us.

In lib/ExtJS.pm

 use Catalyst qw/ -Debug 
   ConfigLoader::Multi
   ...
   /;

 use Config::Any::Perl;

 our $VERSION = '0.01';

 __PACKAGE__->config( name => 'ExtJS' );
 __PACKAGE__->config( file => __PACKAGE__->path_to('conf') );

 __PACKAGE__->setup;

This looks for files below your application root directory in the conf sub-directory named after the lower case version of your application name, in this case 'extjs', with an optional '_local' after it, followed by a suffix from any of the known configuration file extensions / formats:

* .yml or .yaml / YAML
* .pl or .perl / Perl
* .xml / XML
* .json or .jsn / JSON
* .ini / INI
* .cnf or .conf / Config::General

The equivalent regex pattern for this application is something like conf/extjs(_local)?\.{yml,yaml,pl,perl,xml,json,jsn,ini,cnf,conf}

You need to remember to load a reader for the file format you are using. In this application I've used .pl Perl configuration files. I write my code in Perl and like to have my configuration in Perl files too. It's nice to write config like:

  session => {
     expires => (60*60*24 * 1), # 1 day in seconds
  },

Yeah, baby!

Here the line use Config::Any::Perl; adds .pl file reading support. Note that the order of loading would be extjs.pl followed by extjs_local.pl , so the local configuration file settings override the default ones in extjs.pl.

You can read more details about ConfigLoader::Multi here https://metacpan.org/module/Catalyst::Plugin::ConfigLoader::Multi. It's a nice piece of work. Also see https://metacpan.org/module/Catalyst::Plugin::ConfigLoader::Manual.

Set Up Configuration Files

We've set up our Catalyst application to expect multiple configuration files, so we'd better create them:

conf/extjs.pl

 # extjs.pl
 {
   name => 'ExtJS',
   default_view => 'TT',
   static => {
     include_path => [ '__path_to(root/static)__' ],
   },
   overrideme => 'first value',
 }

Note that Catalyst replaces __path_to() with the application root directory as a prefix.

conf/exjs_local.pl

 # extjs_local.pl
 {
   dummy => 'someval',
   overrideme => 'second value',
 }

That 'overrideme' value should override the one in extjs.pl. We'll check later that it has.

conf/extjs_model.pl

 # extjs_model.pl
 {
   # model DBI connection data
   'Model::ExtJSModel' => {
     schema_class => 'ExtJS::Schema',
     connect_info => [ 'dbi:SQLite:extjs.db', '', '', { AutoCommit => 1 } ],
    #connect_info => [ 'DBI:mysql:database=extjs;host=localhost', 'username', 'password', {} ], # mysql connection string sample
   },
 }

Here's our model connection data separated out. Note that we could have more than one model in here. For example, you may have an authentication database holding user and session, and a separate application database holding the other tables.

Add Debugging

In lib/ExtJS.pm add near the top

 use Data::Dump qw(dump);

and then add a debug line after the setup call like this:

 __PACKAGE__->setup;
 $ENV{CATALYST_CONF_DEBUG} && print STDERR 'cat config looks like: '. dump(__PACKAGE__->config) . "\n";# . dump(%INC)."\n";

Now if you run your application like this (Linux)

 $ CATALYST_CONF_DEBUG=1 perl script/extjs_test.pl /

or (Windows)

 C:\mydir> SET CATALYST_CONF_DEBUG=1
 C:\mydir> perl script\extjs_test.pl /

you will see the contents of your loaded configuration prettily dumped to the output.

This makes it easy to spot configuration errors. You can spend a long time re-running an application wondering why it doesn't work only to find out some vital configuration setting is not being loaded, so don't forget to check.

At this point you may want to try running the ExtJS sample app with this debug flag to verify that the configuration key 'overrideme' is indeed set to 'second value'.

Model Configuration Loader

The final step is to make our Model class(es) work with the separate model configuration file we created. The following is a rather kludgy manual solution that could do with a more elegant solution tied to ConfigLoader::Multi but it works with Catalyst today (2007/12/12).

lib/ExtJS/Model/ExtJSModel.pm

 use Catalyst qw/ ConfigLoader /; # gives us __PACKAGE__->config->{'home'}
 use Config::Any::Perl;
 use Path::Class;

 my $cfg;
 eval { $cfg = ExtJS->config; }; # this succeeds if running inside Catalyst
 if ($@) # otherwise if called from outside Catalyst try manual load of model configuration
 {
   my $cfgpath1 = Path::Class::File->new( __PACKAGE__->config->{'home'},
     'conf', 'extjs_model_local.pl' )->stringify;
   my $cfgpath2 = Path::Class::File->new( __PACKAGE__->config->{'home'},
     'conf', 'extjs_model.pl' )->stringify;
   my $cfgpath = -r $cfgpath1 ? $cfgpath1 
               : -r $cfgpath2 ? $cfgpath2
               : die "cannot read $cfgpath1 or $cfgpath2";
   delete $INC{$cfgpath}; # workaround so older Config::Any::Perl will work when reloading config file
   $cfg = Config::Any::Perl->load( $cfgpath );
 }

 # test we have got our model config in
 defined $cfg->{'Model::ExtJSModel'} || die "Catalyst config not found";

 # put model parameters into main configuration
 __PACKAGE__->config( $cfg->{'Model::ExtJSModel'} );

As the comments suggest, if you're running inside the Catalyst framework the configuration just works. However, if you're running from an external batch progam then you have to read the config the hard way and you don't get niceties like the '__path_to(root/static)__' we mentioned before in the 'Set Up Configuration Files' section.

Here I've allowed for reading either a conf/extjs_model_local.pl or conf/extjs_model.pl config file to define the database connection string. Then in your development area you can use an extjs_model_local.pl to override and point to a different database.

You can change the way this code works if you prefer to merge local and master config files (like ConfigLoader::Multi) or if you want to handle them in a more generalised way using a regex file glob().

The reload workaround is not necessary with the latest version of Config::Any::Perl but I mention it because it fixes an issue when using multiple models in earlier versions.

External Batch Script

Finally we get to our external batch script, the reason we went through all the above. In the example app look at t/10_schema.t and script/dump_bookings.pl . These demonstrate how to write an external test script and batch program that access a Catalyst model class.

To run the test do

 $ prove -Ilib t/10_schema.t

or

 $ perl -Ilib t/10_schema.t

Looking at the batch script

script/dump_bookings.pl

 #!/usr/bin/perl
 # script/dump_bookings.pl
 # a really simple example of accessing a Catalyst application's Model
 # from an external script
 # it lists all the booking records in the database

 use strict;
 use warnings;

 # allow for running from root directory or from script directory
 use lib qw / lib ../lib /;

 use ExtJS::Model::ExtJSModel;
 use ExtJS::Schema;

 # demonstrate picking up database connection info
 my $connect_info = ExtJS::Model::ExtJSModel->config->{connect_info};
 print "connecting schema to ".$connect_info->[0]."\n";

 # connect to the Catalyst schema
 my $schema = ExtJS::Schema->connect( @$connect_info );

 # show the model classes available
 my @sources = $schema->sources();
 print 'found schema model sources :-  ' . join(", ",@sources) . "\n";

 # list all bookings
 print "listing all bookings ordered by po_ref\n";
 my $rs = $schema->resultset('Booking')->search({}, { order_by => 'po_ref' });
 for my $row ( $rs->all )
 {
   print "\nBooking ". $row->id ." - PO Ref " . $row->po_ref . "\n";
   for my $col ( sort $row->columns )
   {
     next if $col eq 'id' || $col eq 'po_ref';
     printf "  %-20s: %-50s\n", $col, $row->get_column($col);
   }
 }

Try running it

 $ perl script/dump_bookings.pl

The model connection information is automatically read by the model class from conf/extjs_model.pl . All we have to do is use the Booking schema class to read all the bookings.

That's all for now. I hope this gives you a clearer idea how to play with Catalyst configuration files and model classes.

AUTHOR

peterdragon - Peter Edwards <peter@dragonstaff.co.uk>

http://perl.dragonstaff.co.uk/