Beefy Boxes and Bandwidth Generously Provided by pair Networks
Don't ask to ask, just ask
 
PerlMonks  

RFC: Class::CGI

by Ovid (Cardinal)
on Apr 07, 2006 at 22:25 UTC ( [id://541983]=perlmeditation: print w/replies, xml ) Need Help??

This module is not yet on the CPAN. You may download it from my downloads section.

When writing web-based code, I see an awful lot of stuff like this:

use My::Customer; use CGI; my $cgi = CGI->new; my $cust_id = $cgi->param('customer_id'); error_page("Bad customer id") unless $cust_id && $cust_id =~ /^\d_/; my $email = $cgi->param('email'); # validate email my $cust = My::Customer->load_from_id($cust_id); $cust->email($email); $cust->save;

This gets repeated in many classes throughout the site. To solve this, I wrote Class::CGI. Before I upload it to the CPAN, it would be nice to get comments from folks about it. Let me know what you like, dislike, and so on. If you have other features you think it should support, please let me know.

The above code could become something like this:

use Class::CGI handlers => { customer => 'Class::CGI::Customer' }; my $cgi = Class::CGI->new; my $cust = $cgi->param('customer'); my $email = $cgi->param('email'); # validate email $cust->email($email); $cust->save;


NAME

Class::CGI - Fetch objects from your CGI object


VERSION

Version 0.01


SYNOPSIS

    use Class::CGI
        handlers => {
            customer_id => 'Class::CGI::Customer'
        };
    my $cgi      = Class::CGI->new;
    my $customer = $cgi->param('customer_id');
    my $name     = $customer->name;
    my $email    = $cgi->param('email'); # behaves like normal


DESCRIPTION

For small CGI scripts, it's common to get a parameter, untaint it, pass it to an object constructor and get the object back. This module would allow one to to build Class::CGI handler classes which take the parameter value, automatically perform those steps and just return the object. Much grunt work goes away and you can get back to merely pretending to work.

Because this module is a subclass of CGI::Simple, all of CGI::Simple's methods and behaviors should be available. We do not subclass off of CGI because CGI::Simple is faster and it's assumed that if we're going the full OO route that we are already using templates. This decision may be revisited in the future.


EXPORT

None.


BASIC USE

The simplest method of using Class::CGI is to simply specify each form parameter's handler class in the import list:

  use Class::CGI
    handlers => {
      customer => 'Class::CGI::Customer',
      sales    => 'Sales::Loader'
    };
  my $cgi = Class::CGI->new;
  my $customer = $cgi->param('customer');
  my $email    = $cgi->param('email');
  # validate email
  $customer->email($email);
  $customer->save;

Note that there is no naming requirement for the handler classes and any form parameter which does not have a handler class behaves just like a normal form parameter. Each handler class is expected to have a constructor named new which takes the raw form value and returns an object corresponding to that value. All untainting and validation is expected to be dealt with by the handler. See WRITING HANDLERS.

If you need different handlers for the same form parameter names (this is common in persistent environments) you may omit the import list and use the handlers method.


LOADING THE HANDLERS

When the handlers are specified, either via the import list or the handlers() method, we verify that the handler exists and croak() if it is not. However, we do not load the handler until the parameter for that handler is fetched. This allows us to not load unused handlers but still have a semblance of safety that the handlers actually exist.


METHODS

handlers

  use Class::CGI;
  my $cust_cgi = Class::CGI->new;
  $cust_cgi->handlers(
    customer => 'Class::CGI::Customer',
  );
  my $order_cgi = Class::CGI->new($other_params);
  $order_cgi->handlers(
    order    => 'Class::CGI::Order',
  );
  my $customer = $cust_cgi->param('customer');
  my $order    = $order_cgi->param('order');
  $order->customer($customer);
  
Sometimes we get our CGI parameters from different sources.  This commonly
happens in a persistent environment where the class handlers for one form may
not be appropriate for another form.  When this occurs, you may set the
handler classes on an instance of the C<Class::CGI> object.  This overrides
global class handlers set in the import list:
  use Class::CGI handlers => { customer => "Some::Customer::Handler" };
  my $cgi = Class::CGI->new;
  $cgi->handlers( customer => "Some::Other::Customer::Handler" );

In the above example, the $cgi object will not use the Some::Customer::Handler class.

param

 use Class::CGI
     handlers => {
         customer => 'Class::CGI::Customer'
     };
 my $cgi = Class::CGI->new;
 my $customer = $cgi->param('customer'); # returns an object, if found
 my $email    = $cgi->param('email');    # returns the raw value
 my @sports   = $cgi->param('sports');   # behaves like you would expect

If a handler is defined for a particular parameter, the param() calls the new() method for that handler, passing the parameter's value. Returns the value returned by new(). In the example above, for ``customer'', the return value is essentially:

 return Class::CGI::Customer->new( $self->SUPER::param('customer') );


WRITING HANDLERS

Writing a handler is a fairly straightforward affair. Let's assume that our form has a parameter named ``customer'' and this parameter should point to a customer ID. The ID is assumed to be a positive integer value. For this example, we assume that our customer class is named My::Customer and we load a customer object with the load_from_id() method. The handler might look like this:

  package Class::CGI::Customer;
  
  use strict;
  use warnings;
  use My::Customer;
  
  sub new {
      my ($class, $id) = @_;
      
      unless ( $id && $id =~ /^\d+$/ ) {
          die "Invalid id ($id) for $class";
      }
      return My::Customer->load_from_id($id)
          || die "Could not find customer for ($id)";
  }
  
  1;

Pretty simple, eh?

Using this in your code is as simple as:

  use Class::CGI
    handlers => {
      customer => 'Class::CGI::Customer',
    };

If Class::CGI is being used in a persistent environment and other forms might have a param named customer but this param should not become a My::Customer object, then set the handler on the instance instead:

  use Class::CGI;
  my $cgi = Class::CGI->new;
  $cgi->handlers( customer => 'Class::CGI::Customer' );


TODO

This module should be considered alpha code. It probably has bugs. Comments and suggestions welcome.


AUTHOR

Curtis ``Ovid'' Poe, &lt;ovid@cpan.org&gt;


BUGS

Please report any bugs or feature requests to bug-class-cgi@rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.


ACKNOWLEDGEMENTS


COPYRIGHT & LICENSE

Copyright 2006 Curtis ``Ovid'' Poe, all rights reserved.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

Cheers,
Ovid

New address of my CGI Course.

Replies are listed 'Best First'.
Re: RFC: Class::CGI
by rjray (Chaplain) on Apr 07, 2006 at 22:53 UTC

    A few questions:

    1. Does Class::CGI::new consume query data and/or request body directly? Does it use an internal instance of a CGI object, and if so can it use an existing one? (In which case I would expect it to copy over all the param data triggering any relevant handlers as it encounters them. Of course, that might lead to ordering problems if something later in the form is sensitive to something earlier in the form.)
    2. My first thought when reading this was that you were referring to class-ifying structured data within forms, but from the longer examples I see that you're mostly just abstracting away the untaint and ->new() steps. So, aside from the sugaring, I'm not sure what benefits this provides. Is there a way to express interrelations between elements? If my instantiation of My::Customer needs the contents of other fields as well, do I have to go back to vanilla CGI?
    3. How hard would it be to have handlers that took on multiple input parameters? For example, to have a single handler that gathers address1, address2, city, state and zip_code and returns a single My::StreetAddress? (This is similar to, but not the same as the previous.)

    Still, interesting concept. ++

    --rjray

      This module does not consume the query data. It's merely a subclass of CGI-Simple. As such, it behaves exactly as that class does with the exception noted in the documentation.

      As for your second and third questions, they're closely related. The current implemenation handles existing objects and could easily create new objects for a single field and you'd have to populate the other fields manually. For existing objects that's cumbersome and I don't like it. The only thing which stopped me from implementing that is figuring out the best syntax. One way would be this:

      use Class::CGI handlers => { customer => { handler => 'Class::CGI::Customer', fields => [qw/ first last address1 address2 email city state zip /], } }; my $cgi = Class::CGI->new; my $cust = $cgi->param('customer'); my $zip = $cgi->param('zip');

      With that syntax, the handler would look for a "customer" param and, failing to find one, would attempt to build a new customer object from the fields list. Still, it's a bit clunky.

      One way around that would be to implement another idea I had: "profiles". You would do this:

      use Class::CGI profiles => $profile, use => 'customer';

      With that syntax, $profile would point to a file or class (user's choice) which describes various form profiles and the "use" key would point to a scalar or array ref detailing the profiles to be used for this code. That would put the object profiles in a centralized spot.

      Alternately, one could do this:

      use Class::CGI profiles => $profile, from_param => '__PROFILE__';

      With syntax like that, you specify the location of the profile file or class and the "from_param" lists the name of the param(s) in the form specifying which profile(s) to use. The default would be "__PROFILE__", so even naming it in your code would be optional. Thus, the forms would identify which handlers are needed instead of the code being responsible for it.

      These are all very basic ideas and are not fleshed out, so I just put together a simple proof of concept to see how things would work. More suggestions are welcome.

      Update: D'oh! I'm an idiot! With a very minor change to my existing code, I can make this work very easily. Instead of passing the param value to the handler, I can pass the Class::CGI object to the handler. The handler would have the resposibility of knowing which field(s) it needed. That's the proper place to put this!

      Cheers,
      Ovid

      New address of my CGI Course.

      I've just finished updating the code to handle all of this. Here's an example from the docs:

      package My::Date::Handler; use My::Date; sub new { my ($class, $cgi) = @_; my $month = $cgi->raw_param('month'); my $day = $cgi->raw_param('day'); my $year = $cgi->raw_param('year'); return My::Date->new( month => $month, day => $day, year => $year, ); } 1;

      And in the user's code:

      use Class::CGI handlers => { date => 'My::Date::Handler', }; my $cgi = Class::CGI->new; my $date = $cgi->param('date'); my $day = $date->day;

      It's not uploaded anywhere yet, though. I'll wait for more suggestions. I must say, it's really nice to see how clean design of classes makes extending them so easy :)

      Cheers,
      Ovid

      New address of my CGI Course.

        The one thing that aproach does is mean that each class is set in its parameters.. So you couldn't have two of any one type because they wouldn't know which fields to use unless you passed in the actual param that the user put. /me thinks maybe code will help here

        use Class::CGI handlers => { invoice_date => 'My::Date::Handler', sales_date => 'My::Date::Handler', }; my $cgi = Class::CGI->new; my $date = $cgi->param('invoice_date'); my $date = $cgi->param('sales_date'); my $day = $date->day;

        Sense the handler is picking the fields on its own it doesn't know which three fields belong where. Instead of having the handler pick fields maybe allow the user to specify fields to send as arguments.

        use Class::CGI handlers => { invoice_date => ['My::Date::Handler', qw(invoice_month invoice_d +ay invoice_year)], sales_date => ['My::Date::Handler', qw(sales_month sales_day sal +es_year)], }; my $cgi = Class::CGI->new; my $date = $cgi->param('invoice_date'); my $date = $cgi->param('sales_date'); my $day = $date->day;

        The main class could then grab those params and send the values to the handler so your handler would look like the following

        package My::Date::Handler; use My::Date; sub new { my ($class, $cgi, $param) = (shift,shift,shift); #standard my ($month, $day, $year) = (shift,shift,shift); #extra return My::Date->new( month => $month, day => $day, year => $year, ); } 1;

        The new method would then be sent the class, the cgi object, and the name of the handler (i.e. invoice_date, sales_date) followed by the values of any parameters listed in its definition.

        use Class::CGI handlers => { customer_id => 'My::Customer', referral_id => 'My::Customer', sales_date => ['My::Date::Handler', qw(sales_month sales_day sales_year)], }; my $cgi = Class::CGI->new; my $sales_date = $cgi->param('sales_date'); my $customer = $cgi->param('customer_id'); my $referrer = $cgi->param('referrar_id');

        I think you get the best of all worlds this way. You can have generic classes that validate the field and return it (without hardcoding the fields into the handler). You get handlers that can consume multiple fields in cases where they are known before hand (hardcoded in, like customer_id might expect some other fields in order to build the customer object), and handlers that consume multiple fields but can be passed the params they need so they are flexible. Of course maybe i'm seeing a problem where there isn't one, but date validation definitly needs to be able to be used multiple times on the same page with different fields (or sets of fields).

        The module sounds great otherwise...now if you can tie it to HTML::Template, TT and CGI::Application you will have a winner! ;)


        ___________
        Eric Hodges
Re: RFC: Class::CGI
by siracusa (Friar) on Apr 08, 2006 at 02:28 UTC
Re: RFC: Class::CGI
by Aristotle (Chancellor) on Apr 08, 2006 at 09:22 UTC

    Like the Anonymonk above, I too would suggest taking a look at HTML::Widget. However, I don’t know that it will be what you want; it comes at the problem from the opposite angle, rather like Data::FormValidator, though with an approach that I like better for web forms. Yours is very different and looks quite interesting in its own right.

    Makeshifts last the longest.

Re: RFC: Class::CGI
by leriksen (Curate) on Apr 07, 2006 at 23:50 UTC
    I was wondering if there was any benefit in this kind of interface

    use Class::CGI handlers => { customer => 'Class::CGI::Customer', email => 'Email::Loader' }; my $cgi = Class::CGI->new; my $customer = $cgi->customer(); # dispatch to C::C::C my $email = $cgi->email(); # dispatch to E::L my @sports = $cgi->param('sports'); # as per usual # validate email $customer->email($email); $customer->save;

    Surmising, if you've declared the handlers, perhaps you can construct things so that these handlers become methods in this instance of Class::CGI. You just have to come up with a way to make sure the methods are only in this scope of the "use Class::CGI" declaration. I

    ...reality must take precedence over public relations, for nature cannot be fooled. - R P Feynmann

      That changes the design dramatically since I would then have to delegate to the real CGI::Simple object lest someone have paremeters named "keywords", "param" or "upload" and innapropriately overloading things. Still, it might be a cleaner interface. I would simply have to find an easier way for folks to get at the underlying CGI object, if necessary. I would hate arbitrarily outlawing certain parameters because they're "reserved" methods. Some folks wouldn't be able to use the code that way since it would be tough to integrate with an existing site which used the reserved parameter names.

      I could do something like this:

      use CGI; use Class::CGI handlers => { customer => 'Class::CGI::Customer' }; my $cgi = Class::CGI->new( cgi => CGI->new );

      That would also have the advantage of allowing folks to use any class which implements the required CGI methods (just param(), at this time). It has the disadvantage of forcing them to explicitly load in the other module and I was hoping to do away with that. I could be done implicitly, but that brings back the problems mentioned in the first paragraph. I'll have to think about this.

      Cheers,
      Ovid

      New address of my CGI Course.

Re: RFC: Class::CGI
by rhesa (Vicar) on Apr 08, 2006 at 01:25 UTC

      I like Data::FormValidator, but I was never happy with the syntax and never used it extensively. Class::CGI basically treats form data as a collection of objects which can be fetched. Further, the various objects which different forms contain can be mixed and matched at will, with ease. Because a lot of the validation stuff is encapsulated in the handler classes responsible for a given object, the programmer's main code is focused on the business concerns.

      In short, the code can be as simple as this:

      use Class::CGI handlers => { customer => 'Class::CGI::Customer', date => 'My::Date::Handler', }; my $cgi = Class::CGI->new; eval { my $cust = $cgi->param('customer'); my $date = $cgi->param('date'); }; handle_possible_error($@); my $name = $cust->name; # look ma, objects! my $year = $date->year;

      There's a bunch of work which gets abstracted away instead of cluttering the code in a large data structure with odd parameters the end user has to figure out.

      That's not to say anything is wrong with the Data::FormValidator approach. It's a difference in approach. With DFV, the validation is centralized in the code doing the validating and it validates all of the data. In the Class::CGI approach, each handler is responsible for validating its own data.

      Those are just from my rough impressions of DFV, though. If you can show me how clean the top-level code can be, I'd love to see it.

      Cheers,
      Ovid

      New address of my CGI Course.

        Well, I'm not a veteran DFV user, so I may be stumbling here. But something like this:
        use Data::FormValidator; use Data::FormValidator::Constraints::DateTime qw(to_datetime); use CGI; use My::Customer; my $results = Data::FormValidator->check( CGI->new, { constraints => { customer => My::Customer->constraint, date => to_datetime('%D'), } } ); if( $results->success ) { my $date = $results->valid('date'); my $customer = $results->valid('customer'); $date->year; $customer->address->zipcode; }
        There's no need to inline the profile like that, of course.

        &My::Customer::constraint might look like this:

        package My::Customer; use base 'My::Class::DBI'; sub constraint { my $pkg = shift; return sub { my $value = shift; return unless $value =~ /^(\d+)$/; return $pkg->retrieve($value); }; }
        But, that's all just guesswork on my part (and completely untested). It does show tremendous potential, that I really should try to incorporate in my own work ;)
Re: RFC: Class::CGI
by Anonymous Monk on Apr 08, 2006 at 07:45 UTC
Re: RFC: Class::CGI
by rhesa (Vicar) on Apr 08, 2006 at 14:13 UTC
    I just remembered another contender: CGI::Untaint. With Data::FormValidator, HTML::Widget, and this one, I fail to see the added value of your proposed module.

    Your idea forces me to write wrapper objects for every object I want loaded, and you force me to implement the logic in new(). That would prevent me from putting the logic in my object itself. As I already demonstrated, DFV would allow me to put the logic in my object itself.

    DFV also allows me to use an arbitrary number of form fields in each constraint, like this:

    constraints => { customer_name => { params => [ qw/ customer_name email age / ], constraint => sub { my ($name, $email, $age) = @_; # untaint, trim, whatever return My::Customer->find_or_create( ... ); } } }

    In this case, I have inlined the hashref for customer_name, but it is obvious that I can stick that in my My::Customer object:

    constraints => { customer_name => My::Customer->complex_constraint, }

    As you can see, DFV allows me to create all the logic in-place, or I can push it further down the code; I still have full control over exactly what gets done where, and how. More flexibility, hence more power.

    I'm getting a much clearer picture of the usefulness of DFV, thanks to your RFC. In fact, I 'm beginning to see how to implement your Class::CGI as a thin wrapper around DFV :)
    In any case, you've made me think about this stuff again, and for that alone I thank you :)

      Your idea forces me to write wrapper objects for every object I want loaded,

      Yes. However, as with good code, the handler only need to be written once and if it turns out that something was overlooked, it can easily be added and all code using the handler gets the new advanage.

      ... and you force me to implement the logic in new(). That would prevent me from putting the logic in my object itself.

      I don't get what you're saying. Look at your counter-example:

      constraints => { customer_name => { params => [ qw/ customer_name email age / ], constraint => sub { my ($name, $email, $age) = @_; # untaint, trim, whatever return My::Customer->find_or_create( ... ); } } }

      In your example, customer_name is a declarative form of my handler. It's the same thing. And guess what? My handler reads pretty similarly:

      package Customer::Handler; sub new { my ( $class, $cgi ) = @_; my ( $name, $email, $age ) = map { scalar $cgi->param($_) } qw/customer_name email age/; # untaint, trim, whatever return My::Customer->find_or_create( ... ); } 1;

      There is, however, a huge difference here. Hand those two snippets to programmers who know nothing about the two modules and ask them what the code does. Mine builds on the knowledge programmers already have. There is nothing knew you need to learn for that handler. Once you understand that a handler is "a constructor which takes a CGI object and returns the appropriate instance", that's it. You're done. There's no going back to the docs to read that "constraints" means, or to figure out if "constraint_method_regexp_map" is what you really need.

      In any event, I've no problem with DFV. It's a great module, it's just a different approach and I've never liked how much stuff I seem to have to wade through to figure out what I need.

      Cheers,
      Ovid

      New address of my CGI Course.

        Ok, I'm throwing in the towel :)

        It wasn't my intention to champion DFV in the first place; you obviously have an itch to scratch; I know you write good code; and I suppose every module promoting safe CGI processing is a good one.

        I still have the feeling you're reinventing a wheel for which there already is a perfectly adequate implementation, but in the end, it's not my place to dissuade you from doing that.

        Good luck!
        rhesa

Re: RFC: Class::CGI
by santonegro (Scribe) on Apr 10, 2006 at 18:10 UTC
    the dies in the Factory classes don't seem feasible. What if a user submits a form and you need to do server-side validation and respond back with the form BUT with each problematic aspect of the form indicated in some way?

    Everyone has been smacking you over the head with the tried-and-true bread-and-butter DFV and I don't mean to continue the bashing but I can do the above task with DFV but how can I do it with your Class::CGI?

    Also, a good framework like Catalyst or CGI::Prototype does make object creation from CGI data clean and re-usable already.

    My suggestion is for you to implement at least 3 of the Catalyst examples using Class::CGI and all of the DFV tutorial using Class::CGI. In doing so, you will have the best practical non-theoretical feedback you can get.

Re: RFC: Class::CGI
by santonegro (Scribe) on Apr 11, 2006 at 18:43 UTC
    Another thing, why tie your objects to one request protocol? In this day and age there are many request protocols (HTTP, um er must be another) and formats (XML, email).

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://541983]
Approved by ww
Front-paged by ww
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others surveying the Monastery: (2)
As of 2024-04-26 05:19 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found