[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