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
jgottsh...@capwiz.com


_______________________________________________
List: Catalyst@lists.scsys.co.uk
Listinfo: http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/catalyst
Searchable archive: http://www.mail-archive.com/catalyst@lists.scsys.co.uk/
Dev site: http://dev.catalyst.perl.org/

Reply via email to