[Catalyst] RFC: Catalyst::Controller::RHTMLO

Zbigniew Lukasiak zzbbyy at gmail.com
Sat Jan 31 11:32:58 GMT 2009


On Sat, Jan 31, 2009 at 12:03 AM, Jason Gottshall <jgottshall at capwiz.com> wrote:
> 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;
>    }
>

Maybe I am just being lazy now - but before I start digging too deep -
did you take into accout that some forms require loading stuff from
the DB?  It can happen in to cases:

1) loading SELECT choices lists - this one is easy - because it can be
done at initialisation as it does not depend on th request.

2) loading related forms for one to many relations - where you don't
know how many related forms you need until you have the main object -
i.e. until request time.  See Rose::HTML::Form::Repeatable.

Cheers,
Zbigniew
http://brudnopis.blogspot.com/
http://perlalchemy.blogspot.com/



More information about the Catalyst mailing list