So I really like to follow DRY: Don't Repeat Yourself. In the development of Epitafio (A cemetery management system I mentioned earlier), I was workin on my model classes - note that this is not a DBIC model, but a regular model that do access a DBIC schema - and I realized that for every single method of the models I would need to do two things:
- Enclose code in a transaction, much like:
$schema->txn_do(sub { ... })
- Authorize the user against a specific role:
die 'Access denied!' unless $user->in_role('foo')
So I started wondering at #catalyst if there would be a pretty way of doing it. I was already using Catalyst::Component::InstancePerContext, but mst quickly guided me to avoid saving the context itself in the object, but rather getting the values I need from there. Since my app models will basically follow this same principle I did a model superclass with:
package Epitafio::Model;
use Moose;
with 'Catalyst::Component::InstancePerContext';
has 'user' => (is => 'rw');
has 'dbic' => (is => 'rw');sub build_per_context_instance {
my ($self, $c) = @_;
$self->new(user => $c->user->obj,
dbic => $c->model('DB')->schema->restrict_with_object($c->user->obj));
}
1;
Note that I'm still using the C::M::DBIC::Schema as usual, but I'm additionally making a local dbic schema that is restricted according with the logged user. Check DBIx::Class::Schema::RestrictWithObject for details on how that works, and mst++ for the tip.
Ok, now my model classes can know which user is logged in (in a Cat-independent way) as well as have access to the main DBIC::Schema used in the application. Now we just need to DRO - Don't Repeat Ourselves.
Following, again, mst++ tip, I decided against doing a more fancy solution and gone to a plain and simple:
txn_method 'foo' => authorize 'rolename' => sub {
...
}
For those who didn't get how that is parsed, this could be rewritten as:
txn_method('foo',authorize('rolename',sub { }))
This works as:
- authorize receives a role name and a code ref and returns a code ref that does the user role checking before invoking the actual code.
- txn_method receives the method name and a code ref and installs a new coderef that encloses the given coderef into a transcation in the package namespace as if it were a regular sub definition.
That means you can have a txn_method without authorization, but you would require
our &foo = authorize 'rolename' => sub { ... }
to get authorization without transaction. But as in my application I'll probably have both most of the time, I thought it should suffice the way it is.
But for the txn_method..authorize thing to parse, both subs need to be in the package namespace at BEGIN time, so to solve that, without having to re-type it every time, I wrote a simple Epitafio::ModelUtil module that exports this helpers.
package Epitafio::ModelUtil;
use strict;
use warnings;
use base 'Exporter';our @EXPORT = qw(txn_method authorized);
sub txn_method {
my ($name, $code) = @_;
my $method_name = caller().'::'.$name;
no strict 'refs';
*{$method_name} = sub {
$_[0]->dbic->txn_do($code, @_)
};
}sub authorized {
my ($role, $code) = @_;
return sub {
if ($_[0]->user->in_role($role)) {
$code->(@_);
} else {
die 'Access Denied!';
}
}
}1;
And now the code of the model looks just pretty and non-repetitive ;). See the sources for the full version.
Leave a comment