[Catalyst] RFC: Catalyst::Controller::RHTMLO
Jason Gottshall
jgottshall at capwiz.com
Thu Jan 22 22:52:31 GMT 2009
[Cross-posted to catalyst-users and rhtmlo lists]
I know there are several modules out there that hook up rhtmlo and
catalyst, but none of them do what I want. They all seem to do too much:
connect to a CRUD API, interface with rdbo, build a form object from a
config file, etc. I really just need a simple glue to load
rhtmlo-derived form classes into my catalyst controller actions and
initialize them with any query params, so I created a base controller
and an action class that take care of the details. It works for me, but
I thought it might be useful to the larger community, so I made it
configurable and documented it pretty thoroughly. But before I pollute
CPAN with one more piece of cruft, I want to be sure it's
sensible/useful. The two packages are defined below. Comments would be
appreciated.
package Catalyst::Controller::RHTMLO;
use strict;
use warnings;
use base 'Catalyst::Controller';
use MRO::Compat; # to get $self->next::method() right
=head1 NAME
Catalyst::Controller::RHTMLO - Catalyst Base Controller for
Rose::HTML::Objects forms
=head1 SYNOPSIS
package MyApp::Controller::Books;
use base 'Catalyst::Controller::RHTMLO';
# loads MyApp::Form::Book (which isa Rose::HTML::Form)
sub edit : Local Form('Book') {
my ( $self, $c ) = @_;
# form object is already init'ed with params and stashed
my $form = $c->stash->{form};
if ( $form->was_submitted ) {
if ( $form->validate ) {
# write to db or whatever
}
else {
# show errors or whatever
}
}
}
# display two search forms on same page
sub search : Local Form('ByAuthor,ByTitle') {
my ( $self, $c ) = @_;
if ( $c->stash->{forms}->{ByAuthor}->was_submitted ) {
# look up books by author
}
elsif { $c->stash->{forms}->{ByTitle}->was_submitted ) {
# look up books by title, duh
}
}
=head1 DESCRIPTION
This base controller glues Catalyst actions to form classes derived from
L<Rose::HTML::Form>, a component of John Siracusa's excellent
L<Rose::Object>
framework. Unlike some other form-loading modules (see L</"PRIOR ART">),
this
one does not include any mechanism for defining form structures; it merely
loads, instantiates, and initializes pre-written form classes for use in
your
controllers.
In order to utilize a particular form in a particular Catalyst action,
simply declare an attribute on the subroutine:
sub edit : Local Form('Book') { }
This will ensure that MyApp::Form::Book is loaded and initialized,
basically
equivalent to the following:
my $form = MyApp::Form::Book->new();
$form->params($c->req->params);
$form->init_fields;
$c->stash->{form} = $form;
The namespace used to complete the form class name is
L<configurable|/CONFIGURATION>, or you can specify a full package name by
prepending a 'plus' sign:
sub edit : Local Form('+My::FormClasses::Book') { }
To display more than one distinct form on a page, just list them all in the
attribute, delimited with commas or spaces:
sub search : Local Form('ByAuthor,ByTitle,BySubject') {
my ($self, $c) = @_;
$c->stash->{forms}{ByAuthor}->action($c->uri_for('/search/byauthor'));
$c->stash->{forms}{ByTitle}->method('GET');
$c->stash->{forms}{BySubject}->name('bytopic');
}
(Note that you must put all the form names inside one set of quotes;
I<DO NOT>
try to quote each individual form name. This is a limitation of perl5's
subroutine attributes.) The first form listed will still be stored in
the stash
in the usual location; I<all> the forms (including the first) will be
stored
under a separate stash key, in a hash keyed to the name used to load them.
If for some reason you need to render the same form more than once on a
page,
just list it again:
sub search : Local Form('Search,Search') {
my ($self, $c) = @_;
$c->stash->{forms}{Search}[0]->name('search_0');
$c->stash->{forms}{Search}[1]->name('search_1');
}
In this (weird but possible) case, the forms will be put into an
arrayref in
the expected location. I haven't actually attempted to use this
technique in
production, so I don't know what else you might have to do to make it
work...
=head1 CONFIGURATION
You can override many defaults using Catalyst's configuration mechanism:
__PACKAGE__->config(
'Controller::RHTMLO' => {
form_attr => 'HasForm',
action_class => 'MyApp::Action::RoseForm',
stash_name => 'formobj',
stash_hash_name => 'allforms',
form_prefix => 'MyApp::RoseForm',
}
);
=over
=item C<action_class> (default 'Catalyst::Controller::RHTMLO::Action')
If you want to add more functionality to the automatic form loading and
initialization, you can create your own action class:
package MyApp::Action::RoseForm;
use base 'Catalyst::Controller::RHTMLO::Action';
sub execute {
my $self = shift;
my ($controller, $c, @args) = @_;
# load forms via base class
$self->next::method(@_);
# do cool stuff
$c->stash->{form}->add_fields(
secure_token => {
type => 'hidden',
value => $c->some_cool_security_token
}
);
return;
}
=item C<form_attr>
Default: 'Form'. Set this to alter the subroutine attribute used to
indicate
one or more forms to be loaded by a given action, e.g.:
sub edit : Local HasForm('Books') { }
=item C<form_prefix>
Default: 'MyApp::Form' (using your app's actual name). Set this to the
namespace where your Rose::HTML::Form subclasses live.
=item C<stash_hash_name>
Default: 'forms'. Sets the stash key under which all forms for a given
action
will be stored by class name.
=item C<stash_name>
Default: 'form'. Sets the stash key under which the first form for a given
action will be stored.
=head1 PRIOR ART
There are several other modules on CPAN that do similar things, many having
inspired this module in various ways.
=over
=item L<Catalyst::Controller::FormBuilder>
Provided a lot of insight into how to trigger the form loading process
with a
custom subroutine attribute. Based on L<CGI::FormBuilder> rather than
Rose::HTML::Form.
=item L<CatalystX::RoseIntegrator>
Looks like it uses a CGI::FormBuilder-style config file to construct
Rose::HTML::Form objects on the fly, rather than having static
subclasses. Also
seems to include direct model integration with L<Rose::DB::Object>.
=item L<CatalystX::CRUD::Controller::RHTMLO>
A component that enables use of Rose::HTML::Form objects with Peter
Karman's
cool L<CatalystX::CRUD> API.
=back
=head1 SEE ALSO
L<Rose::HTML::Form>, L<Rose::HTML::Objects>, L<Rose::Object>,
L<Catalyst::Controller>, L<Catalyst::Action>, L<Catalyst>
=head1 AUTHOR
Jason Gottshall <jgottshall att capwiz dott com>
=head1 LICENSE
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
=cut
sub create_action {
my ($self, %args) = @_;
my $config = $self->config->{'Controller::RHTMLO'};
my $form_attr = $config->{'form_attr'} || 'Form';
if( exists $args{attributes}{$form_attr} ) {
my $action_class = $config->{'action_class'} ||
'Catalyst::Controller::RHTMLO::Action';
push @{ $args{attributes}{ActionClass} }, $action_class;
if(my $val = delete $args{attributes}{$form_attr}) {
$args{_form_class} = $val;
}
}
return $self->next::method(%args);
}
package Catalyst::Controller::RHTMLO::Action;
use strict;
use warnings;
use base 'Catalyst::Action';
use MRO::Compat;
use Catalyst::Utils;
__PACKAGE__->mk_accessors(qw/_form_class/);
sub execute {
my $self = shift;
my ($controller, $c, @args) = @_;
$self->get_forms($c);
return $self->next::method(@_);
}
sub get_forms {
my ($self, $c) = @_;
# sanity check; ensure we actually declared form class
return unless $self->_form_class && ref $self->_form_class eq 'ARRAY';
# form classes are delimited by spaces or commas
my @classes = split /[ ,]+/, $self->_form_class->[0];
unless(@classes) {
$c->log->warn('No form class specified for action "' .
$self->reverse . '"');
return;
}
my $config = $c->config->{'Controller::RHTMLO'};
my $stash_name = $config->{'stash_name'} || 'form';
my $stash_hash = $config->{'stash_hash_name'} || 'forms';
my $form_prefix = $config->{'form_prefix'} ||
$c->config->{'name'} . '::Form';
$form_prefix .= '::';
foreach my $name (@classes) {
next unless $name; # ignore leading/trailing delimiters
my $class = $name;
# allow for full class names with leading '+'
$class = $form_prefix . $class unless $class =~ s/^\+//;
Catalyst::Utils::ensure_class_loaded($class);
$c->log->debug("Loading form '$class'");
# setup form
my $form = $class->new();
$form->params($c->req->params);
$form->init_fields;
# put form in stash under its name
if(my $prev_form = $c->stash->{$stash_hash}->{$name}) {
# multiple instances of same form class are stored in arrayref
$c->stash->{$stash_hash}->{$name} = [$prev_form]
unless ref $prev_form eq 'ARRAY';
push @{$c->stash->{$stash_hash}->{$name}}, $form;
}
else {
$c->stash->{$stash_hash}->{$name} = $form;
}
# create shortcut to "main" form
$c->stash->{$stash_name} ||= $form;
}
return;
}
1;
More information about the Catalyst
mailing list