John Napiorkowski

NAME

Catalyst::ActionSignatures::Rationale - Why use this module?

OVERVIEW

The thinking that went into building Catalyst::ActionSignatures. What problems are we trying to solve with it? What are its pros and cons as compared to other prior art attempts.

DISCUSSION

Classically a Catalyst controller is just a plain old Perl class that inherits from Catalyst::Controller. 'Actions' are just normal subroutines that are decorated either via method attributes or via controller configuration to indicate to the dispatcher how an action or group of actions maps to an incoming request. Originally a request would have one main action for each request (although certain 'special' actions like begin, auto and end could also be added to the chain of actions associated with a request).

    package MyApp::Controller::Example;

    use Moose;
    use MooseX::MethodAttributes;

    extends 'Catalyst::Controller;

    ## http://localhost/example/myaction/$arg
    sub myaction :Local Args(1) {
      my ($self, $c, $arg) = @_;
    }

    __PACKAGE__->meta->make_immutable;

Later when chained actions were added, that gave you the ability to assemble a chain of command style tree of actions. This was useful when many actions shared some base common behaviors because it meant you could reuse behavior more easily:

    package MyApp::Controller::User;

    use Moose;
    use MooseX::MethodAttributes;

    extends 'Catalyst::Controller;

    sub find_user :Chained(/) PathPrefix CaptureArg(1) {
      my ($self, $c, $id) = @_;
      $c->stash(user =>
        $c->model('Database::User')->find($id) ||
          $c->detach('/not_found'));
    }

      sub show   :Chained('find_user') Args(0) { ... }
      sub edit   :Chained('find_user') Args(0) { ... }
      sub remove :Chained('find_user') Args(0) { ... }

    __PACKAGE__->meta->make_immutable;

In this example we have created three matchable URLs:

    http://localhost/user/$id/show
    http://localhost/user/$id/edit
    http://localhost/user/$id/remove

The action 'find_user' is responsible for matching an $id to a model, and the remaining actions use that prep work to finalize the response. This chain or responsibility style pattern can be used to great effect in simplifying your code (or making a horrible mess, its really up to you...)

NOTE The above example is not given as an example of the best way to create URLs for your application, but was choosen for brevity and hope of clarity. I'd personally recommend staying away from 'verby' path parts and use HTTP correctly!

However a few things can be noted. First of all the arguments passed to an action are not configurable (you need a custom action role to change them). They are always "($self, $c, @args)". This can lead to action code where you have to reach into $c a lot, which gets verbose and also makes it a little harder to test (you have to mock $c) and perhaps makes it harder on your maintainers since all the verbosity can sometimes hide the main thrust of the code.

For example, if an action is going to want a User model, why not make it easy to say "I want one"? I think on the code readers it makes it a lot more clear what the point of the action it (it does something with User...)

    sub myaction(User $user) {  ... }

That has the nice benefit that testing the subroutine just needs a $user, you've removed boilerplate lookup code and you can leverage the action match system to skip the whole 'see if the $id is in the database and if not return not_found.) Which leads to the second point. Ideally we'd want to use the dispatcher and to be 'declarative' or configured and not require boilerplate code. For example I have code like the following all over:

    sub find_user :Chained(/) PathPrefix CaptureArg(1) {
      my ($self, $c, $id) = @_;
      $c->stash(user =>
        $c->model('Database::User')->find($id) ||
          $c->detach('/not_found'));
    }

The pattern where I take some input (like arguments or POSTed form parameters) and have code to evaluate its truth in some way (like if the $id matches a row in a database or a bunch of form parameters meet some constraint) is pretty common. Ideally we'd be able to encapsulate that with a terse convention that is also reasonable flexible later down the road as your application complexity grows. Using Catalyst::ActionSignatures offers a possible solution.

    sub find_user( Database::User<Capture> $user) :Chained(/) PathPrefix { ... }

    sub default :Path { # User not found }

Note we can also skip declaring 'CaptureArgs(1) since Catalyst::ActionSignatures is smart enough to notice the Capture in the signature and add that to you action configuration for you. So we have a nice side effect that we can reduce our need for method attributes a bit as well.

So Catalyst::ActionSignatures does three things:

1) Least importantly it brings signatures into your project which reduces a bit of boilerplate code.

2) It enables you control what arguments the application passes to the action, enabling you to use the built in Catalyst dependency injection more easily and reduces the need to reach into $context all the time. I believe this will make you actions more simple and straightforward.

3) It lets you leverage the built in Catalyst dispatcher to evaluate those arguments and matche or fails to match an action based on the truth value of the requested signature dependencies. This should result in less boilerplate "If I have this, then do this, otherwise show a not found" code, again making your action more simple and straightforward on the reader.

I also hope that it encourage Catalyst developers to spend more time learning how to make good use of the built in dependency management features and to better model their application use cases.

SEE ALSO

Catalyst::Action, Catalyst, signatures, Catalyst::ActionRole::MethodSignatureDependencyInjection

AUTHOR

John Napiorkowski email:jjnapiork@cpan.org

COPYRIGHT & LICENSE

Copyright 2015, John Napiorkowski email:jjnapiork@cpan.org

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.