Using plain classes as Catalyst models

A common pitfall when designing models is the tendency to tie application logic to Catalyst. The problem with this approach is that model classes become difficult to reuse outside of Catalyst, which is a common requirement for most applications. A better design approach would be to write and test a plain class outside of your main web application, and then painlessly glue it to Catalyst with a couple lines of code.

A review of Catalyst components

The COMPONENT() method

Catalyst gives you a chance to instantiate all your components (Models, Views and Controllers) during load time by calling the COMPONENT() method on each component class. This means you can implement it and return whatever you want from it. For models this boils down to:

    package MyApp::Model::SomeClass;

    use warnings;
    use strict;
    use base qw/Catalyst::Model/;

    use SomeClass;

    sub COMPONENT {
        my($self, $c, @config) = @_;
        return SomeClass->new(@config);
    }

    1;

This particular implementation passes the configuration for this model to the external class on the call to new(). Of course, you could choose to instance the class any way you want to. Later on, $c->model('SomeClass') will get you the SomeClass instance, not MyApp::Model::SomeClass. Notice that the returned object is the very same instance that was created initially when your app was loaded.

The ACCEPT_CONTEXT() method

Every time $c->model is called, Catalyst gives you a chance to run custom logic by attempting to run ACCEPT_CONTEXT on the model, whatever is returned by this method is what $c->model returns as well. So, if you need a new instance on every call, your model becomes something like:

    package MyApp::Model::SomeClass;

    use warnings;
    use strict;
    use base qw/Catalyst::Model/;

    use SomeClass;

    sub ACCEPT_CONTEXT {
        my($self, $c, @args) = @_;
        return SomeClass->new(@args);
    }

    1;

So when you call $c->model('SomeClass'), you'll get a fresh instance of SomeClass not MyApp::Model::SomeClass.

The Catalyst::Model::Adaptor way

Instead of implementing your own glue code, you can use the generic implementation provided by the Catalyst::Model::Adaptor module, which provides several different ways of building your external class instances.

If you need a single application-wide instance of your external class, you can inherit from Catalyst::Model::Adaptor:

    package MyApp::Model::Foo;

    use warnings;
    use strict;
    use base qw/Catalyst::Model::Adaptor/;

    __PACKAGE__->config(
        class => 'SomeClass',
        args  => {
            foo => 'bar'
        }
    );

    1;

Of course, you can also configure your class via myapp.yaml

    Model::Foo:
        class: SomeClass
        args:
            foo: bar

This gives you more flexibility when you decide to change your implementation, just replace SomeClass with whatever class you wish to use, without even touching your code.

Alternate object instancing approaches

For instancing objects on every call to $c->model, just inherit from Catalyst::Model::Factory instead. And for instancing objects on a per-request basis, inherit from Catalyst::Model::Factory::PerRequest. The Catalyst::Model::Adaptor documentation provides information on how to further customize your models to address your specific needs.

For the incredibly lazy

You can easily create glue models by using the helpers provided with Catalyst::Model::Adaptor:

    script/myapp_create.pl model SomeClass <type> MyApp::Model::SomeClass

Where <type> can be Adaptor, Factory or Factory::PerRequest.

ACKNOWLEDGEMENTS

Jonathan Rockway, for writing Catalyst::Model::Adaptor.

AUTHOR

edenc - Eden Cardim - edencardim@gmail.com