[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