package CGI::Ex::App; ### CGI Extended Application ###----------------------------------------------------------------### # Copyright 2004 - Paul Seamons # # Distributed under the Perl Artistic License without warranty # ###----------------------------------------------------------------### ### See perldoc at bottom use strict; use vars qw($VERSION $EXT_PRINT $EXT_VAL $BASE_DIR_REL $BASE_DIR_ABS $BASE_NAME_MODULE $RECURSE_LIMIT %CLEANUP_EXCLUDE); $VERSION = '1.14'; use CGI::Ex::Dump qw(debug); BEGIN { ### Default file locations ### these are used for the provided stub functions - if you are not ### using the stub functions - then you won't need to worry about these $EXT_PRINT ||= 'html'; $EXT_VAL ||= 'val'; $BASE_DIR_REL ||= ''; # relative path - stub methods will look in $BASE_DIR_REL/dir/of/content.html $BASE_DIR_ABS ||= ''; # content should be found at "$BASE_DIR_ABS/$BASE_DIR_REL/dir/of/content.html" $BASE_NAME_MODULE ||= ''; # the cgi name ### list of modules to exclude during cleanup ### this takes care of situations such as ### template toolkits rules area which contains ### a nested structure of rules and sub references. $CLEANUP_EXCLUDE{'Template::Parser'} = 1; } ###----------------------------------------------------------------### sub new { my $class = shift || __PACKAGE__; my $self = ref($_[0]) ? shift : {@_}; bless $self, $class; $self->init; return $self; } sub init {} ###----------------------------------------------------------------### sub navigate { my $self = shift; my $args = ref($_[0]) ? shift : {@_}; $self = $self->new($args) if ! ref $self; eval { ### a chance to do things at the very beginning return $self if $self->pre_navigate; ### run the step loop eval { local $self->{'__morph_lineage_start_index'} = $#{$self->{'__morph_lineage'} || []}; $self->nav_loop; }; if ($@) { ### rethrow the error unless we long jumped out of recursive nav_loop calls die $@ if $@ ne "Long Jump\n"; } ### one chance to do things at the very end $self->post_navigate; }; ### catch errors - if any if ($@) { $self->handle_error($@); } return $self; } sub nav_loop { my $self = shift; ### keep from an infinate nesting local $self->{recurse} = $self->{recurse} || 0; if ($self->{recurse} ++ >= $self->recurse_limit) { my $err = "recurse_limit reached (".$self->recurse_limit.")"; $err .= " number of jumps (".$self->{jumps}.")" if ($self->{jumps} || 0) > 1; die $err; } ### get the path (simple arrayref based thing) my $path = $self->path; ### allow for an early return return if $self->pre_loop($path); # a true value means to abort the navigate ### get a hash of valid paths (if any) my $valid_steps = $self->valid_steps; ### iterate on each step of the path foreach ($self->{path_i} ||= 0; $self->{path_i} <= $#$path; $self->{path_i} ++) { my $step = $path->[$self->{path_i}]; next if $step !~ /^[a-zA-Z_]\w*$/; # don't process the step if it contains odd characters ### check if this is an allowed step if ($valid_steps) { if (! $valid_steps->{$step} && $step ne $self->default_step && $step ne 'forbidden') { $self->stash->{'forbidden_step'} = $step; $self->replace_path('forbidden'); next; } } ### allow for becoming another package (allows for some steps in external files) $self->morph($step); ### run the guts of the step my $status = $self->run_hook('run_step', $step); $self->unmorph($step); ### Allow for the run_step to intercept. ### A true status means the run_step took over navigation. return if $status; } ### allow for one exit point after the loop return if $self->post_loop($path); # a true value means to abort the navigate ### run the default step as a last resort $self->insert_path($self->default_step); $self->nav_loop; # go recursive return; } sub pre_navigate {} sub post_navigate {} sub recurse_limit { shift->{'recurse_limit'} || $RECURSE_LIMIT || 15 } sub run_step { my $self = shift; my $step = shift; ### if the pre_step exists and returns true, exit the nav_loop return 1 if $self->run_hook('pre_step', $step); ### allow for skipping this step (but stay in the nav_loop) return 0 if $self->run_hook('skip', $step); ### see if we have complete valid information for this step ### if so, do the next step ### if not, get necessary info and print it out if ( ! $self->run_hook('prepare', $step, 1) || ! $self->run_hook('info_complete', $step) || ! $self->run_hook('finalize', $step, 1)) { ### show the page requesting the information $self->run_hook('prepared_print', $step); ### a hook after the printing process $self->run_hook('post_print', $step); return 2; } ### a hook before end of loop ### if the post_step exists and returns true, exit the nav_loop return 1 if $self->run_hook('post_step', $step); ### let the nav_loop continue searching the path return 0; } ### standard functions for printing - gather information sub prepared_print { my $self = shift; my $step = shift; my $hash_base = $self->run_hook('hash_base', $step); my $hash_comm = $self->run_hook('hash_common', $step); my $hash_form = $self->run_hook('hash_form', $step); my $hash_fill = $self->run_hook('hash_fill', $step); my $hash_swap = $self->run_hook('hash_swap', $step); my $hash_errs = $self->run_hook('hash_errors', $step); $_ ||= {} foreach $hash_base, $hash_comm, $hash_form, $hash_fill, $hash_swap, $hash_errs; ### fix up errors $hash_errs->{$_} = $self->format_error($hash_errs->{$_}) foreach keys %$hash_errs; $hash_errs->{has_errors} = 1 if scalar keys %$hash_errs; ### layer hashes together my $fill = {%$hash_form, %$hash_base, %$hash_comm, %$hash_fill}; my $swap = {%$hash_form, %$hash_base, %$hash_comm, %$hash_swap, %$hash_errs}; $fill = {} if $self->no_fill($step); ### run the print hook - passing it the form and fill info $self->run_hook('print', $step, undef, $swap, $fill); } sub no_fill { shift->{'no_fill'} } sub exit_nav_loop { my $self = shift; ### undo morphs if (my $ref = $self->{'__morph_lineage'}) { ### use the saved index - this allows for early "morphers" to only get rolled back so far my $index = $self->{'__morph_lineage_start_index'}; $index = -1 if ! defined $index; $self->unmorph while $#$ref != $index; } ### long jump back die "Long Jump\n"; } sub jump { my $self = shift; my $i = ($#_ == -1) ? 1 : shift; my $path = $self->path; my $path_i = $self->{path_i}; die "Can't jump if nav_loop not started" if ! defined $path_i; ### validate where we are jumping to if ($i =~ /^\w+$/) { if ($i eq 'FIRST') { $i = - $path_i - 1; } elsif ($i eq 'LAST') { $i = $#$path - $path_i; } elsif ($i eq 'NEXT') { $i = 1; } elsif ($i eq 'CURRENT') { $i = 0; } elsif ($i eq 'PREVIOUS') { $i = -1; } else { # look for a step by that name for (my $j = $#$path; $j >= 0; $j --) { if ($path->[$j] eq $i) { $i = $j - $path_i; last; } } } } if ($i !~ /^-?\d+$/) { require Carp; Carp::croak("Invalid jump index ($i)"); } ### manipulate the path to contain the new jump location my @replace; my $cut_i = $path_i + $i; if ($cut_i > $#$path) { push @replace, $self->default_step; } elsif ($cut_i < 0) { push @replace, @$path; } else { push @replace, @$path[$cut_i .. $#$path]; } $self->replace_path(@replace); ### record the number of jumps $self->{jumps} ||= 0; $self->{jumps} ++; ### run the newly fixed up path (recursively) $self->{path_i} ++; # move along now that the path is updated $self->nav_loop; $self->exit_nav_loop; } sub default_step { my $self = shift; return $self->{'default_step'} || 'main'; } ###----------------------------------------------------------------### sub step_key { my $self = shift; return $self->{'step_key'} || 'step'; } ### determine the path to follow sub path { my $self = shift; return $self->{path} ||= do { my @path = (); # default to empty path my $step_key = $self->step_key; if (my $step = $self->form->{$step_key}) { push @path, $step; } elsif ($ENV{'PATH_INFO'} && $ENV{'PATH_INFO'} =~ m|^/(\w+)|) { push @path, lc($1); } \@path; # return of the do }; } ### really should only be used during initialization sub set_path { my $self = shift; my $path = $self->{path} ||= []; die "Cannot call set_path after the navigation loop has begun" if $self->{path_i}; splice @$path, 0, $#$path + 1, @_; # change entries in the ref } ### legacy - same as append_path sub add_to_path { my $self = shift; push @{ $self->path }, @_; } ### append entries onto the end sub append_path { my $self = shift; push @{ $self->path }, @_; } ### replace all entries that are left sub replace_path { my $self = shift; my $ref = $self->path; my $i = $self->{path_i} || 0; if ($i + 1 > $#$ref) { push @$ref, @_; } else { splice(@$ref, $i + 1, $#$ref - $i, @_); # replace remaining entries } } ### insert more steps into the current path sub insert_path { my $self = shift; my $ref = $self->path; my $i = $self->{path_i} || 0; if ($i + 1 > $#$ref) { push @$ref, @_; } else { splice(@$ref, $i + 1, 0, @_); # insert a path at the current location } } ### a hash of paths that are allowed, default undef is all sub valid_steps {} ###----------------------------------------------------------------### ### allow for checking where we are in the path sub step_by_path_index { my $self = shift; my $i = shift || 0; my $ref = $self->path; return '' if $i < 0; return $self->default_step if $i > $#$ref; return $ref->[$i]; } sub previous_step { my $self = shift; die "previous_step is readonly" if $#_ != -1; return $self->step_by_path_index( ($self->{path_i} || 0) - 1 ); } sub current_step { my $self = shift; die "current_step is readonly" if $#_ != -1; return $self->step_by_path_index( ($self->{path_i} || 0) ); } sub next_step { my $self = shift; die "next_step is readonly" if $#_ != -1; return $self->step_by_path_index( ($self->{path_i} || 0) + 1 ); } sub last_step { my $self = shift; die "last_step is readonly" if $#_ != -1; return $self->step_by_path_index( $#{ $self->path } ); } sub first_step { my $self = shift; die "first_step is readonly" if $#_ != -1; return $self->step_by_path_index( 0 ); } ###----------------------------------------------------------------### sub pre_loop {} sub post_loop {} ### return the appropriate hook to call sub hook { my $self = shift; my $hook = shift || do { require Carp; Carp::confess("Missing hook name") }; my $step = shift || ''; my $default = shift; my $hist = $self->history; my $code; if ($step && ($code = $self->can("${step}_${hook}"))) { push @$hist, "$step - $hook - ${step}_${hook}"; return $code; } elsif ($code = $self->can($hook)) { push @$hist, "$step - $hook - $hook"; return $code; } elsif (UNIVERSAL::isa($default, 'CODE')) { push @$hist, "$step - $hook - DEFAULT CODE"; return $default; } elsif ($default) { push @$hist, "$step - $hook - DEFAULT"; return sub { return $default }; } else { return sub {}; } } ### get and call the appropriate hook sub run_hook { my $self = shift; my $hook = shift; my $step = shift; my $default = shift; my $code = $self->hook($hook, $step, $default); return $self->$code($step, @_); } sub history { return shift->{'history'} ||= []; } ### default die handler - show what happened and die (so its in the error logs) sub handle_error { my $self = shift; my $err = shift; debug $err, $self->path, $self->history; die $err; } ###----------------------------------------------------------------### ### utility modules for jeckyl/hyde on self sub allow_morph { my $self = shift; return $self->{'allow_morph'} ? 1 : 0; } sub allow_nested_morph { my $self = shift; return $self->{'allow_nested_morph'} ? 1 : 0; } sub morph { my $self = shift; my $step = shift || return; return if ! (my $allow = $self->allow_morph); # not true ### place to store the lineage my $lin = $self->{'__morph_lineage'} ||= []; my $cur = ref $self; # what are we currently push @$lin, $cur; # store so subsequent unmorph calls can do the right thing my $hist = $self->history; push @$hist, "$step - morph - morph"; my $sref = \$hist->[-1]; # get ref so we can add more info in a moment if (ref($allow) && ! $allow->{$step}) { # hash - but no step - record for unbless $$sref .= " - not allowed to morph to that step"; return; } ### make sure we haven't already been reblessed if ($#$lin != 0 # is this the second morph call && (! ($allow = $self->allow_nested_morph) # not true || (ref($allow) && ! $allow->{$step}) # hash - but no step )) { $$sref .= $allow ? " - not allowed to nested_morph to that step" : " - nested_morph disabled"; return; # just return - don't die so that we can morph early } ### if we are not already that package - bless us there my $new = $self->run_hook('morph_package', $step); if ($cur ne $new) { my $file = $new .'.pm'; $file =~ s|::|/|g; if (UNIVERSAL::can($new, 'can') # check if the package space exists || eval { require $file }) { # check for a file that holds this package ### become that package bless $self, $new; $$sref .= " - changed $cur to $new"; if (my $method = $self->can('fixup_after_morph')) { $self->$method($step); } } else { if ($@) { if ($@ =~ /^\s*(Can\'t locate \S+ in \@INC)/) { # let us know what happened $$sref .= " - failed from $cur to $new: $1"; } else { $$sref .= " - failed from $cur to $new: $@"; my $err = "Trouble while morphing to $file: $@"; debug $err; warn $err; } } } } } sub unmorph { my $self = shift; my $step = shift || '__no_step'; my $lin = $self->{'__morph_lineage'} || return; my $cur = ref $self; my $prev = pop(@$lin) || die "unmorph called more times than morph - current ($cur)"; ### if we are not already that package - bless us there my $hist = $self->history; if ($cur ne $prev) { if (my $method = $self->can('fixup_before_unmorph')) { $self->$method($step); } bless $self, $prev; push @$hist, "$step - unmorph - unmorph - changed from $cur to $prev"; } else { push @$hist, "$step - unmorph - unmorph - already isa $cur"; } return $self; } ###----------------------------------------------------------------### ### allow for cleanup including deep nested objects sub cleanup { my $self = shift; ref($self)->cleanup_cross_references($self); } sub cleanup_cross_references { my $class = shift; my $self = shift; my $seen = shift || {}; return if $seen->{$self}; # prevent recursive checking $seen->{$self} = 1; return if $CLEANUP_EXCLUDE{ ref($self) }; if (UNIVERSAL::isa($self, 'HASH')) { require Scalar::Util; # first self will always be hash foreach my $key (keys %$self) { next if ! $self->{$key}; $class->cleanup_cross_references($self->{$key}, $seen); # weaken and remove blessed objects # this will clober objects in global caches that are referenced in the structure # so beware (that means weaken your cached references) if (Scalar::Util::blessed($self->{$key}) && ! Scalar::Util::isweak($self->{$key})) { Scalar::Util::weaken($self->{$key}); $self->{$key} = undef; } elsif (UNIVERSAL::isa($self->{$key}, 'CODE')) { $self->{$key} = undef; } } } elsif (UNIVERSAL::isa($self, 'ARRAY')) { for my $key (0 .. $#$self) { next if ! $self->[$key]; $class->cleanup_cross_references($self->[$key], $seen); if (Scalar::Util::blessed($self->[$key]) && ! Scalar::Util::isweak($self->[$key])) { Scalar::Util::weaken($self->[$key]); $self->[$key] = undef; } elsif (UNIVERSAL::isa($self->[$key], 'CODE')) { $self->[$key] = undef; } } } } ###----------------------------------------------------------------### ### a few standard base accessors sub form { my $self = shift; if ($#_ != -1) { $self->{form} = shift || die "Invalid form"; } return $self->{form} ||= $self->cgix->get_form; } sub cookies { my $self = shift; if ($#_ != -1) { $self->{cookies} = shift || die "Invalid cookies"; } return $self->{cookies} ||= $self->cgix->get_cookies; } sub cgix { my $self = shift; return $self->{cgix} ||= do { my $args = shift || {}; require CGI::Ex; CGI::Ex->new($args); # return of the do }; } sub set_cgix { my $self = shift; $self->{cgix} = shift; } sub vob { my $self = shift; return $self->{vob} ||= do { my $args = shift || {}; $args->{cgix} ||= $self->cgix; require CGI::Ex::Validate; CGI::Ex::Validate->new($args); # return of the do }; } sub set_vob { my $self = shift; $self->{vob} = shift; } sub auth { my $self = shift; return $self->{auth} ||= do { my $args = shift || {}; $args->{cgix} ||= $self->cgix, $args->{form} ||= $self->form, $args->{cookies} ||= $self->cookies, require CGI::Ex::Auth; CGI::Ex::Auth->new($args); # return of the do }; } sub set_auth { my $self = shift; $self->{auth} = shift; } ### provide a place for placing variables sub stash { my $self = shift; return $self->{'stash'} ||= {}; } ### allow for adding arbitrary values to self sub add_property { my $self = shift; my $prop = shift; my $key = '__prop_'. $prop; my $name = __PACKAGE__ ."::". $prop; no strict 'refs'; *$name = sub : lvalue { my $self = shift; $self->{$key} = shift() if $#_ != -1; $self->{$key}; } if ! defined &$name; $self->$prop(shift()) if $#_ != -1; } ###----------------------------------------------------------------### ### js_validation items ### creates javascript suitable for validating the form sub js_validation { my $self = shift; my $step = shift; return '' if $self->ext_val eq 'htm'; # let htm validation do it itself my $form_name = shift || $self->run_hook('form_name', $step); my $hash_val = shift || $self->run_hook('hash_validation', $step, {}); my $js_uri = $self->js_uri_path; return '' if UNIVERSAL::isa($hash_val, 'HASH') && ! scalar keys %$hash_val || UNIVERSAL::isa($hash_val, 'ARRAY') && $#$hash_val == -1; return $self->vob->generate_js($hash_val, $form_name, $js_uri); } ### where to find the javascript files ### default to using this script as a handler sub js_uri_path { my $self = shift; my $script = $ENV{'SCRIPT_NAME'} || die "Missing SCRIPT_NAME"; return ($self->can('path') == \&CGI::Ex::App::path) ? $script . '/js' # try to use a cache friendly URI (if path is our own) : $script . '?'.$self->step_key.'=js&js='; # use one that works with more paths } ### name to attach js validation to sub form_name { 'theform' } ### provide some rudimentary javascript support ### if valid_steps is defined - it should include "js" sub js_run_step { my $self = shift; ### make sure path info looks like /js/CGI/Ex/foo.js my $file = $self->form->{'js'} || $ENV{'PATH_INFO'} || ''; $file = ($file =~ m!^(?:/js/|/)?(\w+(?:/\w+)*\.js)$!) ? $1 : ''; $self->cgix->print_js($file); return 1; # intercepted } ###----------------------------------------------------------------### ### implementation specific subs sub template_args { my $self = shift; my $step = shift; return { INCLUDE_PATH => $self->base_dir_abs, }; } sub print { my $self = shift; my $step = shift; my $swap = shift; my $fill = shift; ### get a filename relative to base_dir_abs my $file = $self->run_hook('file_print', $step); require Template; my $t = Template->new($self->template_args($step)); ### process the document my $out = ''; my $status = $t->process($file, $swap, \$out) || die $Template::ERROR; ### fill in any forms $self->cgix->fill(\$out, $fill) if $fill && ! $self->{no_fill}; ### now print $self->cgix->print_content_type(); print $out; } sub base_dir_rel { my $self = shift; $self->{base_dir_rel} = shift if $#_ != -1; return $self->{base_dir_rel} ||= $BASE_DIR_REL; } sub base_dir_abs { my $self = shift; $self->{base_dir_abs} = shift if $#_ != -1; return $self->{base_dir_abs} || $BASE_DIR_ABS || die "\$BASE_DIR_ABS not set for use in stub functions"; } sub ext_val { my $self = shift; $self->{ext_val} = shift if $#_ != -1; return $self->{ext_val} || $EXT_VAL || die "\$EXT_VAL not set for use in stub functions"; } sub ext_print { my $self = shift; $self->{ext_print} = shift if $#_ != -1; return $self->{ext_print} || $EXT_PRINT || die "\$EXT_PRINT not set for use in stub functions"; } sub has_errors { my $self = shift; return 1 if scalar keys %{ $self->hash_errors }; } sub format_error { my $self = shift; my $error = shift; # return $error if $error =~ /$error"; } ###----------------------------------------------------------------### ### default stub subs ### used for looking up a module to morph into sub morph_package { my $self = shift; my $step = shift || ''; my $cur = ref $self; # default to using self as the base for morphed modules my $new = $cur .'::'. $step; $new =~ s/(\b|_+)(\w)/\u$2/g; # turn Foo::my_step_name into Foo::MyStepName return $new; } sub base_name_module { my $self = shift; $self->{base_name_module} = shift if $#_ != -1; return $self->{base_name_module} ||= $BASE_NAME_MODULE; } ### used for looking up template content sub name_module { my $self = shift; my $step = shift || ''; my $name; if ($name = $self->base_name_module) { return $name; } else { return ($0 =~ m/(\w+)(\.\w+)?$/) ? $1 # allow for cgi-bin/foo or cgi-bin/foo.pl : die "Couldn't determine module name from \"name_module\" lookup ($step)"; } } ### which file is used for templating sub file_print { my $self = shift; my $step = shift; my $base_dir_rel = $self->base_dir_rel; my $module = $self->run_hook('name_module', $step); my $_step = $self->run_hook('name_step', $step, $step); my $ext = $self->ext_print; return "$base_dir_rel/$module/$_step.$ext"; } ### which file is used for validation sub file_val { my $self = shift; my $step = shift; my $base_dir = $self->base_dir_rel; my $module = $self->run_hook('name_module', $step); my $_step = $self->run_hook('name_step', $step, $step); my $ext = $self->ext_val; ### get absolute if necessary if ($base_dir !~ m|^/|) { $base_dir = $self->base_dir_abs . "/$base_dir"; } return "$base_dir/$module/$_step.$ext"; } sub info_complete { my $self = shift; my $step = shift; return 0 if ! $self->run_hook('ready_validate', $step); return $self->run_hook('validate', $step); } sub ready_validate { my $self = shift; my $step = shift; ### could do a slightly more complex test return 0 if ! $ENV{REQUEST_METHOD} || $ENV{REQUEST_METHOD} ne 'POST'; return 1; } sub set_ready_validate { my $self = shift; my $ready = shift; $ENV{REQUEST_METHOD} = ($ready) ? 'POST' : 'GET'; } sub validate { my $self = shift; my $step = shift; my $form = shift || $self->form; my $hash = $self->run_hook('hash_validation', $step, {}); my $what_was_validated = []; my $eob = eval { $self->vob->validate($form, $hash, $what_was_validated) }; if (! $eob && $@) { die "Step $step: $@"; } ### had an error - store the errors and return false if ($eob) { $self->add_errors($eob->as_hash({ as_hash_join => "
\n", as_hash_suffix => '_error', })); return 0; } ### allow for the validation to give us some redirection my $val; OUTER: foreach my $ref (@$what_was_validated) { foreach my $method (qw(append_path replace_path insert_path)) { next if ! ($val = $ref->{$method}); $self->$method(ref $val ? @$val : $val); last OUTER; } } return 1; } ### allow for using ConfUtil instead of yaml sub hash_validation { my $self = shift; my $step = shift; return $self->{hash_validation}->{$step} ||= do { my $hash; my $file = $self->run_hook('file_val', $step); ### allow for returning the validation hash in the filename ### a scalar ref means it is a yaml document to be read by get_validation if (ref($file) && ! UNIVERSAL::isa($file, 'SCALAR')) { $hash = $file; ### read the file - it it fails - errors should shown in the error logs } elsif ($file) { $hash = eval { $self->vob->get_validation($file) } || {}; } else { $hash = {}; } $hash; # return of the do }; } sub hash_base { my ($self, $step) = @_; return $self->{hash_base} ||= { script_name => $ENV{'SCRIPT_NAME'} || $0, path_info => $ENV{'PATH_INFO'} || '', js_validation => sub { $self->run_hook('js_validation', $step, shift) }, form_name => sub { $self->run_hook('form_name', $step) }, }; } sub hash_common { shift->{'hash_common'} ||= {} } sub hash_form { shift->form } sub hash_fill { shift->{'hash_fill'} ||= {} } sub hash_swap { shift->{'hash_swap'} ||= {} } sub hash_errors { shift->{'hash_errors'} ||= {} } sub add_errors { my $self = shift; my $hash = $self->hash_errors; my $args = ref($_[0]) ? shift : {@_}; foreach my $key (keys %$args) { my $_key = ($key =~ /error$/) ? $key : "${key}_error"; if ($hash->{$_key}) { $hash->{$_key} .= '
' . $args->{$key}; } else { $hash->{$_key} = $args->{$key}; } } $hash->{'has_errors'} = 1; } sub add_to_errors { shift->add_errors(@_) } sub add_to_swap { my $self = shift; $self->add_to_hash($self->hash_swap, @_) } sub add_to_fill { my $self = shift; $self->add_to_hash($self->hash_fill, @_) } sub add_to_form { my $self = shift; $self->add_to_hash($self->hash_form, @_) } sub add_to_common { my $self = shift; $self->add_to_hash($self->hash_common, @_) } sub add_to_base { my $self = shift; $self->add_to_hash($self->hash_base, @_) } sub add_to_hash { my $self = shift; my $old = shift; my $new = shift; $new = {$new, @_} if ! ref $new; # non-hashref $old->{$_} = $new->{$_} foreach keys %$new; } ###----------------------------------------------------------------### sub forbidden_info_complete { 0 } sub forbidden_file_print { my $self = shift; my $step = $self->stash->{'forbidden_step'}; my $str = "You do not have access to \"$step\""; return \$str; } ###----------------------------------------------------------------### 1; __END__ =head1 NAME CGI::Ex::App - Full featured (within reason) application builder. =head1 DESCRIPTION Fill in the blanks and get a ready made CGI. This module is somewhat similar in spirit to CGI::Application, CGI::Path, and CGI::Builder and any other "CGI framework." As with the others, CGI::Ex::App tries to do as much as possible, in a simple manner, without getting in the developer's way. Your milage may vary. =head1 SYNOPSIS More examples will come with time. Here are the basics for now. #!/usr/bin/perl -w MyApp->navigate; # OR you could do the following which cleans # circular references - useful for a mod_perl situation # MyApp->navigate->cleanup; exit; package MyApp; use strict; use base qw(CGI::Ex::App); use CGI::Ex::Dump qw(debug); sub valid_steps { return {success => 1, js => 1} } # default_step (main) is a valid path # note the inclusion of js step to allow the # javascript scripts in js_validation to function properly. # base_dir_abs is only needed if default print is used # template toolkit needs an INCLUDE_PATH sub base_dir_abs { '/tmp' } sub main_file_print { # reference to string means ref to content # non-reference means filename return \ "

Main Step

[% foo_error %]
[% js_validation %] Link to forbidden step "; } sub post_print { debug shift->history; } # show what happened sub main_file_val { # reference to string means ref to yaml document # non-reference means filename return \ "foo: required: 1 min_len: 2 max_len: 20 match: 'm/^([a-z]\\d)+[a-z]?\$/' match_error: Characters must alternate letter digit letter. \n"; } sub main_finalize { my $self = shift; debug $self->form, "Do something useful with form here"; ### add success step $self->add_to_swap({success_msg => "We did something"}); $self->append_path('success'); $self->set_ready_validate(0); return 1; } sub success_file_print { \ "

Success Step

All done.
([% success_msg %])
(foo = [% foo %])"; } ### not necessary - this is the default hash_base sub hash_base { # used to include js_validation my ($self, $step) = @_; return $self->{hash_base} ||= { script_name => $ENV{SCRIPT_NAME} || '', js_validation => sub { $self->run_hook('js_validation', $step) }, form_name => sub { $self->run_hook('form_name', $step) }, }; } __END__ Note: This example would be considerably shorter if the html file (file_print) and the validation file (file_val) had been placed in separate files. Though CGI::Ex::App will work "out of the box" as shown it is more probable that any platform using it will customize the various hooks to their own tastes (for example, switching print to use a system other than Template::Toolkit). =head1 HOOKS / METHODS CGI::Ex::App works on the principles of hooks which are essentially glorified method lookups. When a hook is called, CGI::Ex::App will look for a corresponding method call for that hook for the current step name. See the discussion under the method named "hook" for more details. The methods listed below are normal method calls. Hooks and methods are looked for in the following order: =over 4 =item Method C<-Enew> Object creator. Takes a hash or hashref. =item Method C<-Einit> Called by the default new method. Allows for any object initilizations. =item Method C<-Eform> Returns a hashref of the items passed to the CGI. Returns $self->{form}. Defaults to CGI::Ex::get_form. =item Method C<-Enavigate> Takes a class name or a CGI::Ex::App object as arguments. If a class name is given it will instantiate an object by that class. All returns from navigate will return the object. The method navigate is essentially a safe wrapper around the ->nav_loop method. It will catch any dies and pass them to ->handle_error. =item Method C<-Enav_loop> This is the main loop runner. It figures out the current path and runs all of the appropriate hooks for each step of the path. If nav_loop runs out of steps to run (which happens if no path is set, or if all other steps run successfully), it will insert the ->default_step into the path and run nav_loop again (recursively). This way a step is always assured to run. There is a method ->recurse_limit (default 15) that will catch logic errors (such as inadvertently running the same step over and over and over). The basic outline of navigation is as follows (the default actions for hooks are shown): navigate { eval { ->pre_navigate ->nav_loop ->post_navigate } # dying errors will run the ->handle_error method } nav_loop { ->path (get the path steps) # DEFAULT ACTION # look in $ENV{'PATH_INFO'} # look in ->form for ->step_key ->pre_loop # navigation stops if true ->valid_steps (get list of valid paths) foreach step of path { # check that path is valid ->morph # DEFAULT ACTION # check ->allow_morph # check ->allow_nested_morph # ->morph_package (hook - get the package to bless into) # ->fixup_after_morph if morph_package exists ->run_step (hook) ->unmorph # DEFAULT ACTION # ->fixup_before_unmorph if blessed to previous package # exit loop if ->run_step returned true (intercepted) } end of step foreach ->post_loop # navigation stops if true ->default_step (inserted into path at current location) ->nav_loop (called again recursively) } end of nav_loop run_step { ->pre_step (hook) # exits nav_loop if true ->skip (hook) # skips this step if true (stays in nav_loop) ->prepare (hook - defaults to true) ->info_complete (hook - ran if prepare was true) # DEFAULT ACTION # ->ready_validate (hook) # return false if ! ready_validate # ->validate (hook) # ->hash_validation (hook) # ->file_val (hook - uses base_dir_rel, name_module, name_step, ext_val) # uses CGI::Ex::Validate to validate the hash # returns true if validate is true ->finalize (hook - defaults to true - ran if prepare and info_complete were true) if ! ->prepare || ! ->info_complete || ! ->finalize { ->prepared_print # DEFAULT ACTION # ->hash_base (hook) # ->hash_common (hook) # ->hash_form (hook) # ->hash_fill (hook) # ->hash_swap (hook) # ->hash_errors (hook) # merge form, base, common, and fill into merged fill # merge form, base, common, swap, and errors into merged swap # ->print (hook - passed current step, merged swap hash, and merged fill) # DEFAULT ACTION # ->file_print (hook - uses base_dir_rel, name_module, name_step, ext_print) # ->template_args # Processes the file with Template Toolkit # Fills the any forms with CGI::Ex::Fill # Prints headers and the content ->post_print (hook - used for anything after the print process) # return true to exit from nav_loop } ->post_step (hook) # exits nav_loop if true } end of run_step =item Method C<-Epre_navigate> Called from within navigate. Called before the nav_loop method is started. If a true value is returned then navigation is skipped (the nav_loop is never started). =item Method C<-Epost_navigate> Called from within navigate. Called after the nav_loop has finished running. Will only run if there were no errors which died during the nav_loop process. =item Method C<-Ehandle_error> If anything dies during execution, handle_error will be called with the error that had happened. Default is to debug the error and path history. =item Method C<-Ehistory> Returns an arrayref of which hooks of which steps of the path were ran. Useful for seeing what happened. In general - each line of the history will show the current step, the hook requested, and which hook was actually called. (hooks that don't find a method don't add to history) =item Method C<-Epath> Return an arrayref (modifyable) of the steps in the path. For each step the remaining hooks can be run. Hook methods are looked up and ran using the method "run_hook" which uses the method "hook" to lookup the hook. A history of ran hooks is stored in the array ref returned by $self->history. Default will be a single step path looked up in $form->{path} or in $ENV{PATH_INFO}. By default, path will look for $ENV{'PATH_INFO'} or the value of the form by the key step_key. For the best functionality, the arrayref returned should be the same reference returned for every call to path - this ensures that other methods can add to the path (and will most likely break if the arrayref is not the same). If navigation runs out of steps to run, the default step found in default_step will be run. =item Method C<-Edefault_step> Step to show if the path runs out of steps. Default value is the 'default_step' property or the value 'main'. =item Method C<-Estep_key> Used by default to determine which step to put in the path. The default path will only have one step within it =item Method C<-Eset_path> Arguments are the steps to set. Should be called before navigation begins. This will set the path arrayref to the passed steps. =item Method C<-Eappend_path> Arguments are the steps to append. Can be called any time. Adds more steps to the end of the current path. =item Method C<-Ereplace_path> Arguments are the steps used to replace. Can be called any time. Replaces the remaining steps (if any) of the current path. =item Method C<-Einsert_path> Arguments are the steps to insert. Can be called any time. Inserts the new steps at the current path location. =item Method C<-Ejump> This method should not normally be used. It provides for moving to the next step at any point during the nav_loop. It effectively short circuits the remaining hooks for the current step. It does increment the recursion counter (which has a limit of ->recurse_limit - default 15). It is normally better to allow the other hooks in the loop to carry on their normal functions and avoid jumping. (Essentially, this hook behaves like a goto method to bypass everything else and continue at a different location in the path - there are times when it is necessary or useful - but most of the time should be avoided) Jump takes a single argument which is the location in the path to jump to. This argument may be either a step name, the special words "FIRST, LAST, CURRENT, PREVIOUS, OR NEXT" or the number of steps to jump forward (or backward) in the path. The default value, 1, indicates that CGI::Ex::App should jump to the next step (the default action for jump). A value of 0 would repeat the current step (watch out for recursion). A value of -1 would jump to the previous step. The special value of "LAST" will jump to the last step. The special value of "FIRST" will jump back to the first step. In each of these cases, the path array retured by ->path is modified to allow for the jumping. ### goto previous step $self->jump($self->previous_step); $self->jump('PREVIOUS'); $self->jump(-1); ### goto next step $self->jump($self->next_step); $self->jump('NEXT'); $self->jump(1); $self->jump; ### goto current step (repeat) $self->jump($self->current_step); $self->jump('CURRENT'); $self->jump(0); ### goto last step $self->jump($self->last_step); $self->jump('LAST'); ### goto first step $self->jump($self->first_step); $self->jump('FIRST'); =item Method C<-Eexit_nav_loop> This method should not normally used. It allows for a long jump to the end of all nav_loops (even if they are recursively nested). This effectively short circuits all remaining hooks for the current and remaining steps. It is used to allow the ->jump functionality. If the application has morphed, it will be unmorphed before returning. =item Method C<-Erecurse_limit> Default 15. Maximum number of times to allow nav_loop to call itself. If ->jump is used alot - the recurse_limit will be reached more quickly. It is safe to raise this as high as is necessary - so long as it is intentional. =item Method C<-Evalid_steps> Returns a hashref of path steps that are allowed. If step found in default method path is not in the hash, the method path will return a single step "forbidden" and run its hooks. If no hash or undef is returned, all paths are allowed (default). A key "forbidden_step" containing the step that was not valid will be placed in the stash. Often the valid_steps method does not need to be defined as arbitrary method calls are not possible with CGI::Ex::App. =item Method C<-Eprevious_step, -Ecurrent_step, -Enext_step, -Elast_step, -Efirst_step> Return the previous, current, next, last, and first step name - useful for figuring out where you are in the path. Note that first_step may not be the same thing as default_step if the path was overridden. =item Method C<-Epre_loop> Called right before the navigation loop is started. At this point the path is set (but could be modified). The only argument is a reference to the path array. If it returns a true value - the navigation routine is aborted. =item Method C<-Erun_hook> Calls "hook" to get a code ref which it then calls and returns the result. Arguments are the same as that for "hook". =item Method C<-Ehook> Arguments are a hook name, a pathstep name, and an optional code sub or default value (default value will be turned to a sub) (code sub will be called as method of $self). my $code = $self->hook('main', 'info_complete', sub {return 0}); ### will look first for $self->main_info_complete; ### will then look for $self->info_complete; ### will then run $self->$default_passed_sub; # sub {return 0} This system is used to allow for multiple steps to be in the same file and still allow for moving some steps out to external sub classed packages. If the application has successfully morphed then it is not necessary to add the step name to the beginning of the method name as the morphed packages method will override the base package (it is still OK to use the full method name "${step}_hookname"). If a hook is found (or a default value is found) then an entry is added to the arrayref contained in ->history. =item Method C<-Emorph> Allows for temporarily "becoming" another object type for the execution of the current step. This allows for separating some steps out into their own packages. Morph will only run if the method allow_morph returns true. Additionally if the allow_morph returns a hash ref, morph will only run if the step being morphed to is in the hash. The morph call occurs at the beginning of the step loop. A corresponding unmorph call occurs before the loop is exited. An object can morph several levels deep if allow_nested_morph returns true. For example, an object running as Foo::Bar that is looping on the step "my_step" that has allow_morph = 1, will do the following: call the hook morph_package (which would default to returning Foo::Bar::MyStep in this case), translate this to a package filename (Foo/Bar/MyStep.pm) and try and require it, if the file can be required, the object is blessed into that package. If that package has a "fixup_after_morph" method, it is called. The navigate loop then continues for the current step. At any exit point of the loop, the unmorph call is made which reblesses the object into the original package. It is possible to call morph earlier on in the program. An example of a useful early use of morph would be as in the following code: sub allow_morph { 1 } sub pre_navigate { my $self = shift; if ($ENV{'PATH_INFO'} && $ENV{'PATH_INFO'} =~ s|^/(\w+)||) { my $step = $1; $self->morph($step); $ENV{'PATH_INFO'} = "/$step"; $self->stash->{'base_morphed'} = 1; } return 0; } sub post_navigate { my $self = shift; $self->unmorph if $self->stash->{'base_morphed'}; } If this code was in a module Base.pm and the cgi running was cgi/base and called: Base->navigate; # OR - for mod_perl resident programs Base->navigate->cleanup; # OR sub post_navigate { shift->cleanup } and you created a sub module that inherited Base.pm called Base/Ball.pm -- you could then access it using cgi/base/ball. You would be able to pass it steps using either cgi/base/ball/step_name or cgi/base/ball?step=step_name - Or Base/Ball.pm could implement its own path. It should be noted that if you do an early morph, it is suggested to provide a call to unmorph. And if you want to let your early morphed object morph again - you will need to provide sub allow_nested_morph { 1 } With allow_nested_morph enabled you could create the file Base/Ball/StepName.pm which inherits Base/Ball.pm. The Base.pm, with the custom init and default path method, would automatically morph us first into a Base::Ball object (during init) and then into a Base::Ball::StepName object (during the navigation loop). =item Method C<-Eunmorph> Allows for returning an object back to its previous blessed state. This only happens if the object was previously morphed into another object type. Before the object is reblessed the method "fixup_before_unmorph" is called if it exists. =item Method C<-Eallow_morph> Boolean value. Specifies whether or not morphing is allowed. Defaults to the property "allow_morph" if found, otherwise false. For more granularity, if true value is a hash, the step being morphed to must be in the hash. =item Method C<-Eallow_nested_morph> Boolean value. Specifies whether or not nested morphing is allowed. Defaults to the property "allow_nested_morph" if found, otherwise false. For more granularity, if true value is a hash, the step being morphed to must be in the hash. =item Hook C<-Emorph_package> Used by morph. Return the package name to morph into during a morph call. Defaults to using the current object type as a base. For example, if the current object running is a Foo::Bar object and the step running is my_step, then morph_package will return Foo::Bar::MyStep. =item Hook C<-Erun_step> Runs all of the hooks specific to each step, beginning with pre_step and ending with post_step. Called after ->morph($step) has been run. If this returns true, the nav_loop is exited (meaning the run_step hook displayed the information). If it returns false, the nav_loop continues on to run the next step. This is essentially the same thing as a method defined in CGI::Applications ->run_modes. =item Hook C<-Epre_step> Ran at the beginning of the loop before prepare, info_compelete, and finalize are called. If it returns true, execution of nav_loop is returned and no more steps are processed. =item Hook C<-Eskip> Ran at the beginning of the loop before prepare, info_compelete, and finalize are called. If it returns true, nav_loop moves on to the next step (the current step is skipped). =item Hook C<-Eprepare> Defaults to true. A hook before checking if the info_complete is true. =item Hook C<-Einfo_complete> Checks to see if all the necessary form elements have been passed in. Calls hooks ready_validate, and validate. Will not be run unless prepare returns true (default). =item Hook C<-Efinalize> Defaults to true. Used to do whatever needs to be done with the data once prepare has returned true and info_complete has returned true. On failure the print operations are ran. On success navigation moves on to the next step. =item Hook C<-Eready_validate> Should return true if enough information is present to run validate. Default is to look if $ENV{'REQUEST_METHOD'} is 'POST'. A common usage is to pass a common flag in the form such as 'processing' => 1 and check for its presence - such as the following: sub ready_validate { shift->form->{'processing'} } =item Method C<-Eset_ready_validate> Sets that the validation is ready to validate. Should set the value checked by the hook ready_validate. The following would complement the processing flag above: sub set_ready_validate { my $self = shift; if (shift) { $self->form->{'processing'} = 1; } else { delete $self->form->{'processing'}; } } Note thate for this example the form key "processing" was deleted. This is so that the call to fill in any html forms won't swap in a value of zero for form elements named "processing." =item Hook C<-Evalidate> Runs validation on the information posted in $self->form. Uses CGI::Ex::Validate for the validation. Calls the hook hash_validation to load validation information. Should return true if enough information is present to run validate. Errors are stored as a hash in $self->{hash_errors} via method add_errors and can be checked for at a later time with method has_errors (if the default validate was used). Upon success, it will look through all of the items which were validated, if any of them contain the keys append_path, insert_path, or replace_path, that method will be called with the value as arguments. This allows for the validation to apply redirection to the path. A validation item of: {field => 'foo', required => 1, append_path => ['bar', 'baz']} would append 'bar' and 'baz' to the path should all validation succeed. =item Hook C<-Ehash_validation> Returns a hash of the validation information to check form against. By default, will look for a filename using the hook file_val and will pass it to CGI::Ex::Validate::get_validation. If no file_val is returned or if the get_validation fails, an empty hash will be returned. Validation is implemented by ->vob which loads a CGI::Ex::Validate object. =item Hook C<-Efile_val> Returns a filename containing the validation. Adds method base_dir_rel to hook name_module, and name_step and adds on the default file extension found in $self->ext_val which defaults to the global $EXT_VAL (the property $self->{ext_val} may also be set). File should be readible by CGI::Ex::Validate::get_validation. =item Hook C<-Ejs_validation> Requires YAML.pm. Will return Javascript that is capable of validating the form. This is done using the capabilities of CGI::Ex::Validate. This will call the hook hash_validation which will then be encoded into yaml and placed in a javascript string. It will also call the hook form_name to determine which html form to attach the validation to. The method js_uri_path is called to determine the path to the appropriate yaml_load.js and validate.js files. If the method ext_val is htm, then js_validation will return an empty string as it assumes the htm file will take care of the validation itself. In order to make use of js_validation, it must be added to either the hash_base, hash_common, hash_swap or hash_form hook (see examples of hash_base used in this doc). =item Hook C<-Eform_name> Return the name of the form to attach the js validation to. Used by js_validation. =item Method C<-Ejs_uri_path> Return the URI path where the CGI/Ex/yaml_load.js and CGI/Ex/validate.js files can be found. This will default to "$ENV{SCRIPT_NAME}/js" if the path method has not been overridden, otherwise it will default to "$ENV{SCRIPT_NAME}?step=js&js=" (the latter is more friendly with overridden paths). A default handler for the "js" step has been provided in "js_run_step" (this handler will nicely print out the javascript found in the js files which are included with this distribution - if valid_steps is defined, it must include the step "js" - js_run_step will work properly with the default "path" handler. =item Hook C<-Ehash_swap> Called in preparation for print after failed prepare, info_complete, or finalize. Should contain a hash of any items needed to be swapped into the html during print. Will be merged with hash_base, hash_common, hash_form, and hash_errors. Can be populated by passing a hash to ->add_to_swap. =item Hook C<-Ehash_form> Called in preparation for print after failed prepare, info_complete, or finalize. Defaults to ->form. Can be populated by passing a hash to ->add_to_form. =item Hook C<-Ehash_fill> Called in preparation for print after failed prepare, info_complete, or finalize. Should contain a hash of any items needed to be filled into the html form during print. Items from hash_form, hash_base, and hash_common will be layered on top during a print cycle. Can be populated by passing a hash to ->add_to_fill. By default - forms are sticky and data from previous requests will try and populate the form. There is a method called ->no_fill which will turn off sticky forms. =item Method C<-Eno_fill> Passed the current step. Should return boolean value of whether or not to fill in the form on the printed page. (prevents sticky forms) =item Hook C<-Ehash_errors> Called in preparation for print after failed prepare, info_complete, or finalize. Should contain a hash of any errors that occured. Will be merged into hash_form before the pass to print. Eash error that occured will be passed to method format_error before being added to the hash. If an error has occurred, the default validate will automatically add {has_errors =>1}. To the error hash at the time of validation. has_errors will also be added during the merge incase the default validate was not used. Can be populated by passing a hash to ->add_to_errors or ->add_errors. =item Hook C<-Ehash_common> Almost identical in function and purpose to hash_base. It is intended that hash_base be used for common items used in various scripts inheriting from a common CGI::Ex::App type parent. Hash_common is more intended for step level populating of both swap and fill. =item Hook C<-Ehash_base> A hash of base items to be merged with hash_form - such as pulldown menues. It will now also be merged with hash_fill, so it can contain default fillins. Can be populated by passing a hash to ->add_to_base. By default the following sub is what is used for hash_common (or something similiar). Note the use of values that are code refs - so that the js_validation and form_name hooks are only called if requested: sub hash_base { my ($self, $step) = @_; return $self->{hash_base} ||= { script_name => $ENV{SCRIPT_NAME}, js_validation => sub { $self->run_hook('js_validation', $step) }, form_name => sub { $self->run_hook('form_name', $step) }, }; } =item Hook C<-Ename_module> Return the name (relative path) that should be prepended to name_step during the default file_print and file_val lookups. Defaults to base_name_module. =item Hook C<-Ename_step> Return the step (appended to name_module) that should used when looking up the file in file_print and file_val lookups. Defaults to the current step. =item Hook C<-Efile_print> Returns a filename of the content to be used in the default print hook. Adds method base_dir_rel to hook name_module, and name_step and adds on the default file extension found in $self->ext_print which defaults to the global $EXT_PRINT (the property $self->{ext_print} may also be set). Should be a file that can be handled by hook print. =item Hook C<-Eprint> Take the information generated by prepared_print, format it, and print it out. Default incarnation uses Template::Toolkit. Arguments are: step name, form hashref, and fill hashref. =item Hook C<-Eprepared_print> Called when any of prepare, info_complete, or finalize fail. Prepares a form hash and a fill hash to pass to print. The form hash is primarily intended for use by the templating system. The fill hash is intended to be used to fill in any html forms. =item Hook C<-Epost_print> A hook which occurs after the printing has taken place. Is only run if the information was not complete. Useful for printing rows of a database query. =item Hook C<-Epost_step> Ran at the end of the step's loop if prepare, info_complete, and finalize all returned true. Allows for cleanup. If a true value is returned, execution of navigate is returned and no more steps are processed. =item Method C<-Epost_loop> Ran after all of the steps in the loop have been processed (if prepare, info_complete, and finalize were true for each of the steps). If it returns a true value the navigation loop will be aborted. If it does not return true, navigation continues by then inserting the step $self->default_step and running $self->nav_loop again (recurses) to fall back to the default step. =item Method C<-Estash> Returns a hashref that can store arbitrary user space data without clobering the internals of the application. =item Method C<-Eadd_property> Takes the property name as an argument. Creates an accessor that can be used to access a new property. If there were additional arguments they will call the new accessor. Calling the new accessor with an argument will set the property. Using the accessor in an assignment will also set the property (it is an lvalue). Calling the accessor in any other way will return the value. =item Method C<-Ecleanup> Can be used at the end of execution to tear down the structure. Default method starts a cleanup_cross_references call. =item Method C<-Ecleanup_cross_references> Used to destroy links in nested structures. Will spider through the data structure of the passed object and remove any blessed objects that are no weakly referenced. This means if you have a reference to an object in a global cache, that object should have its reference weakened in the global cache. Requires Scalar::Util to function. Use of this function is highly recommended in mod_perl environments to make sure that there are no dangling objects in memory. There are some global caches that can't be fixed (such as Template::Parser's reference to Template::Grammar in the Template::Toolkit). For these situations there is a %CLEANUP_EXCLUDE hash that contains the names of Object types to exclude from the cleanup process. Add any such global hashes (or objects with references to the global hashes) there. =back =head1 OTHER APPLICATION MODULES The concepts used in CGI::Ex::App are not novel or unique. However, they are all commonly used and very useful. All application builders were built because somebody observed that there are common design patterns in CGI building. CGI::Ex::App differs in that it has found more common design patterns of CGI's. CGI::Ex::App is intended to be sub classed, and sub sub classed, and each step can choose to be sub classed or not. CGI::Ex::App tries to remain simple while still providing "more than one way to do it." It also tries to avoid making any sub classes have to call ->SUPER::. There are certainly other modules for building CGI applications. The following is a short list of other modules and how CGI::Ex::App is different. =over 4 =item C Seemingly the most well know of application builders. CGI::Ex::App is different in that it: * Uses Template::Toolkit by default CGI::Ex::App can easily use another toolkit by simply overriding the ->print method. CGI::Application uses HTML::Template. * Offers integrated data validation. CGI::Application has had custom addons created that add some of this functionality. CGI::Ex::App has the benefit that once validation is created, * Allows the user to print at any time (so long as proper headers are sent. CGI::Application requires data to be pipelined. * Offers hooks into the various phases of each step ("mode" in CGI::Application lingo). CGI::Application essentially provides ->runmode * Support for easily jumping around in navigation steps. * Support for storing some steps in another package. CGI::Ex::App and CGI::Application are similar in that they take care of handling headers and they allow for calling other "runmodes" from within any given runmode. CGI::Ex::App's ->run_step is essentially equivalent to a method call defined in CGI::Application's ->run_modes. The ->run method of CGI::Application starts the application in the same manner as CGI::Ex::App's ->navigate call. Many of the hooks around CGI::Ex::App's ->run_step call are similar in nature to those provided by CGI::Application. =item C There are actually many simularities. One of the nicest things about CGI::Prototype is that it is extremely short (very very short). The ->activate starts the application in the same manner as CGI::Ex::App's =>navigate call. Both use Template::Tookit as the default template system. CGI::Ex::App is differrent in that it: * Offers integrated data validation. CGI::Application has had custom addons created that add some of this functionality. CGI::Ex::App has the benefit that once validation is created, * Offers more hooks into the various phases of each step. * Support for easily jumping around in navigation steps. * Support for storing some steps in another package. =item C CGI::Path and CGI::Ex::App are fairly similar in may ways as they were created under similar lines of thought. The primary difference in these two is that CGI::Ex::App: * Does not provide "automated path following" based on validated key information. CGI::Path works well for wizard based applications. CGI::Ex::App assumes that the application will chose it's own path (it works very well in non-linear paths - it also works fine in linear paths but it doesn't provide some of magic that CGI::Path provides). * Does not provide integrated session support. CGI::Path requires it for path navigation. CGI::Ex::App assumes that if session support or authentication is needed by an application, a custom Application layer that inherits from CGI::Ex::App will be written to provide this support. * Offers more granularity in the navigation phase. CGI::Path has successfully been used as a sub class of CGI::Ex::App with limited modifications. =back =head1 BUGS Uses CGI::Ex for header support by default - which means that support for mod_perl 2 is limited at this point. There are a lot of hooks. Actually this is not a bug. Some may prefer not calling as many hooks - they just need to override methods high in the chain and subsequent hooks will not be called. =head1 THANKS Bizhosting.com - giving a problem that fit basic design patterns. Earl Cahill - pushing the idea of more generic frameworks. Adam Erickson - design feedback, bugfixing, feature suggestions. James Lance - design feedback, bugfixing, feature suggestions. =head1 AUTHOR Paul Seamons =cut