RFC: Catalyst::Controller::RHTMLO

6 views
Skip to first unread message

Jason Gottshall

unread,
Jan 22, 2009, 5:52:31 PM1/22/09
to The elegant MVC web framework, rose-htm...@googlegroups.com
[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;


John Siracusa

unread,
Jan 22, 2009, 8:32:01 PM1/22/09
to rose-htm...@googlegroups.com
On Thu, Jan 22, 2009 at 5:52 PM, Jason Gottshall <jgott...@capwiz.com> wrote:
> 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.

A nit: Rose::Object is just a simple object base class. The
"framework" (such as it is) would be called just "Rose."

Otherwise, what you've described sounds a lot like how I use RHTMLO in
my web apps: storing and retrieving forms by name (albeit without the
raw hash access of the Catalyst stash ;) I also keep my forms around
between requests (I have web app objects, which each have form
objects) and merely reset them after each use. There's much less
memory and CPU churn that way. With Catalyst, "app objects" are
probably out of the question, but you might still be able store the
form objects as class data in the controller. Just something worth
considering.

-John

Reply all
Reply to author
Forward
0 new messages