[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