CatalystX::Controller::PSGI - Using Plack apps inside Catalyst Controllers - Part 2

Overview

In part 1 of this article, I covered the basics of wrapping a .psgi app inside Catalyst so we could work on adding new urls the Catalyst way, whilst still having our legacy app working as before.

In this part I'll be covering converting parts of that legacy app into Cataylst Models, and passing them back to the legacy app at startup

Prequisites

Read Part 1 first

The Code

We'll start by inlining our legacy app.

Change the _build_legacy_app method in Root.pm to contain the contents of the legacy psgi.

    use Legacy::App;
    use Legacy::DB;

    sub _build_legacy_app {
        my $db = Legacy::DB->new(
            dbspec  => "legacy",
            region  => "en",
        );

        my $legacy_app = Legacy::App->new(
            db      => $db,
        );

        my $app = sub {
            my ( $env ) = @_;

            my ( $status, $body );
            if ( $env->{PATH_INFO} eq 'some/action' ) {
               ( $status, $body ) = $legacy_app->handle_request( $env->{PATH_INFO} );
            }
            elsif ( $env->{PATH_INFO} eq 'some/other/action' ) {
               ( $status, $body ) = $legacy_app->handle_request( $env->{PATH_INFO} );
            }
            elsif ( $env->{PATH_INFO} eq 'foo' ) {
               ( $status, $body ) = $legacy_app->handle_request( $env->{PATH_INFO} );
            }
            else {
                $status = 404;
                $body = 'not found';
            }

            my $res = Plack::Response->new( $status );
            $res->content_type('text/html');
            $res->body( $body );

            return $res->finalize;
        };

        $app;
    }

Looking at our legacy app, we only need a few urls, one of which has already been over written. So we can tidy it up, and seperate out the not found action into a proper Catalyst action, if instead of using call we use mount

The downside of this is we pass a subref to mount, so we can't use a Moose attribute, but that's ok.

So we'll remove the legacy app attribute, and replace it with a plain old subref, which we can pass to mount. And whilst we're at it remove the not found handler and the /some/other/action handler, since we've replaced that.

    use Legacy::App;
    use Legacy::DB;

    my $app = sub {
        my $db = Legacy::DB->new(
            dbspec  => "legacy",
            region  => "en",
        );

        my $legacy_app = Legacy::App->new(
            db      => $db,
        );

        my $app = sub {
            my ( $env ) = @_;

            my ( $status, $body );
            if ( $env->{PATH_INFO} eq 'some/action' ) {
               ( $status, $body ) = $legacy_app->handle_request( $env->{PATH_INFO} );
            }
            elsif ( $env->{PATH_INFO} eq 'foo' ) {
               ( $status, $body ) = $legacy_app->handle_request( $env->{PATH_INFO} );
            }

            my $res = Plack::Response->new( $status );
            $res->content_type('text/html');
            $res->body( $body );

            return $res->finalize;
        };

        $app;
    }

    __PACKAGE__->mount( 'some/action'   => $app );
    __PACKAGE__->mount( 'foo'           => $app );

There's a lot going on there, and we can split it up further, lets do some cleaning and add a Cataylst 404 handler, aka the default action

A good thing to note with mount is that $self is passed into the app, as well $env, meaning we can set Moose attributes for the legacy_app and db

    use Legacy::App;
    use Legacy::DB;

    has "_db" => (
        is      => 'ro',
        builder => '_build_db',
        lazy    => 1,
    );
    sub _build_db {
        my $self = shift;
        return Legacy::DB->new(
            dbspec  => "legacy",
            region  => "en",
        );
    }

    has "_legacy_app" => (
        is      => "ro",
        builder => "_build_legacy_app",
        lazy    => 1,
    );
    sub _build_legacy_app {
        my $self = shift;
        return Legacy::App->new(
            db      => $self->_db,
        );
    }

    my $legacy_app_wrapper = sub {
        my ( $self, $env ) = @_;
        my ( $status, $body ) = $self->_legacy_app->handle_request( $env->{PATH_INFO} );

        my $res = Plack::Response->new( $status );
        $res->content_type('text/html');
        $res->body( $body );

        return $res->finalize;
    };

    __PACKAGE__->mount( 'some/action'   => $legacy_app_wrapper );
    __PACKAGE__->mount( 'foo'           => $legacy_app_wrapper );

    sub default: Private {
        my ( $self, $c ) = @_;

        $c->res->body('not found');
        $c->res->status(404);
    }

Nice, we've now got a tidy controller. A very fat controller though, and fat controllers are bad, we want thin controllers, and fat models, let's see what we can do about that.

So the first thing we should do is create a model for the db, we'll use Catalyst::Model::Adaptor to wrap our module. So create the file MyApp/lib/MyApp/Model/DB.pm

    package MyApp::Model::DB;
    use strict;
    use warnings;

    use base 'Catalyst::Model::Adaptor';

    __PACKAGE__->config(
        class   => 'Legacy::DB',
        args    => {
            dbspec  => "legacy",
            region  => "en",
        },
    );

    sub mangle_arguments {
        my ( $self, $args ) = @_;
        return %$args;
    }

    1;

and MyApp/lib/MyApp/Model/LegacyApp.pm

    package MyApp::Model::LegacyApp;
    use strict;
    use warnings;

    use base 'Catalyst::Model::Adaptor';

    __PACKAGE__->config(
        class   => 'Legacy::App',
    );

    sub prepare_arguments {
        my ( $self, $app ) = @_;

        # this is fine as long as DB doesn't change, if it does you should use
        # Catalyst::Model::Factory::PerRequest
        return {
            db  => $app->model('DB'),
        };
    }

    sub mangle_arguments {
        my ( $self, $args ) = @_;
        return %$args;
    }

    1;

Notice how we had to mangle_arguments, that's because our legacy app expects a hash to ->new, and not a hashref. And for LegacyApp we use prepare_arguments to setup db, that's because we needed to access the Catalyst model, and this way we can pass it in nice and cleanly.

Sweet, now that's done we can tidy up the controller even more.

    my $legacy_app_wrapper = sub {
        my ( $self, $env ) = @_;
        my ( $status, $body ) = $self->c->model('LegacyApp')->handle_request( $env->{PATH_INFO} );

        my $res = Plack::Response->new( $status );
        $res->content_type('text/html');
        $res->body( $body );

        return $res->finalize;
    };

    __PACKAGE__->mount( 'some/action'   => $legacy_app_wrapper );
    __PACKAGE__->mount( 'foo'           => $legacy_app_wrapper );

    sub default: Private {
        my ( $self, $c ) = @_;

        $c->res->body('not found');
        $c->res->status(404);
    }

But wait, where does this $self->c come from? How are we going to get from a controller to the context? Enter Catalyst::Component::InstancePerContext

    with 'Catalyst::Component::InstancePerContext';

    has 'c' => (
        is  => 'rw',
    );

    sub build_per_context_instance{
        my ( $self, $c ) = @_;

        return $self->new(
            %{ $self->config },
            c => $c,
        );
    }

What we're doing here is creating a new copy of the controller for each request and stashing the Catalyst context object in $self->c. We could use ACCEPT_CONTEXT or even COMPONENT, since we only need access to c->model, but a lot can go wrong, and better to be safe than sorry.

Now our controller is pretty thin, and our legacy code has been converted into Catalyst Models, and we can get on making our website, only now we have the power and flexibility of Catalyst behind us, and one day we won't even need that legacy code at all.

Summary

You can see the code for part 1 and 2 at https://github.com/perl-catalyst/2013-Advent-Staging/tree/master/CatalystX-Controller-PSGI

Part 1 is Myapp, part 2 is MyApp2.

Hope this has been of use, and in the meantime, Relax, don't worry, have a homebrew.

Author