Created
May 14, 2010 19:09
-
-
Save jjn1056/401514 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package Catalyst::ActionRole::FindDBICResult; | |
use Moose::Role; | |
use namespace::autoclean; | |
## Lots of unwritten code :) | |
1; | |
=head1 NAME | |
Catalyst::ActionRole::FindDBICResult | |
=head1 SYNOPSIS | |
Assuming "model("DBICSchema::User") is a L<DBIx::Class::ResultSet>, we can | |
replace the following code: | |
sub user :Path :Args(1) { | |
my ($self, $ctx, $photo_id) = @_; | |
my $photo; | |
eval { | |
$photo = $ctx->model('DBICSchema::User')->find({user_id=>$photo_id}); | |
1; | |
} or $ctx->log->error("Error finding User: $@"); | |
if($photo) { | |
## You Found a Photo, do something useful... | |
} else { | |
## You didn't find a photo (or got an error). | |
$ctx->go('/error/not_found'); | |
} | |
} | |
With this code: | |
__PACKAGE__->config( | |
action_args => { | |
user => { store => 'Schema::User' }, | |
} | |
); | |
sub user :Path :Args(1) | |
:ActionRole('FindsDBICResult') | |
{ | |
my ($self, $ctx, $arg) = @_; | |
## This is always executed, and is done so first. | |
} | |
sub user_FOUND :Action { | |
my ($self, $ctx, $user) = @_; | |
} | |
sub user_NOTFOUND :Action { | |
my ($self, $ctx, $arg) = @_; | |
$ctx->go('/error/not_found') | |
} | |
sub user_ERROR :Action { | |
my ($self, $ctx, $error, $arg) = @_; | |
$ctx->log->error("Error finding User: $error") | |
} | |
Another example this time with Chained actions: | |
__PACKAGE__->config( | |
action_args => { | |
user => { | |
store => { stash_key => 'user_rs' }, | |
find_condition => { columns => ['email'] }, | |
auto_stash => 1, | |
handlers => { | |
notfound => { detach => '/error/notfound' }, | |
}, | |
} | |
); | |
sub root :Chained :CaptureArgs(0) { | |
my ($self, $ctx) = @_; | |
$ctx->stash(user_rs=>$ctx->model('DBICSchema::User')); | |
} | |
sub user :Chained('root') :CaptureArgs(1) | |
:ActionRole('FindsDBICResult') {} | |
sub details :Chained('user') :Args(0) | |
{ | |
use Data::Dumper; | |
my ($self, $ctx, $arg) = @_; | |
my $user_details = $ctx->stash->{user}; | |
## Do something with the details, probably delagate to a View, etc. | |
} | |
Please see the test cases for more detailed examples. | |
=head1 DESCRIPTION | |
Mapping incoming arguments to a particular result in a L<DBIx::Class> based model | |
is a pretty common development case. Making choices based on the return of that | |
result is also quite common. The goal of this action role is to reduce the | |
amount of boilerplate code you have to write to get these common cases completed. | |
Additionally, by canonicalizing how to handle these common cases, we hope to | |
increase code comprehension familiarity as well as leverage the many eyes of | |
the community to solve bugs and create a solid solution suitable for reuse. | |
Basically we encapsulate the logic: "For a given DBIC resultset, does the find | |
condition return a valid result given the incoming arguments? Depending on the | |
result, follow a chain of assigned handlers until the result is handled." | |
A find condition basically maps incoming action arguments to a DBIC unique | |
constraint. This condition resolves to one of three results: "FOUND", | |
"NOTFOUND", "ERROR". Result condition "FOUND" returns when the find condition | |
finds a single row against the defined ResultSet, NOTFOUND when the find | |
condition fails and ERROR when trying to resolve the find condition results | |
in a catchable thrown error. | |
Based on the result condition we automatically call an action whose name | |
matches a default template, as in the SYNOPSIS above. You may also override | |
this default template via configuration. This makes it easy to configure | |
common results, like NOTFOUND, to be handled by a common action. | |
Be default an ERROR result also calls a NOTFOUND (after calling the ERROR | |
handler), since both conditions logically match. | |
When dispatching a result condition, such as ERROR, FOUND, etc., to a handler, | |
we follow a hierachy of defaults, followed by any handlers added in configuration. | |
The first matching handler takes the request and the remaining are ignored. | |
It is not the intention of this action role to handle 'kitchen sink' tasks | |
related to accessing the your DBIC model. If you need more we recommend looking | |
at L<Catalyst::Controller::DBIC::API> for general API access needs or for a | |
more complete CRUD setup check out L<CatalystX::CRUD> or L<Catalyst::Plugin::AutoCRUD>. | |
=head1 ATTRIBUTES | |
This role defines the following attributes | |
=head2 store | |
This defines the method by which we get a L<DBIx::Class::ResultSet> suitable | |
for applying a L</find_condition>. The canonical form is a HashRef where the | |
keys / values conform to the following template. | |
=over 4 | |
=item {model => '$dbic_model_name'} | |
Store comes from a L<Catalyst::Model::DBIC::Schema > based model. | |
__PACKAGE__->config( | |
action_args => { | |
user => { | |
store => { model => 'DBICSchema::User' }, | |
}, | |
} | |
); | |
This retrieves a L<DBIx::Class::ResultSet> via $ctx->model($dbic_model_name). | |
This is the default common case. | |
=item {method => '$get_resultset'} | |
Calls a method on the containing controller. | |
__PACKAGE__->config( | |
action_args => { | |
user => { | |
store => { method => 'get_user_resultset' }, | |
}, | |
} | |
); | |
The containing controller must define this method and it must return a proper | |
L<DBIx::Class::ResultSet> or an exception is thrown. | |
=item {stash_key => '$name_of_stash_key' } | |
Looks in $ctx->stash->{$name_of_stash_key} for a resultset. | |
__PACKAGE__->config( | |
action_args => { | |
user => { | |
store => { stash_key => 'user_rs' }, | |
}, | |
} | |
); | |
This is useful if you are descending a chain of actions and modifying or | |
restricting a resultset based on the context or other logic. | |
=back | |
NOTE: We also automatically coerce a Str value of $str to {model => $str}, since | |
this is a common case. For example | |
__PACKAGE__->config( | |
action_args => { | |
user => { | |
## Internally coerced to "store => {model=>'DBICSchema::User'}". | |
store => 'DBICSchema::User', | |
}, | |
} | |
); | |
=head2 find_condition | |
This should a way for a given resultset (defined in L</store> to find a single | |
row. Not finding anything is also an accepted option. Everything else is some | |
sort error. | |
Canonically, the find condition is an arrayref of unique constraints, as | |
defined in L<DBIx::Class::ResultSource> either with 'set_primary_key' or with | |
'add_unique_constraint'. for example: | |
## in your DBIx::Class ResultSource | |
__PACKAGE__->set_primary_key('category_id'); | |
__PACKAGE__->add_unique_constraint(category_name_is_unique => ['name']); | |
## in your (canonical expressed) L<Catalyst::Controller> | |
__PACKAGE__->config( | |
action_args => { | |
category => { | |
store => {model => 'DBICSchema::Category'}, | |
find_condition => [ | |
'primary', | |
'category_name_is_unique', | |
], | |
} | |
} | |
); | |
sub category :Path :Args(1) :ActionRole('FindsDBICResult') { | |
my ($self, $ctx, $category_arg) = @_; | |
} | |
sub category_FOUND :action {} | |
sub category_NOTFOUND :action {} | |
sub category_ERROR :action {} | |
In this example $category_arg would first be checked as a primary key, and then | |
as a category name field. This allows you a degree of polymorphism in your url | |
design or web api. | |
Each unique constraint refers to one or more columns in your database. Incoming | |
args to an action are mapped to columns by the order they are defined in the | |
primary key or unique constraint condition, or in a configured order. Example | |
of reordering multi field unique constraints: | |
## in your DBIx::Class ResultSource | |
__PACKAGE__->add_unique_constraint(user_role_is_unique => ['user_id', 'role_id']); | |
## in your L<Catalyst::Controller> | |
__PACKAGE__->config( | |
action_args => { | |
user_role => { | |
store => {model => 'DBICSchema::UserRole'}, | |
find_condition => [ | |
{ | |
constraint_name => 'category_name_is_unique', | |
match_order => ['role_id','user_id'], | |
} | |
], | |
} | |
} | |
); | |
Additionally since most developers don't bother to name their unique constraints | |
we allow you to specify a constraint by its column(s): | |
## in your DBIx::Class ResultSource | |
__PACKAGE__->add_unique_constraint(['user_id', 'role_id']); | |
## in your L<Catalyst::Controller> | |
__PACKAGE__->config( | |
action_args => { | |
user_role => { | |
store => {model => 'DBICSchema::UserRole'}, | |
find_condition => [ | |
{ | |
columns => ['user_id','role_id'], | |
match_order => ['role_id','user_id'], | |
} | |
], | |
} | |
} | |
); | |
sub role_user :Path :Args(2) { | |
my ($self, $ctx, $role_id, $user_id) = @_; | |
} | |
Please note that 'columns' is used merely to discover the unique constraint | |
which has already been defined via 'add_unique_constraint'. You cannot name | |
columns which are not already marked as fields in a unique constraint or in a | |
primary key. Additionally the order of columns used in 'columns' is not | |
relevent or meaningful; if you need to control how your action args order map | |
to DBIC fields, use 'match_order' | |
We automatically handle the common case of mapping a single field primary key | |
to a single argument in a controller "Args(1)". If you fail to defined a | |
find_condition this is the default we use. See the L<SYNOPSIS> for this | |
example. | |
This is an API overview, please see L</FIND CONDITIONS DETAILS> for more. | |
=head2 detach_exceptions | |
detach_exceptions => 1, # default is 0 | |
By default we $ctx->forward to expection handlers (NOTFOUND, ERROR), which we | |
believe gives you the most flexibility. You can always detach within a handling | |
action. However if you wish, you can force NOTFOUND or ERROR to detach instead | |
of forwarding by setting this option to any true value. | |
=head2 auto_stash | |
If this is true (default is false), upon a FOUND result, place the found | |
DBIC result into the stash. If the value is alpha_numeric, that value is | |
used as the stash key. if its either 1, '1', 'true' or 'TRUE' we default | |
to the name of the method associated with the consuming action. For example: | |
__PACKAGE__->config( | |
action_args => { | |
user => { store => 'DBICSchema::User', auto_stash => 1 }, | |
}, | |
); | |
sub user :Path :Args(1) { | |
my ($self, $ctx, $user_id) = @_; | |
## $ctx->stash->{user} is defined if $user_id is found. | |
} | |
This could be combined with the L</handlers> attribute to make fast mocks and | |
prototypes. See below | |
=head2 handlers | |
Expects a HashRef and is optional. | |
By default we delegate result conditions (FOUND, NOTFOUND, ERROR) to an action | |
from a list of predefined options. These predefined options work very similarly | |
to L<Catalyst::Action::REST>, so if you are familiar with that system this will | |
seem very natural. | |
First we try to match a result to an action specific handler, which follows the | |
template $action_name .'_'. $result_condition. So for an action named 'user' | |
which is consuming this role, there could be actions 'user_FOUND', 'user_NOTFOUND', | |
'user_ERROR' which would get $ctx->forwarded too AFTER executing the body of | |
the consuming action. | |
If this template fails to match (as in you did not define such an action in | |
the same L<Catalyst::Controller> subclass as your consuming action) we then | |
look for a 'global' action in the controller, which is in the form of an action | |
named $result_condition (basically actions named FOUND, NOTFOUND or ERROR). | |
This could be useful if you wish to centralize control of execeptional | |
conditions. For example you could create a base controller or controller role | |
that defined the "NOTFOUND" or "ERROR" actions and then extend or consume that | |
into the controller containing actions using this action role. | |
However there may be cases where you need direct control over the action that | |
get's called for a given result condition. In this case you can add handlers | |
to the end of the lookup list for a given result condition. This is a HashRef | |
that accepts one or more of the following keys: found, notfound, error. Example: | |
handlers => { | |
found => { forward|detach => $found_action_name }, | |
notfound => { forward|detach => $notfound_action_name }, | |
error => { forward|detach => $error_action_name }, | |
} | |
Globalizing the 'error' and 'notfound' action handlers is probably the most | |
useful. Each option key within 'handlers' canonically takes a hashref, where | |
the key is either 'forward' or 'detach' and the value is the name of something we | |
can call "$ctx->forward" or "$ctx->detach" on. We coerce from a string value | |
into a hashref where 'forward' is the key (unless 'detach_exceptions' is true). | |
If youd actually set the key value, that value is used no matter what the state | |
of L</detach_exceptions>. | |
=head1 METHODS | |
This role defines the follow methods which subclasses may wish to override. | |
=head1 FIND CONDITION DETAILS | |
This section adds details regarding what a find condition is on provides some | |
examples. | |
=head2 defining a find condition | |
A find condition is the definition of something unique we can match and return | |
a single row or result. Basically this is anything you'd pass to the 'find' | |
method of L<DBIx::Class::ResultSet>. | |
Canonically a find_condition is an ArrayRef of key limited HashRefs, but we | |
coerce from some common cases to make things a bit easier. Examples follow. | |
By default we automatically handle the most common case, where a single argument | |
maps to a single column primary key field. In every other case, such as when | |
you have multi field primary keys or you are finding by an alternative unique | |
constraint (either single or multi fields) you need to declare the name of the | |
L<DBIx::Class::ResultSource> unique constraint you are matching against. Since | |
L<DBIx::Class> does not require you to name your unique constraints (many people | |
let the underlying database follow its default convention in this matter), | |
instead of a unique constraint name you may pass an ArrayRef of one or more | |
columns which together define a uniqiue nstraint. Please note if you use this | |
form of defining a find condition, you must use an ArrayRef EVEN if your condition | |
has only a single column. | |
Also note that in the case of multi field primary keys or unique constraints, | |
we attempt to match against the field order as defined in your call to | |
L<DBIx::Class::ResultSource/primary_columns> or L<DBIx::Class::ResultSource/add_unique_constraint>. | |
If you need to to specify the mapping of L<Catalyst> arguments to unique | |
constraint fields, please see 'match_order' options. | |
=head2 example find conditions | |
Find where one arg is mapped to a single field primary key (default case). | |
__PACKAGE__->config( | |
action_args => { | |
photo => { | |
store => 'Schema::User', | |
find_condition => 'primary', | |
} | |
} | |
); | |
BTW, the above would internally 'canonicalize' the find_condition to: | |
find_condition => [{ | |
constraint_name=>'primary', | |
columns=>['user_id'], | |
match_order=>['user_id'], | |
}], | |
Same as above but the find condition can be any of several named constraints, | |
all of which have the same number of fields. In this case we'd expect the | |
underlying User ResultSource to define a primary key and a unique constraint | |
named 'unique_email'. | |
__PACKAGE__->config( | |
action_args => { | |
photo => { | |
store => 'Schema::User', | |
find_condition => ['primary', 'unique_email'], | |
} | |
} | |
); | |
Same as above, but the unique email constraint was not named so we need to map | |
some fields to a unique constraint. Please note we actually look for a unique | |
constraint using the named columns, failed matches throw an expection. | |
__PACKAGE__->config( | |
action_args => { | |
photo => { | |
store => 'Schema::User', | |
find_condition => ['primary', {columns=>['email']}], | |
} | |
} | |
); | |
An example where the find condition is a mult key unique constraint. | |
__PACKAGE__->config( | |
action_args => { | |
photo => { | |
store => 'Schema::User', | |
find_condition => {columns=>['user_id','role_id']}, | |
} | |
} | |
); | |
As above but lets you specify an argument to field order mapping which is | |
different from that defined in your L<DBIx::Class::ResultSource>. This let's | |
you decouple your L<Catalyst> action arg definition from your L<DBIx::Class::ResultSource> | |
definition. | |
__PACKAGE__->config( | |
action_args => { | |
photo => { | |
store => 'Schema::User', | |
find_condition => { | |
columns=>['user_id','role_id'], | |
match_order=>['role_id','user_id'], | |
}, | |
} | |
} | |
); | |
This last would internally canonicalize to: | |
__PACKAGE__->config( | |
action_args => { | |
photo => { | |
store => {model => 'Schema::User'}, | |
find_condition => [{ | |
constraint_name=>'fk_user_id_fk_role_id', | |
columns=>['user_id','role_id'], | |
match_order=>['role_id','user_id'], | |
}], | |
} | |
} | |
); | |
Please note the 'constraint_name' in this case is provided by the underlying | |
storage, the value given is a reasonable guess. | |
=head2 subroutine handlers versus action handlers | |
Based on the result of the find condition we try to invoke methods or actions | |
in the containing controller, based on a naming convention. By default we first | |
try to invoke an action based on the template $action."_".$result | |
=head1 NOTES | |
The following section is additional notes regarding usage or questioned related | |
to this action role. | |
=head2 Why an Action Role and not an Action Class? | |
Role are more flexible, you can combine many roles easily to compose flexible | |
behavior in an elegant way. This does of course mean that you will need a | |
more modern L<Catalyst> based on L<Moose>. | |
=head2 Why require such a modern L<Catalyst>? | |
We need a version of L<Catalyst that is post the L<Moose> migration; additionally | |
we need equal to or greater than version '5.80019' for the ability to define | |
'action_args' in a controller. See L<Catalyst::Controller> for more. | |
=head1 AUTHOR | |
John Napiorkowski <jjnapiork@cpan.org> | |
=head1 COPYRIGHT & LICENSE | |
Copyright 2010, John Napiorkowski <jjnapiork@cpan.org> | |
This program is free software; you can redistribute it and/or modify it under | |
the same terms as Perl itself. | |
=cut | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment