[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 

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


     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


This base controller glues Catalyst actions to form classes derived from
L<Rose::HTML::Form>, a component of John Siracusa's excellent 
framework. Unlike some other form-loading modules (see L</"PRIOR ART">), 
one does not include any mechanism for defining form structures; it merely
loads, instantiates, and initializes pre-written form classes for use in 

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, 
equivalent to the following:

     my $form = MyApp::Form::Book->new();
     $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) = @_;


(Note that you must put all the form names inside one set of quotes; 
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 
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 
just list it again:

     sub search : Local Form('Search,Search') {
         my ($self, $c) = @_;


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 


You can override many defaults using Catalyst's configuration mechanism:

         'Controller::RHTMLO' => {
             form_attr       => 'HasForm',
             action_class    => 'MyApp::Action::RoseForm',
             stash_name      => 'formobj',
             stash_hash_name => 'allforms',
             form_prefix     => 'MyApp::RoseForm',


=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

         # do cool stuff
             secure_token => {
                 type  => 'hidden',
                 value => $c->some_cool_security_token

=item C<form_attr>

Default: 'Form'. Set this to alter the subroutine attribute used to 
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 
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.


=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

=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 
cool L<CatalystX::CRUD> API.


=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.


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'} || 
         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;


sub execute {
     my $self = shift;
     my ($controller, $c, @args) = @_;


     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 . '"');

     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/^\+//;
         $c->log->debug("Loading form '$class'");

         # setup form
         my $form = $class->new();

         # 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;



More information about the Catalyst mailing list