CGI parameters as objects

Ovid on 2004-08-25T23:56:08

I've been toying with the idea of creating something like this:

use CGI::Simple::AsObjects 
    handle => [qw/Customers Products/];

my $cgi = CGI::Simple::AsObjects->new;
my $customer = $cgi->cust_param('customer_no');
print $customer->name; # objects directly from the CGI request

Still, if you know OO Perl, do you still use CGI, or would anyone even want this? I think it's a useful idea, but I'd have to play with the interface. The import list is almost like traits in that you can have a variety of specialized "param" methods automatically available and you can fetch an object directly back from an CGI parameter without the tedious hassle of validating the object ids and instantiating the objects.


Can it be easy?

brian_d_foy on 2004-08-26T05:54:26

How do you map form fields to objects? Is there a way that you have to do that without a lot of work?

Re:Can it be easy?

Adrian on 2004-08-26T13:57:26

Something like CGI::State maybe?

Re:Can it be easy?

Ovid on 2004-08-26T14:30:44

If you are just writing a small script or two, this would be overkill. However, if you're building a large system, I think the setup would be worth it. Going with Juerd's name of "Class::CGI", I am thinking something along the following lines, with three required subs:

package Class::CGI::Customer;
use My::Customer;
use Class::CGI;

sub register {
  name   => 'Customer',
  method => 'cust_param',
  param  => 'cust_id',    # can be overridden
}

sub validate {
  my $cgi = shift; # not needed here, but perhaps elsewhere?
  my ($id) = shift =~ /^(\d+)$/;
  return $id;
}

sub create {
  my ($cgi, $id) = @_;
  return My::Customer->new($id);
}

1;

Obviously, the validation and creation routine may take a lot more work, depending upon the needs of a module, but a basic one like this could be reused quite a bit and save some headache. Maybe something like this would be useful, too:

use Class::CGI
  Customer => 'customer_id,
  Product  => 'product_id';

my $cgi = Class::CGI->new;
my $customer = $cgi->param('customer_id');
print $customer->name;

That would avoid the the necessesity of hardcoding the exact same parameter name in every form, something which I see is frequently not done.

I think I may just have a project after I get back from Burning Man.

Re:Can it be easy?

btilly on 2004-08-26T14:59:01

You should call a register method in Class::CGI, not define one that it is supposed to track down and call. Also you need to figure out how to handle conflicts where two different objects use similar parameter names.

However when you finish your module and flesh it out, you'll probably discover that you've started writing a controller in an MVC pattern.

Re:Can it be easy?

Ovid on 2004-08-26T15:17:18

I was certainly considering the issue of handling conflicts. That could get annoying, particularly if a given controller does not use the conflicting packages but some poor sod gets bitten by it later down the road.

Class::CGI

Juerd on 2004-08-26T10:59:50

Nice idea, but can you call it Class::CGI then, like Class::DBI? It's shorter and IMHO nicer to look at. After all, they do almost the same, do they not?

Re:Class::CGI

Ovid on 2004-08-26T14:31:49

Much better name. Thank you. See my response to brian d foy for more implementation ideas.

I do a similar thing

gav on 2004-08-26T16:03:03

I have a class that combines Data::FormValidator and some other things so I can write:
my $addr = Foo::Address->new($cgi);
if ($addr->is_valid) {
  # XXX
} else {
  print "The following fields are missing or invalid: ", join("\n", $addr->errors);
}
It has a few neat features including supporting a prefix so if you have two addresses in the same field you can use:
<input type="text" name="addr1.name">
<input type="text" name="addr2.name">
and:
my $addr1 = Foo::Address->new($cgi, 'addr1');
my $addr2 = Foo::Address->new($cgi, 'addr2');
It's made things way easier.

Re:I do a similar thing

ajtaylor on 2004-08-27T16:16:10

Very cool idea! Could it work in conjuntion with something like CGI::Untaint or Class::DBI::FromCGI? Any chance this could be released to CPAN? How do you handle testing with your object level validation - dummy CGI object?

I have something similar to you except I combine the validation at a higher level when deciding if a form should be processed or simply displayed. So I can't do the individual object level validation like you're doing. Any errors from D::FV are eventually stuffed into the template parameters before processing. The caveat is that to get the errors to show in the template you have to manually add them where appropriate. Even better would be a way to define a form programmatically and have the error params automatically included. Of course, this would take away my ability to customize my templates but anyway...

package My::App;
use base 'My::AppBase';

sub runmode_foo {
  my $self = shift;
  if ($self->form_submitted) {
    # process form & redirect
  }

  # print the form
}

package My::AppBase;
use base 'CGI::Application';

sub form_submitted {
  my $self = shift;
  my $q = $self->query;

  my $submitted = $q->param('_form_submitted');
  return 0 unless $submitted;

  # get the D::FV profile based on runmode
  my $profile = $self->get_validation_profile;
  if ($profile) {
    my $dfv = Data::FormValidator->new({}, $self->param('dfv_defaults') );
    my $r = $dfv->check($q, $profile);
    if ($r->has_missing or $r->has_invalid) {
      $self->param('validation_errors', $r->msgs);
      $self->error_msg("There were problems submitting the form.");
      return 0;
    }
  }
  return 1;
}
But I like the idea of being able to validate data for an individual object. One thing that would be super cool (and this is partially inspired by a job posting) would be to combine server-side runmode level validation (is the combination of inputs for this screen valid?), client-side validation via JS (for quicker simple validation), and then individual object level validation (like your setup). The killer feature of course is being able to define all this data in a single place and be used throughout.

Re:I do a similar thing

gav on 2004-08-27T17:28:27

I've been thinking about the client-side JS validation recently. What my plan is:
  1. Write something like D::FV but with a more flexible store for the rules
  2. Have a JS version of this
  3. Use one of the SpiderMonkey based CPAN modules to test Perl vs JS
  4. Release to CPAN
  5. ...
  6. Profit!

Re:I do a similar thing

ajtaylor on 2004-08-27T18:13:20

My thinking was to have the validation module be able to write a JS function (ie. check_form() ), that would do at least the easier checks. This of course requires some other template support to automatically set the onSubmit attribute of the appropriate element.

The checks would just cover the simple cases: required field & dependencies. If this field is not present, then fail. If field a is "foo", then fields x & y are also required. I want to do it all in a single module, which can then be transposed on either the client (via templates) or server side.