[Catalyst] RFC: Catalyst::Controller::RHTMLO
Jason Gottshall
jgottshall at capwiz.com
Fri Jan 30 23:03:59 GMT 2009
Ok, for those of you who have been waiting with bated breath, here's my
second attempt at a base controller for loading Rose::HTML::Form
classes. I've completely refactored/rewritten the configuration stuff
after a long chat with mst. I also eliminated the handling of multiple
instances of the same form in the same action, having determined that
it's just not a meaningful case. I did *not* implement any caching
mechanism, based on my concern about form objects being modified beyond
simple parameter initialization.
I think this version is a *lot* better, and I'd appreciate any
additional feedback before I package it up for CPAN. Thanks!
package Catalyst::Controller::RHTMLO;
use strict;
use warnings;
use base 'Catalyst::Controller';
use MRO::Compat; # to get $self->next::method() right
use Catalyst::Utils;
=head1 NAME
Catalyst::Controller::RHTMLO - Catalyst base controller for integrating
Rose::HTML::Form objects
=head1 SYNOPSIS
package MyApp::Controller::Books;
use base 'Catalyst::Controller::RHTMLO';
# loads MyApp::Form::Book, which should ->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 several search forms on same page
sub search : Local Form('ByAuthor') Form('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 L<Catalyst> actions to form classes derived
from L<Rose::HTML::Form>, a component of John Siracusa's burgeoning
L<Rose> 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 C<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 utilize more than one distinct form class in the same action, simply
declare additional attributes:
sub search : Local Form('ByAuthor') Form('ByTitle')
Form('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');
}
The first form listed will be stored in the stash in the usual location;
I<all> the forms (including the first) will be stored under a separate
(L<configurable|/CONFIGURATION>) stash key, in a hash keyed to the name
used to load them.
=head1 CONFIGURATION
You can override many defaults using Catalyst's configuration mechanism:
__PACKAGE__->config(
# settings for all controllers using this base
'Catalyst::Controller::RHTMLO' => {
form_attr => 'HasForm',
form_action_class => 'MyApp::Action::RoseForm',
form_stash_name => 'formobj',
form_stash_hash => 'allforms',
form_prefix => 'MyApp::RoseForm',
},
# settings for specific controllers in MyApp
'Controller::Foo' => { # or 'C::Foo' if MyApp is built that way
form_prefix => 'MyApp::FooForms',
},
);
=over
=item C<form_action_class>
Default: C<'Catalyst::Controller::RHTMLO::Action'>
If you want to add more functionality to the automatic form loading and
initialization, you can create your own custom 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: C<'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: C<'MyApp::Form'> (using your app's actual name)
Set this to the namespace where your Rose::HTML::Form subclasses live.
=item C<stash_hash>
Default: C<'forms'>
Sets the stash key under which all forms for a given action will be
stored, keyed according to the name used to load them.
=item C<stash_name>
Default: C<'form'>
Sets the stash key under which the first (and often I<only>) 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 and custom action class. Based on
L<CGI::FormBuilder> (rather than L<Rose::HTML::Form>).
=item L<CatalystX::RoseIntegrator>
Looks like it uses a L<CGI::FormBuilder>-style config file to construct
L<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 L<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>,
L<Catalyst::Controller>, L<Catalyst::Action>, L<Catalyst>
=head1 AUTHOR
Jason Gottshall <jgottshall att capwiz dott com>
=head1 ACKNOWLEDGEMENTS
mst: for patiently helping me make sense of Catalyst's configuration system
phaylon: for pointing me to existing documentation on extending components
=head1 LICENSE
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
=cut
__PACKAGE__->mk_accessors(
qw/
form_attr
form_action_class
form_stash_name
form_stash_hash
form_prefix
/
);
__PACKAGE__->config(
form_attr => 'Form',
form_action_class => 'Catalyst::Controller::RHTMLO::Action',
form_stash_name => 'form',
form_stash_hash => 'forms',
);
sub COMPONENT {
my ( $self, $c, $arguments ) = @_;
# merge global config with controller-specific config
#TODO refactor this into Moose role for Catalyst components
$arguments = $self->merge_config_hashes(
$c->config->{'Catalyst::Controller::RHTMLO'},
$arguments
);
# set default form prefix according to appname
$self->config->{form_prefix} = $c->config->{name} . "::Form";
return $self->next::method( $c, $arguments );
}
sub create_action {
my ( $self, %args ) = @_;
if ( my $formnames = delete $args{attributes}{ $self->form_attr } )
{
# ensure at least one form name is provided
@$formnames = grep {$_} @$formnames;
unless (@$formnames) {
die sprintf
q{Attribute '%s()' for action '/%s' must specify an
RHTMLO-based form class},
$self->form_attr, $args{reverse};
}
# validate and load form classes
foreach my $name (@$formnames) {
my $class =
$name =~ s/^\+//
? $name # leading plus indicates fully-qualified package
: join( '::', $self->form_prefix, $name );
Catalyst::Utils::ensure_class_loaded($class);
$args{form_classes}{$name} = $class;
}
# pass config to action
$args{stash_name} = $self->form_stash_name;
$args{stash_hash} = $self->form_stash_hash;
# set action class
push @{ $args{attributes}{ActionClass} }, $self->form_action_class;
}
return $self->next::method(%args);
}
1;
package Catalyst::Controller::RHTMLO::Action;
use strict;
use warnings;
use base 'Catalyst::Action';
use MRO::Compat;
use Catalyst::Utils;
__PACKAGE__->mk_accessors(qw/stash_name stash_hash form_classes/);
sub execute {
my $self = shift;
my ( $controller, $c, @args ) = @_;
$self->_setup_forms($c);
return $self->next::method(@_);
}
sub _setup_forms {
my ( $self, $c ) = @_;
while ( my ( $name, $class ) = each %{ $self->form_classes } ) {
# setup form
$c->log->debug("Loading form '$class' as '$name'");
my $form = $class->new();
$form->params( $c->req->params );
$form->init_fields;
# put form in stash (first or only)
$c->stash->{ $self->stash_name } ||= $form;
# put form in stash under its name, in case of multiple forms
$c->stash->{ $self->stash_hash }->{$name} = $form;
}
return;
}
1;
--
Jason Gottshall
jgottshall at capwiz.com
More information about the Catalyst
mailing list