geektron has asked for the wisdom of the Perl Monks concerning the following question:

i'm getting sick of creating almost the same subroutines over and over again, but the details *seem* different enough that i can't quite get away with AUTOLOAD ... and the immediate new level of abstraction (that I can see) would require a configuration hash ... a step i don't think really helps.

basically, i have a handful of routines that pull values out of a database and create an anon array of hashrefs to pass into HTML::Template:

sub _getBuilderList { my $self = shift; my $selection = shift; my $sth = $self->param('DBH')->prepare("SELECT id, buildername + FROM builder ORDER BY buildername" ); $sth->execute(); my $cities; while ( my $row = $sth->fetchrow_hashref() ) { $row->{SELECTED}++ if $row->{id} == $selection; push @$cities, $row; } return $cities; } sub _getSchoolList { my $self = shift; my $selection = shift; my $sth = $self->param('DBH')->prepare("SELECT schoolname FROM + school ORDER BY schoolname" ); $sth->execute(); my $cities; while ( my $row = $sth->fetchrow_hashref() ) { $row->{SELECTED}++ if $row->{schoolname} eq $selection +; push @$cities, $row; } return $cities; } sub _buildGenericSelect { my ( $self, $max, $selection ) = @_; my $array; foreach my $number ( ( 1 .. $max ) ) { my $row; $row->{VALUE} = $number; $row->{SELECTED}++ if $number == $selection; push @$array, $row; } return $array; }
and so on.

the end result is a select menu using the anon array as a TMPL_LOOP

some of them use name as the option value, others use a numeric value ... ( the data's a mess, but it's coming from another datasource ... )

i wonder if there's a quicker way to do it (rather than to keep creating a new 'accessor' every time.

Replies are listed 'Best First'.
Re: abstraction -- level up!
by sacked (Hermit) on Jun 16, 2004 at 18:24 UTC
    If you are able to remove the code that is specific to each selection (i.e., the comparison used to assign a SELECTED element to your result set), than your database code can be generalized (like the code provided by dragonchild above).

    It appears that you are using CGI::Application. I assume from your code that your template has something like the following for the <TMPL_LOOP> inside a <select> list:
    <select name=city_id> <TMPL_LOOP cities> <option value="<TMPL_VAR id>" <TMPL_IF selected>selected</TMPL_IF>> <TMPL_VAR city_name> </TMPL_LOOP> </select>
    If so, the following suggestion may help.

    You can simplify using HTML::FillInForm. First, remove the logic from your template:
    <select name=city_id> <TMPL_LOOP cities> <option value="<TMPL_VAR id>"><TMPL_VAR city_name> </TMPL_LOOP> </select>

    Next, update your database selection code (remove the loop that assigns a SELECTED element):
    sub _getBuilderList { my $self = shift; my $sth = $self->param('DBH')->prepare("SELECT id, city_name FROM ci +ty ORDER BY city_name" ); $sth->execute(); # returns a reference to an array of hash refs my $cities= $sth->fetchall_arrayref( {} ); $sth->finish; return $cities; }

    Next, let HTML::FillInForm handle the selection for you:
    my $city_list= $self->_getBuilderList; # ... my $template= $self->load_tmpl( ... ); $template->param( cities => $city_list, # ... ); require HTML::FillInForm; return HTML::FillInForm->new->fill( scalarref => \$template->output, fobject => $self->query, );

    This assumes that the selection (in this case, city_id) is passed in as a form parameter. If not, you can assign it yourself:
    $self->query->param( city_id => $some_value ); # HTML::FillInForm code as before

    Now _getBuilderList can be simplified to use a general prepare/select routine:
    sub _getBuilderList { my $self = shift; my $sql = q{ SELECT id, city_name FROM city ORDER BY city_name }; return selectall_sql( $self->param('DBH'), $sql ); } # (needs proper error handling) sub selectall_sql { my( $dbh, $sql, $values )= @_; $values ||= []; + my $results= $dbh->selectall_arrayref( $sql, { Slice => {} }, @$values ); + return $results if $results; + warn $dbh->err if $dbh->err; + return; }

    --sacked
Re: abstraction -- level up!
by dragonchild (Archbishop) on Jun 16, 2004 at 18:19 UTC
    I separate out the actual communication with the database from the processing of the result set. So, I have a do_select() function that is (cut'n'pasted from production code)
    sub do_select { my $self = shift; my %params = @_; $params{params} ||= []; + my $results; eval { my $sth = $self->dbh->prepare_cached( $params{sql}, ); + $sth->execute( @{$params{params}}, ); + $results = $sth->fetchall_arrayref( {} ); }; if ($@) { warn $@, "\n$params{sql}\n@{$params{params}}\n"; return; } + if (UNIVERSAL::isa($params{inflater}, 'CODE')) { my @results = $params{inflater}->($results); + return ( wantarray ? @results : $results[0] ); } else { return $results; } }

    The function takes three parameters, two of which are optional. The sql parameter is the SQL statement you want executed. params is an optional array reference of the parameters to that statement. (You are using placeholders, right?) If you pass an inflater parameter, it will call that function, passing in the returned value from the fetchrow_arrayref({}) call.

    Assumptions:

    • $self->dbh will return a database handle.
    • That $dbh was created with the following call:
      $dbh = DBI->connect( $connect_string, $user, $password, { PrintError = +> 0, RaiseError => 1 } );

    Now, the inflater is passed an arrayref of hashes. Do with it what you will. if you don't pass in an inflater, that AoH is passed back to the caller of do_select().

    ------
    We are the carpenters and bricklayers of the Information Age.

    Then there are Damian modules.... *sigh* ... that's not about being less-lazy -- that's about being on some really good drugs -- you know, there is no spoon. - flyingmoose

    I shouldn't have to say this, but any code, unless otherwise stated, is untested

Re: db -> HTML::Template abstraction abstraction -- level up!
by Sifmole (Chaplain) on Jun 16, 2004 at 17:59 UTC
    I use a set of libraries that I created a while back that does very much this kind of thing.

    There is a base class which supports all the basic get / set functions, a meta-SQL language ( I know that this is not liked by many ), and one very important function "unroll()".

    I have a class for each table, or join of tables, in the db. Each of these classes inherits from the base class, and thus "can" unroll itself. Unroll knows how to crawl the attributes and arrays within the class and produce an HTML::Template friendly representation of the data within the instance.

    Within the driver code, which is usually procedural, I build an array of the data that is supposed to be made available to the template processor. I pass this array and the template name to a Display function. It unrolls the objects in the array, then processes the template using the new HTML::Template friendly struct.

    The naming of the template values is standardized so that you know how to call them; for instance a class FOO with an attribute "bar" would be in the hash as "FOO.bar" variable.

    I have lately been wondering whether Class::DBI supports this kind of transformation because I would be interested in replacing my home-grown code, which attempts the same thing as Class::DBI, with Class::DBI.

    Does anyone know?

Re: abstraction -- level up!
by eric256 (Parson) on Jun 16, 2004 at 18:08 UTC

    Well one step to simplifiy that would be to use selectall_array($sql, {Slice => {}}) and then one of your subs becomes:

    sub _getBuilderList { my $self = shift; my $selection = shift; my @cities = $self->param('DBH')->selectall_array("SELECT id, +buildername FROM builder ORDER BY buildername", {Slice => {}} ); @cities = grep { $_->{SELECTED}++ if $_->{id} == $selection; $ +_ } @cities; return \@cities; }
    Something along those lines at least. Then you see your just passing a SQL statment and you have a condition for selection. I'm not sure if if can be reduced any but this is a nice reduction so far.


    ___________
    Eric Hodges
      hmmm ... i was thinking more about not having to build each little accessor sub over and over.

        But why would you have to? Just pass in the SQL and the selected id:

        sub make_select { my ($dbh, $sql, $args, $id) = @_; my $items = $dbh->selectall_arrayref($sql, {Slice => {}}, $args); for ($items) { $_->{selected} = 1 if $_->{id} == $id; } return $items; }

        jeffa

        L-LL-L--L-LL-L--L-LL-L--
        -R--R-RR-R--R-RR-R--R-RR
        B--B--B--B--B--B--B--B--
        H---H---H---H---H---H---
        (the triplet paradiddle with high-hat)
        
Re: abstraction -- level up!
by jZed (Prior) on Jun 16, 2004 at 19:21 UTC
    I've created DBI::Format::HtmlTemplate and DBI::Format::Template for use with DBI::Shell. You feed them the name of an HTML::Template or Template::Toolkit template file and an active $sth and they do the rest. DBI::Shell is undergoing some revisions so I'm not releasing them yet, but if you're interested, I could make them available.