[Catalyst-commits] r14477 - trunk/examples/CatalystAdvent/root/2013

jnapiorkowski at dev.catalyst.perl.org jnapiorkowski at dev.catalyst.perl.org
Fri Dec 6 16:08:07 GMT 2013


Author: jnapiorkowski
Date: 2013-12-06 16:08:07 +0000 (Fri, 06 Dec 2013)
New Revision: 14477

Added:
   trunk/examples/CatalystAdvent/root/2013/16.pod
   trunk/examples/CatalystAdvent/root/2013/17.pod
Log:
added days 16 / 17

Added: trunk/examples/CatalystAdvent/root/2013/16.pod
===================================================================
--- trunk/examples/CatalystAdvent/root/2013/16.pod	                        (rev 0)
+++ trunk/examples/CatalystAdvent/root/2013/16.pod	2013-12-06 16:08:07 UTC (rev 14477)
@@ -0,0 +1,181 @@
+=head1 CataylstX::Controller::PSGI - Using Plack apps inside Catalyst Controllers - Part 1
+
+=head1 Overview
+
+Sometimes you might want to use a Plack app as part of your application, but
+don't want to mess around with your .psgi file mounting different parts under
+different urls, or running seperate servers and setting up rewrite/proxy rules.
+
+=head1 Introduction
+
+The scenario is thus, we have an existing application, which used to be a
+cgi/modperl application, and we've since wrapped in Plack. Now we've decided
+we want to start using Catalyst, but we don't want to rewite our decade old legacy
+app, and we don't want to run multiple servers or mess around with rewrite rules.
+
+=head1 Prequisites
+
+L<Task::Catalyst>, L<CatalystX::Controller::PSGI>
+
+=head1 The Aim
+
+Wrap our legacy app in Catalyst, so that all requests will dispatch to the existing
+code, unless a new action is created in which case that will take precedence.
+
+Our legacy app has the following actions,
+
+    /some/action
+    /some/other/action
+    /foo
+    /foo/bar
+
+We're going to replace the following action with a Catalyst action
+
+    /some/other/action
+
+=head1 The Code
+
+We'll start by creating a new Cataylst app
+
+    catalyst.pl MyApp
+
+Now that that's out of the way, let's get cracking.
+
+First, edit the new Root.pm to use L<CatalystX::Controller::PSGI> and mount our
+legacy .psgi as the root action
+
+edit MyApp/lib/MyApp/Controller/Root.pm and replace everything with the following,
+since cataylst.pl has given us a lot of helpful defaults, which are not helpful
+in our case.
+
+    package MyApp::Controller::Root;
+    use Moose;
+    use namespace::autoclean;
+
+    BEGIN { extends 'CatalystX::Controller::PSGI' };
+
+    __PACKAGE__->config(namespace => '');
+
+    use Plack::Util;
+
+    has 'legacy_app' => (
+        is      => 'ro',
+        builder => '_build_legacy_app',
+    );
+
+    sub _build_legacy_app {
+        return Plack::Util::load_psgi( MyApp->path_to("bin/legacy.psgi") );
+    }
+
+    sub call {
+        my ( $self, $env ) = @_;
+
+        $self->legacy_app->( $env );
+    }
+
+    __PACKAGE__->meta->make_immutable;
+
+Ok, so what's going on here? The first 7 lines are the standard boilerplate for
+the Root controller, after that we setup a Moose accessor that will hold our
+legacy .psgi app. The important part of this is I<sub call>, which
+L<CatalystX::Controller::PSGI> uses as the / action of this controller. Which
+is also the / of the app, because this is the root controller.
+
+So now any url will match the root action (call is registered as :Local, meaning
+it can take an unlimited amount of arguments). But it still won't work, becuase
+bin/legacy.psgi doesn't exist. So let's fix that by creating it.
+
+    use strict;
+    use warnings;
+    use Plack::Response;
+
+    use Legacy::App;
+    use Legacy::DB;
+
+    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("some/action");
+        }
+        elsif ( $env->{PATH_INFO} eq 'some/other/action' ) {
+           ( $status, $body ) = $legacy_app->handle_request("some/other/action");
+        }
+        elsif ( $env->{PATH_INFO} eq 'foo' ) {
+           ( $status, $body ) = $legacy_app->handle_request("foo");
+        }
+        else {
+            $status = 404;
+            $body = 'not found';
+        }
+
+        my $res = Plack::Response->new( $status );
+        $res->content_type('text/html');
+        $res->body( $body );
+
+        return $res->finalize;
+    };
+
+    $app;
+
+So now our "legacy" app is ready to rock. Let's fire it up.
+
+    plackup -I MyApp/lib MyApp/myapp.psgi
+
+If we fire up our web browser and head to L<http://127.0.0.1:5000/some/other/action>
+you'll see I<"this is some/other/action">, which is great, because it means Catalyst is
+dispatching our requests to the psgi app. Obviously this has a performance hit,
+as we've added the Catalyst request/response cycle in, but it doesn't matter, as
+we're going to gain more than we've lost in the long term.
+
+Next lets replace I</some/other/action> with our new awesome cataylst code.
+
+Create the file MyApp/lib/MyApp/Controller/Some.pm
+
+    touch MyApp/lib/MyApp/Controller/Some.pm
+
+edit the above add the following
+
+    package MyApp::Controller::Some;
+    use Moose;
+    use namespace::autoclean;
+
+    BEGIN { extends 'Catalyst::Controller' };
+
+    sub other_action: Path('other/action') {
+        my ( $self, $c ) = @_;
+
+        $c->res->body("WOOO CATALYST");
+    }
+
+    __PACKAGE__->meta->make_immutable;
+
+Now if we fire up our web browser and head to L<http://127.0.0.1:5000/some/other/action>
+we'll see I<"WOOO CATALYST">, which is awesome. Now bolt on some L<DBIx::Class>,
+extract some of the legacy app out into reusable modules, if it's not already,
+combine those with L<Catalyst::Model::Adaptor> and before you know it, you'll be
+a Catalyst app with a little legacy code, rather than a legacy app with a little
+Catalyst
+
+=head1 Summary
+
+Although the above code is simple, and plainly not a real world legacy app, the
+the example still applies, and it's a good start on modernising an old codebase,
+and taking full advantage of the rapid development that Catalyst offers you,
+once it's set up.
+
+=head1 Author
+
+Mark Ellis L<email:nobody at cpan.org>
+
+=cut

Added: trunk/examples/CatalystAdvent/root/2013/17.pod
===================================================================
--- trunk/examples/CatalystAdvent/root/2013/17.pod	                        (rev 0)
+++ trunk/examples/CatalystAdvent/root/2013/17.pod	2013-12-06 16:08:07 UTC (rev 14477)
@@ -0,0 +1,293 @@
+=head1 CataylstX::Controller::PSGI - Using Plack apps inside Catalyst Controllers - Part 2
+
+=head1 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
+
+=head1 Prequisites
+
+Read L<Part 1|http://www.catalystframework.org/calendar/2013/16> first
+
+=head1 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
+L<Catalyst::Model::Adaptor> to wrap our module. So create the file
+I<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 I<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 L<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.
+
+=head1 Summary
+
+You can see the code for part 1 and 2 at L<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.
+
+=head1 Author
+
+Mark Ellis L<email:nobody at cpan.org>
+
+=cut




More information about the Catalyst-commits mailing list