Rethinking Catalyst Actions and Controllers
The recent release of Perl Catalyst 5.8 has does wonders for our ability to incorporate new developers and new ideas into the Elegant Perl Web Application Framework. As I have reflected on in other posts, Moose based projects offer a shared context that eases the learning curve barrier for newcomers. I look forward to the coming explosion of thoughts and ideas!
One idea that keeps coming up is how to replace the existing method attribute system used by Catalyst Controllers to map Actions to URIs into something more flexible and sustainable. If you don't know what I mean you should read here. Basically, Catalyst Controller classes use these method attributes as a way to 'tag' actions with the information necessary to tie a given controller + action to a URI endpoint. This system, which has endured from the first versions of Catalyst (brought over from Catalyst's inspirational ancestor, Maypole) is starting to show its weaknesses. Although attributes are cute, this is a system unique to Catalyst controllers, so it doesn't make good use of leveraging code and knowledge from other more widely know skills. For more about troubles with the current method attribute syntax see Declare Catalyst Actions.
A possibility to fix this problem is to create a sort of mini language crafted specifically for the job. This would seem to bring Catalyst more in line with how some other popular frameworks are trending, or even exceed them in some ways. As was recently said to me in IRC, “What I want is the maximum power and flexibility.” From my perspective, I'd rather make controllers easier for the most common cases, and create a system that works in a usable manner even if all you know is how to build normal methods. People with a bit more ability and knowledge should be able to influence or 'hint' the system in order to achieve custom effects, but in the best tradition of Perl you should be usefully productive with minimum knowledge. All the dangerous flexibility is placed one level up the skill scale. As I once read regarding Perl, when the developer is still a baby, baby talk should be adequate to achieve what a baby needs. The knowledge needed to do more then can act to better guide them to develop the skill and best practices necessary.
In order to achieve this “Baby Talk” controller, we need a deeper and more fundamental rethinking of the role of Controllers and how they function, and particularly how creating Controllers fit into the development process. Simply adding a sleek mini language is not going to address what I think of as these main problems. So let's, as a thought experiment, consider how we could create controllers with the minimum expected knowledge. Then, as a second step, let's try to talk things further and see if we can create a controller system that is equally useful for web, command line, GUI or other types of interfaces.
First, here's a 'classic' Catalyst Controller:
package MyApp::Controller::People;
use base 'Catalyst::Controller';
## URL = $base/people/gallery?page={page}&rows={rows}
sub gallery :Path('gallery') {
my ($self, $c) = @_;
my ($page, $rows) = @{$c->request->query_parameters}{qw/page rows/};
}
## URL = $base/people/person/{id}
sub person :Path('person') Args(1) {
my ($self, $c, $id) = @_;
}
## URL = $base/people/create + POST entity
sub create :Path('create') {
my ($self, $c) = @_;
my ($name, $age) = @{$c->request->body_parameters}{qw/name age/};
}
## Plain method, not an action
sub prepare_database {
my($self, %args) = @_;
}
Assume the controllers actually do something useful. The idea here is to show the work involved in mapping a URL and getting some common stuff, like body or post parameters and path arguments. This gets the job done, although the developer is left with a lot of validation work. Notice how there's different ways to get the path parameters from the query and post parameters? In this case the developer has to do a lot of work just to get URI information, and we still have something that's very loosely defined, with lots of room for errors.
Okay, so let's see what we can do if we start with something based on MooseX::Declare instead. Yes, I know I am presupposing knowledge of this module, but: 1) Catalyst is Moose based now and we can assume the developer knows 'baby talk' Moose, 2) knowledge of MooseX::Declare is useful for more than just controllers and 3) It looks familiar enough to other programming systems that the learning curve should be low. However, if you don't know MooseX::Declare, you should go take a quick look now.
Here's the Controller. It's not all “Plain old Perl/Moose” yet, but much closer:
use Catalyst::Declare::Controller; ## Doesn't exist yet :)
class People {
action gallery(Int :$page, Int :$rows) {}
multi action person(Int $id) {}
multi action person(Email $email) {}
action create(Dict[name=>Str,age=Int]) {}
method prepare_database() { #just a plain old method, move along}
}
The idea here is that MooseX::Declare, along with MooseX::Method::Signatures, already offers us a pretty powerful system for expressing arguments, and it's highly introspectable. Why make yet another new signature system, one that is only useful for Controllers, and makes a controller feel like something other than a friendly Perl class?
Now, I know the above example need a bit of explaining, and of course there's gaps, as you would expect at this point. So, going though each action:
Gallery: This has a signature that wants (page=>..., rows=>...) as named arguments and additional expects both those to be an Int (see Moose::Utils::TypeConstraints if you need a primer on Moose TypeConstraints). Since the Int is a standard Moose type constraint, you can leverage the entire type constrain tool chain, including MooseX::Types and coercions, which gives you a lot of possibilities as well as greatly assisting you in your first level validation. The URI generator would map the path parts are $base, 'people' and 'gallery' as is already does, but we'd add in that trailing Hash arguments in the signature become mapped to query parameters. So this makes a URL like “$base/people/gallery?page={page}&rows={rows}”. This is the default, and is probably very close to what you are looking for. I think it works intuitively, and has the bonus of helping people see query parameters as something more than a dumping ground.
Person: I tossed in the 'multi' keyword from MooseX::MultiMethods but I think you get the idea. Again, here's a nice bonus to using Controllers that are just Moose classes , we can take advantage of this great module, and whatever else the future brings along. In this case both methods want a single positioned parameter, which the URL mapper converts into a path part.
Create: This one is the most challenging, but my idea here is that if the final argument in a signature is a collection like type, that means it's a POST or PUT listener, and expects an entity body. We need a bit more work here, but I think it's very doable. Imagine how useful this would be for dealing with uploaded entities as well!
Well, how might you perform the hinting I mentioned. Right now I think this could fit into either the config or possibly as a meta description;
class Person {
meta_description(
actions=>{
person->{
## would change url to: $base/people/{id}
uri => '{base}/people/{id}',
},
}
);
## all the rest...
}
Again, this is still a mock up, but the idea I think is workable, and feels clean to me.
Another intriguing possibility is that once you cleanly separate your Web decorated controllers from the core logic being expressed, it easy to imagine how you could use the same inference to build command line applications. For example, the same system above might reflect to a command line tool as:
myapp.pl people.gallery --page=1 –rows=10
My thinking here is that you'd create a base 'MyApp::People' and then depending on what engines you set up in the configuration, it would generate a 'MyApp::Web::Controller::People' or a 'MyApp::App::Controller::People', or even 'MyApp::SOAP::People', and so on.
Final Thoughts.
I'm opposed to creating a new solution for something that already exists in Perl or that exists in a sufficiently large community (such as Moose). We have a big enough learning curve with Moose and related, and I think the more we add in, the worse off we are. My feeling is that controllers should be the minimal decoration on top of plain old Perl necessary to map URIs to the work required. This would make is possible to leverage the entire existing Moose tool chain as well as the collected wisdom of Moose development best practices. Additionally, since Moose has such a rich set of introspection ability, this should make it possible to make a reasonable inference as to how a given controller maps to a URI even without any additional meta information.
Overall, this has been a thought experiment. There's no code behind it yet. My goal here is to see if I can get you to think differently about how your projects are organized and see if we can come up with something that really is ahead of the the current web development architectures. I really believe that although Model – View – Controller is a solid paradigm for web development, our implementations of it have so far been too tyrannical and have gone down paths that cause confusion as well as make it hard for Perl programmers to use the knowledge they already have. A system as described above would avoid those issues, yet increase options and flexibility, while making it easier to just get started.
Comments