Marcus Ramberg responded to my post on How I Use Catalyst, and I’d like to respond to a few points he made.

Marcus wrote:

I disagree that $schema->resultset(‘Person’) is a significant improvement on $c->model(‘DBIC::Person’).

Me too! I don’t think the former is a significant improvement over the latter. They are, after all, more or less the same. The one big problem is that the latter version uses a nonexisting DBIC::Person namespace. There are no DBIC classes anywhere in the app. I think the model version would be much better if it was just written as $c->model('Person').

Marcus also points out that the model layer lets you configure multiple models and access them in a unified way. That is indeed nice. Unfortunately, that has the problem of tying yourself to Catalyst’s config, which is problematic for reasons I already described. Similarly, the unified layer only exists inside Catalyst, which is really only accessible during a web request. So now we’re stuck with recreating all of this if we need to access our models outside of a web request.

The long-term Catalyst roadmap includes the much-talked-about application/context split. Once this is done, presumably you will be able to access the application, which I take to mean config and models, outside of the context (of a web request). Once that is in place, I think many of my objections will go away. Unfortunately, for now I have to write my own application/context splitting code.

I created a new website as a fun little personal project, She Said What?!

It was a fun experiment both in minimal web design, and also in minimal code. I can update it from the command line just by typing:

ssw 'A quote goes here|and commentary goes here'

This adds a quote to the quote “database”, which is just a directory of timestamped flat files on my desktop. Then it regenerates the site as static HTML and pushes it to the live server.

The code is in my mercurial repository for anyone who might care.

Now that I’ve written about My Way of the Webapp and what Catalyst really is, I’ll explain what I don’t like about Catalyst.

I’m not going to talk about things I know the Catalyst developers are aware of. In particular, the use of subroutine attributes for dispatching is horrible, and they know it. I’m excited to see CatalystX::Declare, since something like that should be the future of Catalyst controllers. Another well-known misfeature is the rampant use of subclassing for plugins and the lack of well-defined APIs. Yuval Kogman explained why this is so problematic very nicely already.

Instead, I’m going to focus on what I consider “Catalyst Worst Practices”, in particular misfeatures of Catalyst (and/or plugins) that many people use.

Configuration File (Mis)Handling

Catalyst::Plugin::ConfigLoader loads a config file and merges it into the application config set via MyApp->config(...). “Wonderful”, you say, “I’m sick of dealing with config files”. Me too! Unfortunately, if you embrace this style of config handling you’re setting yourself up for problems later.

It is absolutely crucial that your configuration file be available outside of a web environment. Yes, we’re writing webapps, but any sufficiently complex web application will expand to include a cron job or job queue or some sort of asynchronous task. Usually this will involve sending email.

Unfortunately, ConfigLoader’s config handling is very tightly integrated into its web components. First, it gets things conceptually wrong by combining all sorts of config into one massive hash. When you call $c->config you can find configuration items for …

  • Configuration info from your config file
  • Configuration info set in a call to MyApp->config(...)
  • Configuration info for the current controller and its parents

When you use ConfigLoader, your config file can contain both non-web things like database connections, as well configuration specific to your app, and configuration for plugins you use.

All of this gets jumbled together into one simplistic API. This API just gives you back the config info as a giant data structure, with no opportunity to add logic to the mix. Worse it’s only available from inside an instantiated webapp via $c->config. This is wrong, wrong, wrong.

How I Do It Instead

I always write my own app-specific config module. This module will use a CPAN module for the actualy reading of files. I like to stick with a simple format, so Config::INI works nicely, but that’s a small detail.

The configuration file contains the most minimal set of things it can in order to bootstrap the application. Typically, this will include database connection info and not much else. Maybe it also includes a hostname for the application, which may sometimes be necessary.

This module also includes logic for determining various application configuration values. Note that it does not allow (or require) the end user to configure these things. The fact that PluginLoader lets you configure everything from a configuration file is a nightmare. A configuration file is something that non-developers see, and should have a well-defined, small set of options.

I then use this module to generate configuration data for various parts of my application. In my webapp class, I use it to feed configuration data into Catalyst. That looks something like this:

package R2;

use R2::Config;

use Moose;

my $Config;

BEGIN {
    extends 'Catalyst';

    $Config = R2::Config->new();

    Catalyst->import( @{ $Config->catalyst_imports() } );
}

__PACKAGE__->config(
    name => 'R2',
    %{ $Config->catalyst_config() },
);

Most of the configuration passed to Catalyst is not user-settable. For example, I don’t want people installing an app to have control over how the Catalyst Session plugin is configured! This is part of the application internals, and users have no business messing with it.

This R2::Config module just works both inside Catalyst and outside of it. When I need application-wide config I simply need to write R2::Config->new()->share_dir() and it works. This means I can take advantage of my configuration in any context, not just inside a web request. This makes writing cron jobs and other non-web pieces trivially easy, although there is a bigger investment up front in designing the configuration module’s API.

BTW, the “R2″ example comes from a real app in progress.

The Maleficent “Model”

Have you ever looked in a Catalyst class and seen something like $c->model('DBIC::Person')->find(...)? What is it doing? Well, not much, but it’s just enough to make a mess.

A good example is the MojoMojo source, which I’ve been hacking on recently. If you look at the source tree, you’ll see that the model code lives under MojoMojo::Schema::ResultSet::* and MojoMojo::Schema::Result::*. The MojoMojo::Schema class ties this all together. In any sane world you’d be writing $schema->resultset('Person')->find(...). But this is not a sane world.

You might argue that the Model bit is solving a problem, which is that we need to instantiate a schema object before we can get at the database. That is a problem that needs solving, but the model API adds nothing to this.

What is wrong with something like this?

package MojoMojo;

has schema => (
    is      => 'ro',
    lazy    => 1,
    default => sub { MojoMojo::Schema->connect() },
);

Then later in our controllers we can write:

$c->schema()->resultset('Person')->find(...);

If we’ve done our work on configuration handling as I described above, then MojoMojo::Schema knows just where to look for connection info. All that the model API adds is a useless layer of redirection (aka confusion) and a useless ‘DBIC::’ prefix to our resultset names.

(Nosy readers might point out that the R2 code does have a Model class. That was an experiment which must die.)

$c->uri_for? Not for Me!

Here comes my ultimate heresy. I never use $c->uri_for. I always write application-specific logic for generating URIs. Once again, this comes back to being able to use my application outside of a web environment. For example, I may want to generate email from a cron job that includes application URIs. If I rely on $c->uri_for I would then need to duplicate its logic outside of Catalyst.

My current approach is to simply make generating URIs a responsibility of each object in the system. I don’t love this, because it inflicts “web-ness” on my model, but I can rationalize this by considering the URI a persistent unique identifier. In the age of REST that actually makes sense.

This also lets me do things like install the application under a path prefix like “/r2″. If the application supports adding an arbitrary prefix to all outgoing paths, this works nicely. I can strip the prefix before any controllers see it, so it requires very little code to support, just some configuration.

This approach is especially handy when an application is designed to be served from multiple hostnames. If you’re doing this, you need to account for this in the above-mentioned emails. With R2, each Account (a group of Users) is associated with a Domain. A domain can have separate web and email hostnames, and those hostnames are always used when generating URIs for anything associated with the account.

If I used $c->uri_for I’d still need a way to go from a web hostname to an email hostname.

Summary

I encourage you to think twice before adopting every feature you see someone else use in a Catalyst app. Catalyst is great, but not everything about it supports long-term maintainable applications.

Some of its features make getting started with small apps really easy, but they will bite you in the ass as your app grows. With a little more work up front, you can build a cleaner app that won’t require major hacks or rewriting later.