20 posts tagged “catalyst”
We're approaching the two-year anniversary of the first release in the 5.7x series of the Catalyst framework. I'm really proud of how 5.7x has gone -- it has given the project some much needed stability that was missing in the early goings. It still amuses me to look back at the changelog to watch it go from version 3.X (which is basically "Catalyst 1.0") to 5.X in the span of about two and a half months.
Although development was obviously very fast-paced then, with 14 releases since 5.7000 I wouldn't say we've stalled. Naturally, the bulk of the changes since then have been bug fixes. We've also increased the test suite from 1416 tests to 1805 (the old test suite actually ran most tests twice by default, but, by setting CAT_BENCH_ITERS=1, you will see the "1416" result).
A 5.71 dev release (5.7099_01) was recently shipped which includes a new method: go(). As marcus describes it, it "works like an internal redispatch to another action, while retaining the stash intact." I believe one more dev release will happen as I've recently checked in the long lost PathPrefix attribute.
5.71xx will be more of a short-lived series of releases to act as a buffer between 5.70XX and 5.8000. 5.8000 being the Moose conversion (see this interview for more information).
Catalyst 5.7014 is out the door. Hopefully that will stop the flood of questions about a "strange uri_for() behavior." With that done, I've taken out 5 more RT tickets.
Two extremely old wishlist items were rejected (RT #26758, RT #24132). This is basically due to the fact that they were over a year old, and really should be talked about on the dev list if they are still inclined to have them resolved.
A couple others required that I cook up a test or two to ensure the patch was applying was satisfactory (naturally, we would always hope to have a test submitted along side the patch): RT #26455, RT #34437.
The last ticket seems to have been related to a regression in 5.7013, as the ticket author claims the latest release fixed the issue. Closed! (RT #35994)
I've put the 5-a-day thing away for a bit to focus on other things. Firstly it was trying to get a new release of Catalyst-Runtime out the door (5.7013 hit CPAN on the 17th). One of the last bits preventing that release was some back-compat methods for Catalyst::Stats so it could mimic Tree::Simple behavior, which is what $c->stats() used to return.
Unfortunately, this release introduced two regressions, now fixed in svn:
1) "sub foo : Path {}" in the root controller didn't work.
This was as a result of my attempt to allow Path(0) to match properly. A simple 1-line fix resolved this problem. This bug also prevents some components' tests from working successfully (so far Catalyst-Model-Adaptor and Catalyst-Plugin-Unicode) as they used this idiom in their test apps.
2) invalid namespace for relative arguments to uri_for from an action that was run from a $c->forward() command.
Since there are a few conditions to satisfy before this bug appears I'm not too suprised it snuck in to the release. Peter Karman provided a much needed test case and the simple fix (remove "local $c->{namespace} = $self->namespace") which i've included in the 5.70 trunk.
I hope we can get 5.7014 out the door relatively swiftly.
I've also fixed up a couple of Catalyst::Plugin::Authentication RT tickets. Both were simple pod fixes: RT #36062, RT #36063
Note to self...
If, in my Catalyst app, I have a template that includes unicode text, then not only should I save the file as UTF-8, but I should also include a Byte Order Mark (BOM). If I don't I'll end up seeing what looks like double encoded bytes -- and that annoys the hell out of me.
Currently, your "MyApp.pm" file is both your application class and your context class (NB: this is expected to change in 5.8000). We've been slowing suggesting that people move things out of th context/app class so as not to pollute it with an abundance of mehods which may occasionally have unwanted consequences -- for example "login : Global {}" conflicting with the Authentication plugin's login() method.
This tip is broken into two parts:
- It's been said time and time again (c.f. this and this [phaylon++]), not everthing has to be a plugin! If the sum total of your plugin is this:
You should reconsider. Either use the module directly, or make a controller base class. That should handle most cases.package Catalyst::Plugin::Foo;
use Foo;
sub foo {
my( $c, @rest ) = @_;
return Foo->new( @rest );
} - Be careful what you import into MyApp.pm! Some modules will export methods (and other symbols) by default, and sometimes you'll do it manually. Consider explicitly importing nothing:
before:
after:package MyApp;
use Digest::MD5 qw(md5_hex); # MyApp now has the md5_hex method
sub foo {
# ...
return md5_hex( $string );
}
If you want a quick-n-easy cleanup, try namespace::clean.package MyApp;
use Digest::MD5 (); # no imports
sub foo {
# ...
return Digest::MD5::md5_hex( $string );
}
Since I've push a new stable release of WWW::OpenSearch, I was able to push some new modules that depended on it. In particular WebService::Lucene -- a perl client to the lucene-ws java servlet. We've been using the module in production for just under a year with great success.
I've also pushed a simple Catalyst model to CPAN as well. However, once I give DBIx::Class::Indexed + Indexer::* the ability to search, then that model will have very little practical usage for us.
After re-reading my last post, I realized that I neglected to mention one special exception: $Catalyst::DETACH.
When you call $c->detach(... ), underneath it calls $c->forward(... ) then dies with a special detach message. Unfortunately, all of our exception handling will catch this too. It's pretty easy to clean this up in our end action:
sub end : Private {
my( $self, $c ) = @_;
if( my( $error ) = @{ $c->error } ) {
if( $error->message eq $Catalyst::DETACH ) {
$c->clear_errors;
}
else {
return;
}
}
# continue on as normal...
}
detach().I've been doing exception handling with perl in the way most people expect
eval { code that might throw an exception };
if( $@ ) {
handle the issue
}
This code tends to be a bit fragile when it comes to deciphering particular type of failure. Was it a "file not found" error? You can do a simple regex to find out. However, what if error messages are localized? Ugh.
It wasn't until recently that I truly learned the power of "exceptions as objects" in programming. In particular, its usage in a web framework.
I can't really take the credit for this. It comes from my fellow Catalyst developer Christian Hansen and his Isotope code.
If you've browsed that last link, you should notice something. It looks a lot like an HTTP response. That was kind of a mind blowing concept to me. This means that we can throw exceptions that aren't necessarily fatal, but, uh, exceptional in some manner for lack of a better word -- all the while staying in the HTTP world.
Adapting the above to Catalyst is pretty easy.
package MyApp::Exceptions;
use strict;
use warnings;
BEGIN {
$Catalyst::Exception::CATALYST_EXCEPTION_CLASS = 'MyApp::Exception';
my %classes = (
'MyApp::Exception' => {
description => 'Generic exception',
fields => [ qw( headers status status_message payload ) ],
alias => 'throw'
},
'MyApp::Exception::FileNotFound' => {
isa => 'MyApp::Exception',
description => '404 - File Not Found',
},
'MyApp::Exception::AccessDenied' => {
isa => 'MyApp::Exception',
description => '401 - Access Denied',
},
);
my @exports = grep { defined } map { $classes{ $_ }->{ alias } } keys %classes;
require Exception::Class;
require Sub::Exporter;
Exception::Class->import(%classes);
Sub::Exporter->import( -setup => { exports => \@exports } );
}
package MyApp::Exception;
use strict;
use warnings;
no warnings 'redefine';
use HTTP::Headers ();
use HTTP::Status ();
use Scalar::Util qw( blessed );
sub headers {
my $self = shift;
my $headers = $self->{headers};
unless ( defined $headers ) {
return undef;
}
if ( blessed $headers && $headers->isa('HTTP::Headers') ) {
return $headers;
}
if ( ref $headers eq 'ARRAY' ) {
return $self->{headers} = HTTP::Headers->new( @{ $headers } );
}
if ( ref $headers eq 'HASH' ) {
return $self->{headers} = HTTP::Headers->new( %{ $headers } );
}
MyApp::Exception->throw(
message => qq(Can't coerce a '$headers' into a HTTP::Headers instance.)
);
}
sub status {
return $_[0]->{status} ||= 500;
}
sub is_info {
return HTTP::Status::is_info( $_[0]->status );
}
sub is_success {
return HTTP::Status::is_success( $_[0]->status );
}
sub is_redirect {
return HTTP::Status::is_redirect( $_[0]->status );
}
sub is_error {
return HTTP::Status::is_error( $_[0]->status );
}
sub is_client_error {
return HTTP::Status::is_client_error( $_[0]->status );
}
sub is_server_error {
return HTTP::Status::is_server_error( $_[0]->status );
}
sub status_line {
return sprintf "%s %s", $_[0]->status, $_[0]->status_message;
}
sub status_message {
return $_[0]->{status_message} ||= HTTP::Status::status_message( $_[0]->status );
}
my %messages = (
400 => 'Browser sent a request that this server could not understand.',
401 => 'The requested resource requires user authentication.',
403 => 'Insufficient permission to access the requested resource on this server.',
404 => 'The requested resource was not found on this server.',
405 => 'The requested method is not allowed.',
500 => 'The server encountered an internal error or misconfiguration and was unable to complete the request.',
501 => 'The server does not support the functionality required to fulfill the request.',
);
sub public_message {
return $messages{ $_[0]->status } || 'An error occurred.';
}
sub as_public_html {
my $self = shift;
my $title = shift || $self->status_line;
my $header = shift || $self->status_message;
my $message = shift || $self->public_message;
return <<EOF;
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head>
<title>$title</title>
</head>
<body>
<h1>$header</h1>
<p>$message</p>
</body>
</html>
EOF
}
sub has_headers {
return defined $_[0]->{headers} ? 1 : 0;
}
sub has_payload {
return defined $_[0]->{payload} && length $_[0]->{payload} ? 1 : 0;
}
sub has_status_message {
return defined $_[0]->{status_message} ? 1 : 0;
}
sub full_message {
my $self = shift;
my $message = $self->message;
if ( $self->has_payload ) {
$message .= sprintf " %s.", $self->payload;
}
return $message;
}
package MyApp::Exception::FileNotFound;
sub status {
return $_[0]->{status} ||= 404;
}
package MyApp::Exception::AccessDenied;
sub status {
return $_[0]->{status} ||= 401;
}
1;
Now to use the classes and handle exceptions.
package MyApp;
# ...
use MyApp::Exceptions;
use Scalar::Util ();
# ...
sub finalize {
my ( $c ) = shift;
$c->handle_exception if @{ $c->error };
$c->NEXT::finalize( @_ );
}
sub handle_exception {
my( $c ) = @_;
my $error = $c->error->[ 0 ];
if( !Scalar::Util::blessed( $error ) or !$error->isa( 'MyApp::Exception' ) ) {
$error = MyApp::Exception->new( message => "$error" );
}
# handle debug-mode forced-debug from RenderView
if( $c->debug && $error->message =~ m{^forced debug} ) {
return;
}
$c->clear_errors;
if ( $error->is_error ) {
$c->response->headers->remove_content_headers;
}
if ( $error->has_headers ) {
$c->response->headers->merge( $error->headers );
}
# log the error
if ( $error->is_server_error ) {
$c->log->error( $error->as_string );
}
elsif ( $error->is_client_error ) {
$c->log->warn( $error->as_string ) if $error->status =~ /^40[034]$/;
}
if( $error->is_redirect ) {
# recent Catalyst will give us a default body for redirects
if( $error->can( 'uri' ) ) {
$c->response->redirect( $error->uri( $c ) );
}
return;
}
$c->response->status( $error->status );
$c->response->content_type( 'text/html; charset=utf-8' );
$c->response->body(
$c->view( 'HTML' )->render( $c, 'error.tt', { error => $error } )
);
# processing the error has bombed. just send it back plainly.
$c->response->body( $error->as_public_html ) if $@;
}
$SIG{ __DIE__ } = sub {
return if Scalar::Util::blessed( $_[ 0 ] );
MyApp::Exception->throw( message => join '', @_ );
};
That's a fair bit of code, but it's pretty straight-forward. We've added a __DIE__ handler to convert string-based exceptions to our object, and we also, make sure we get the type of object we expect near the top of our error handling routine. RenderView has a special debug-mode exception that we want to pass through.
The rest of the code preps the response. A redirect is basically passed through, everything else is rendered via a template (which could be customized in our exception if we wanted to add that bit of logic). There's even a little fallback mechanism in case template rendering is where our problems lie.
As a quick-n-dirty usage example, we can throw exceptions quite simply:
MyApp::Exception::FileNotFound->throw( message => "Widget $id not found." );
Nice!
I hope you've found this bit of code as interesting as i have.
In relation to my last post, a co-worker asked me how i might add a URL like/admin/account/create to the mix. here is what i came up with for a solution:
package MyApp::Controller::Admin::Account;
use strict;
use warnings;
use base 'Catalyst::Controller';
# methods on/admin/account/
sub root : Chained('/') PathPrefix CaptureArgs(0) { }
sub list : Chained('root') PathPart('') Args(0) { die "index of accounts" }
# methods on/admin/account/$name
sub create : Chained('root') PathPart Args(0) { die "create an account" }
# methods on/admin/account/$id/[$name]
sub instance : Chained('root') PathPart('') CaptureArgs(1) { }
sub view : Chained('instance') PathPart('') Args(0) { die "view account" }
sub update : Chained('instance') PathPart Args(0) { die "update account" }
1;
I've added in a root midpoint which will allow us to chain any other method off of /admin/account.
(Again, sanity checked by mst)
So, you want to write a generic base controller for some set of actions? Great! Want to use Chained actions for nicer looking URLs? Fantastic!
Let's presume you're writing a generic account admin controller. Your URL-space might look like:
/admin/account/- account list
/admin/account/1/- account view
/admin/account/1/update- account update
Currently, you have a couple of options in order to make this a reality:
1) Action-specific configs
Catalyst's configurability is extremely granular -- you can set specific attributes as configuration parameters for any given action.
package MyApp::Controller::Admin::Account;
use strict;
use warnings;
use base 'Catalyst::Controller';
__PACKAGE__->config( actions => {
instance => { PathPart => 'admin/account' },
list => { PathPart => 'admin/account' }
} );
sub list : Chained('/') Args(0) { die "index of accounts" }
sub instance : Chained('/') CaptureArgs(1) { } # do something with $c->req->captures->[ 0 ]
sub view : Chained('instance') PathPart('') Args(0) { die "view account" }
sub update : Chained('instance') PathPart Args(0) { die "update account" }
1;
In the above example we're explicitly setting the PathPart for the list and instance actions -- However, it's not quite fully generic.
2) Create our own attribute
We'll create an attribute that will expose a controller's path_prefix to any action -- aptly named PathPrefix.
package MyApp::Controller::Admin::Account;
use strict;
use warnings;
use base 'Catalyst::Controller';
sub _parse_PathPrefix_attr {
my ( $self, $c, $name, $value ) = @_;
return PathPart => $self->path_prefix;
}
sub list : Chained('/') PathPrefix Args(0) { die "index of accounts" }
sub instance : Chained('/') PathPrefix CaptureArgs(1) { } # do something with $c->req->captures->[ 0 ]
sub view : Chained('instance') PathPart('') Args(0) { die "view account" }
sub update : Chained('instance') PathPart Args(0) { die "update account" }
1;
Now that's better -- except for that pesky sub. Well, you're in luck -- PathPrefix is now in the current Catalyst::Runtime branch. Our code is now even simpler:
package MyApp::Controller::Admin::Account;
use strict;
use warnings;
use base 'Catalyst::Controller';
sub list : Chained('/') PathPrefix Args(0) { die "index of accounts" }
sub instance : Chained('/') PathPrefix CaptureArgs(1) { } # do something with $c->req->captures->[ 0 ]
sub view : Chained('instance') PathPart('') Args(0) { die "view account" }
sub update : Chained('instance') PathPart Args(0) { die "update account" }
1;
As I've stated, Catalyst is extremely configurable. Let's say you don't like "admin/account" as the path? No Problem!
package MyApp::Controller::Admin::Account;
use strict;
use warnings;
use base 'Catalyst::Controller';
__PACKAGE__->config( path => 'foo/bar' );
sub list : Chained('/') PathPrefix Args(0) { die "index of accounts" }
sub instance : Chained('/') PathPrefix CaptureArgs(1) { } # do something with $c->req->captures->[ 0 ]
sub view : Chained('instance') PathPart('') Args(0) { die "view account" }
sub update : Chained('instance') PathPart Args(0) { die "update account" }
1;
All of the actions will now start with "/foo/bar/"!
Happy Hacking.
Special thanks to mst for being my editor and sanity checker.