DEV Community

John Napiorkowski
John Napiorkowski

Posted on

Catalyst Tricks: Map Request Parameters to a Model

Introduction

Dealing with incoming request parameters (both query and body parameters) is something nearly all Perl Catalyst applications need to cope with. Unfortunately Catalyst punts here and doesn't give you a lot of guidance and the built in handling leaves a lot to be desired. In this blog I will first example how the default handling works, some of the problems with it and how Catalyst developers have tried to improve it over the years (with minor success IMHO; I can say that since half the redos are my fault ;)).

How Catalyst Handles Request Bodies and Query Parameters

By default incoming query and body parameters get mapped to the Catalyst Request object:

  $c->request->query_parameters
  $c->request->body_parameters
Enter fullscreen mode Exit fullscreen mode

query_parameters gives you access to parameters passed in the 'query' section of your request URL. For example if your URL is https://example.com/page/?aaa=1&bbb=2 then query_parameters will return the following hashref:

  +{
    aaa => "1",
    bbb => "2",
  }
Enter fullscreen mode Exit fullscreen mode

The body_parameters method gives you access to classic HTML Form POST bodies. For example if you have an HTML Form like this:

<form action="/login" method="post">
    <label for="username">Username:</label>
    <input type="text" id="username" name="username">
    <label for="password">Password:</label>
    <input type="password" id="password" name="password">
    <button type="submit">Login</button>
</form>
Enter fullscreen mode Exit fullscreen mode

When the user clicks the submit button you would expect the following hashref in body_parameters:

  +{
    username => "$USERNAME",
    password => "$PASSWORD",
  }
Enter fullscreen mode Exit fullscreen mode

(Substitute $USERNAME and $PASSWORD for whatever the user typed into the form).

Both method return a hashref of key value pairs where the key in the field or parameter and the value is a scalar or arrayref (depending on if there is one or several values for the given field in the request).

For basic applications this has worked acceptably but there's a number of issues. First of all the fact that the key can be either a scalar or arrayref is annoying, requiring you to write tons of defensive code like:

my $username = $c->req->body_parameters->{username};
$username = ref $username eq 'ARRAY' ? $username[-1] : ($username);
Enter fullscreen mode Exit fullscreen mode

Or just ignore the problem and potentially open yourself to security issues. Speaking of security issues I don't know how many times I've seen code like this, passing incoming body parameters straight into a DBIx::Class object:

  my $new_user = $c->model('Schema::User')->create($c->req->body_parameters);
Enter fullscreen mode Exit fullscreen mode

This is a world of hurt since you are basically passing whatever the user submitted (or your site hacker is submitting) directly to DBIC create. You need to be more choosey about the incoming at the very least:

  my $new_user = $c->model('Schema::User')->create(
    username => ref($c->req->body_params->{username}) eq 'ARRAY' ? $c->req->body_params->{username}[-1] : $c->req->body_params->{username},
    password => ref($c->req->body_params->{password}) eq 'ARRAY' ? $c->req->body_params->{password}[-1] : $c->req->body_params->{password},
  );
Enter fullscreen mode Exit fullscreen mode

At which point you are starting to have a lot of ugly code and you haven't even started on form validation yet. And with all this repeated code its easy to have a hard to spot typo:

  my $new_user = $c->model('Schema::User')->create(
    username => ref($c->req->body_params->{username}) eq 'ARRAY' ? $c->req->body_params->{usrname}[-1] : $c->req->body_params->{usernme},
  ...
Enter fullscreen mode Exit fullscreen mode

I've seen a lot of typo issues in Catalyst applications just like this, and they can lead to hard to spot problems since in Perl having a typo in the hash de-reference will not lead to a hard runtime error generally, you just get 'undef' for a value in an unexpected location. I've seen this problem in Catalyst code which existed for years in the wild.

Another thing I've seen a lot of is line after line of parameter processing code stuck into controllers. As it turns out parameter munging is one of the bigger jobs a programmer in a web application can have, especially as the application gets older and you need to introduce new features without breaking backward compatibility. This can lead to very long and ugly controllers that make following the flow of logic in your request to response cycle difficult.

You can solve the 'is it a value or an arrayref?' problem by enabled the use_hash_multivalue_in_request configuration option. This gives you a Hash::MultiValue object instead of a hashref of request parameters. Amongst other things it make it easy to say 'when there's more than one value give me only the last one', which is nearly always the right thing as legitimate uses for this typically revolve around HTML Form tricks where some field types like checkboxes don't make it easy to know when the user is explicitly setting an 'off' state. See CONFIGURATION For more. This however doesn't really help with the other problems such as easy to make typos or dirtying up your controllers with tons of parameter tweaking code.

Mapping Incoming Request Parameters to a Model

One trick I've used for years when encountering this issue is to use a Catalyst Model as a container for my request parameters. This model converts the hashref to an actual object with methods, which means any typos get picked up at runtime fast. This model is also a great place to stick validations and incoming value filters, as well as a good spot to stick and complex logic involving these parameters. Let's keep this simple and just see how one might do that for the example login form already described:

package Example::Model::Params::Login;

use Moose;
use Valiant::Validations;
use Valiant::Filters;

extends 'Catalyst::Model';
with 'Catalyst::Component::InstancePerContext';

sub build_per_context_instance {
  my ($self, $c) = @_;
  my $body = ref($self)->new(%{$c->req->query_parameters}, ctx=>$c);
  return $body->validate; # ->validate returns '$self' for chaining
}

has ctx => (is=>'ro');

has username => (
  is => 'ro',
  validates => [
    presence => 1,
    length => {
      maximum => 64,
      minimum => 1,
    },
  ],
);

has password => (
  is => 'ro',
  validates => [
    presence => 1,
    length => {
      maximum => 64,
      minimum => 1,
    },
  ],
);

has user => (
  is => 'ro',
  lazy => 1,
  predicate => 'has_user',
  default => \&_find_user,
  validates => [
    presence => {message=>'User Not Found with credentials.'},
  ],
)

filters_with 'Truncate', max_length=>100;

sub _find_user {
  my $self = shift;
  my $user = $self->ctx
    ->model('Schema::User')
    ->find({username=>$self->username});

  return unless $user && $user->password_eq($self->password);  
  return $user;
}
Enter fullscreen mode Exit fullscreen mode

This cleanly encapsulates the entire job of getting the POSTed parameters, making sure they are valid and that the parameters match a user in the database (and that the given password matches the latest in the DB via the password_eq method, which is an exercise I leave for you; don't forget to hash your passwords in the DB!).

You can use it in a controller similar to:

package Example::Controller::Session;

use Moose;
use MooseX::Attributes;

extends 'Catalyst::Controller';

sub login :Path Args(0) {
  my ($self, $c) = @_;
  my $params = $c->model('Params::Login');
  return $c->login_user($params->user) if $params->valid;
  return $c->stash(params => $params);
}
Enter fullscreen mode Exit fullscreen mode

In this example we map the incoming request body to $params and if the object is valid we perform the login workflow (via login_user($user), a method that again I leave to your imagination but probably involves storing the user id in the session and redirecting to some sort of "You're Logged in" page). If it not we stick $params in the stash and let the view inspect it for the errors, displaying such to the user in whatever view system you prefer.

Conclusion

In real life you'd probably use the Authentication plugin for something like this, but the general idea here can map to basically any type of incoming query or request bodies, even those via APIs requests that might be in JSON rather than a form POST. What you get is a nice, clear separation of concerns that improves code readability and long term maintainability. I like the idea so much, and started using it so much that I've tried to encapsulate the pattern in CatalystX::RequestModel. Other approaches on CPAN that can do similar would be HTML::FormHandler, although that tends to focus more on validation and HTML Form field generation, so might be a bigger hammer than you want.

Modules mentioned in this blog include Catalyst and Valiant

Bonus Idea

I often use a similar approach to wrap the Catalyst session (also represented as a hash reference) in a model, to offer a strongly typed interface to the session. Can you figure out the code for that?

Top comments (0)