=head1 NAME
CGI::Ex::App - Anti-framework application framework.
=head1 DESCRIPTION
Fill in the blanks and get a ready made web application. 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 of the mundane things, in a simple
manner, without getting in the developer's way. Your mileage may vary.
If you build applications that submit user information, validate it,
re-display it, fill in forms, or separate logic into separate modules,
then this module may be for you. If all you need is a dispatch
engine, then this still may be for you. If all want is to look at
user passed information, then this may still be for you. If you like
writing bare metal code, this could still be for you. If you don't want
to write any code, this module will help - but you still need to
provide you key actions.
=head1 SYNOPSIS (A LONG "SYNOPSIS")
More examples will come with time. Here are the basics for now.
This example script would most likely be in the form of a cgi, accessible via
the path http://yourhost.com/cgi-bin/my_app (or however you do CGIs on
your system. About the best way to get started is to paste the following
code into a cgi script (such as cgi-bin/my_app) and try it out. A detailed
walk-through follows in the next section. There is also a longer recipe
database example at the end of this document that covers other topics including
making your module a mod_perl handler.
### File: /var/www/cgi-bin/my_app (depending upon Apache configuration)
### --------------------------------------------
#!/usr/bin/perl -w
use strict;
use base qw(CGI::Ex::App);
use CGI::Ex::Dump qw(debug);
__PACKAGE__->navigate;
# OR
# my $obj = __PACKAGE__->new;
# $obj->navigate;
exit;
###------------------------------------------###
sub post_navigate {
# show what happened
debug shift->dump_history;
}
sub main_hash_validation {
return {
'general no_alert' => 1,
'general no_confirm' => 1,
'group order' => [qw(username password password2)],
username => {
required => 1,
min_len => 3,
max_len => 30,
match => 'm/^\w+$/',
match_error => 'You may only use letters and numbers.',
},
password => {
required => 1,
min_len => 6,
},
password2 => {
equals => 'password',
},
};
}
sub main_file_print {
# reference to string means ref to content
# non-reference means filename
return \ "
Main Step
[% js_validation %]
";
}
sub main_finalize {
my $self = shift;
if ($self->form->{'username'} eq 'bar') {
$self->add_errors(username => 'A trivial check to say the username cannot be "bar"');
return 0;
}
debug $self->form, "Do something useful with form here in the finalize hook.";
### 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 - [% success_msg %]
Username: [% username %]
Password: [% password %]
";
}
__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 templating system other than CGI::Ex::Template).
=head1 SYNOPSIS STEP BY STEP
This section goes step by step over the previous example.
Well - we start out with the customary CGI introduction.
#!/usr/bin/perl -w
use strict;
use base qw(CGI::Ex::App);
use CGI::Ex::Dump qw(debug);
Note: the "use base" is not normally used in the "main" portion of a script.
It does allow us to just do __PACKAGE__->navigate.
Now we need to invoke the process:
__PACKAGE__->navigate;
# OR
# my $obj = __PACKAGE__->new;
# $obj->navigate;
exit;
Note: the "exit" isn't necessary - but it is kind of nice to infer
that process flow doesn't go beyond the ->navigate call.
The navigate routine is now going to try and "run" through a series of
steps. Navigate will call the ->path method which should return an
arrayref containing the valid steps. By default, if path method has
not been overridden, the path method will default first to the step
found in form key named ->step_name, then it will fall to the contents
of $ENV{'PATH_INFO'}. If navigation runs out of steps to run it will
run the step found in ->default_step which defaults to 'main'. So the
URI '/cgi-bin/my_app' would run the step 'main' first by default. The
URI '/cgi-bin/my_app?step=foo' would run the step 'foo' first. The
URI '/cgi-bin/my_app/bar' would run the step 'bar' first.
CGI::Ex::App allows for running steps in a preset path. The navigate
method will go through a step of the path at a time and see if it is
completed (various methods determine the definition of "completed").
This preset type of path can also be automated using the CGI::Path
module. Rather than using a preset path, CGI::Ex::App also has
methods that allow for dynamic changing of the path, so that each step
can determine which step to do next (see the jump, append_path,
insert_path, and replace_path methods).
During development it would be nice to see what happened during the
course of our navigation. This is stored in the arrayref contained in
->history. There is a method that is called after all of the navigation
has taken place called "post_navigate". This chunk will display history after we
have printed the content.
sub post_navigate {
debug shift->dump_history;
} # show what happened
Ok. Finally we are looking at the methods used by each step of the path. The
hook mechanism of CGI::Ex::App will look first for a method ${step}_${hook_name}
called before falling back to the method named $hook_name. Internally in the
code there is a call that looks like $self->run_hook('hash_validation', $step). In
this case the step is main. The dispatch mechanism finds our method at the following
chunk of code.
sub main_hash_validation { ... }
The process flow will see if the data is ready to validate. Once it is ready
(usually when the user presses the submit button) the data will be validated. The
hash_validation hook is intended to describe the data and will be tested
using CGI::Ex::Validate. See the CGI::Ex::Validate perldoc for more
information about the many types of validation available.
sub main_file_print { ... }
The navigation process will see if user submitted information (the form)
is ready for validation. If not, or if validation fails, the step needs to
be printed. Eventually the file_print hook is called. This hook should
return either the filename of the template to be printed, or a reference
to the actual template content. In this example we return a reference
to the content to be printed (this is useful for prototyping applications
and is also fine in real world use - but generally production applications
use external html templates).
A few things to note about the template:
First, we add a hidden form field called step. This will be filled in
at a later point with the current step we are on.
We provide locations to swap in inline errors.
[% username_error %]
As part of the error html we name each span with the name of the error. This
will allow for us to have Javascript update the error spots when the javascript
finds an error.
At the very end we add the TT variable [% js_validation %]. This swap in is
provided by the default hash_base hook and will provide for form data to be
validated using javascript.
Once the process flow has deemed that the data is validated, it then calls
the finalize hook. Finalize is where the bulk of operations should go.
We'll look at it more in depth.
sub main_finalize {
my $self = shift;
my $form = $self->form;
At this point, all of the validated data is in the $form hashref.
if ($form->{'username'} eq 'bar') {
$self->add_errors(username => 'A trivial check to say the username cannot be "bar"');
return 0;
}
It is most likely that though the data is of the correct type and formatting,
it still isn't completely correct. This previous section shows a hard coded
test to see if the username was 'bar'. If it was then an appropriate error will
be set, the routine returns 0 and the run_step process knows that it needs to
redisplay the form page for this step. The username_error will be shown inline.
The program could do more complex things such as checking to see if the username
was already taken in a database.
debug $form, "Do something useful with form here in the finalize hook.";
This debug $form piece is simply a place holder. It is here that the program would
do something useful such as add the information to a database.
### add success step
$self->add_to_swap({success_msg => "We did something"});
Now that we have finished finalize, we add a message that will be passed to the template
engine.
$self->append_path('success');
$self->set_ready_validate(0);
The program now needs to move on to the next step. In this case we want to
follow with a page that informs us we succeeded. So, we append a step named "success".
We also call set_ready_validate(0) to inform the navigation control that the
form is no longer ready to validate - which will cause the success page to
print without trying to validate the data. It is normally a good idea
to set this as leaving the engine in a "ready to validate" state can result
in an recursive loop (that will be caught).
return 1;
}
We then return 1 which tells the engine that we completed this step successfully
and it needs to move on to the next step.
Finally we run the "success" step because we told it to. That step isn't
ready to validate so it prints out the template page.
For more of a real world example, it would be good to read the sample recipe db
application included at the end of this document.
=head1 DEFAULT PROCESS FLOW
The following pseudo-code describes the process flow
of the CGI::Ex::App framework. Several portions of the flow
are encapsulated in hooks which may be completely overridden to give
different flow. All of the default actions are shown. It may look
like a lot to follow, but if the process is broken down into the
discrete operations of step iteration, data validation, and template
printing the flow feels more natural.
=head2 navigate
The process starts off by calling ->navigate.
navigate {
eval {
->pre_navigate
->nav_loop
->post_navigate
}
# dying errors will run the ->handle_error method
->destroy
}
=head2 nav_loop
The nav_loop method will run as follows:
nav_loop {
->path (get the array of path steps)
# look in $ENV{'PATH_INFO'}
# look in ->form for ->step_key
# make sure step is in ->valid_steps (if defined)
->pre_loop($path)
# navigation stops if true
foreach step of path {
->morph
# check ->allow_morph
# check ->allow_nested_morph
# ->morph_package (hook - get the package to bless into)
# ->fixup_after_morph if morph_package exists
# if no package is found, process continues in current file
->run_step (hook)
->refine_path (hook)
# only called if run_step returned false (page not printed)
->next_step (hook) # find next step and add to path
->set_ready_validate(0) (hook)
->unmorph
# only called if morph worked
# ->fixup_before_unmorph if blessed to current package
# exit loop if ->run_step returned true (page printed)
} end of foreach step
->post_loop
# navigation stops if true
->default_step
->insert_path (puts the default step into the path)
->nav_loop (called again recursively)
} end of nav_loop
=head2 run_step (hook)
For each step of the path the following methods will be run
during the run_step hook.
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)
->ready_validate (hook)
return false if ! ready_validate
->validate (hook - uses CGI::Ex::Validate to validate form info)
->hash_validation (hook)
->file_val (hook)
->base_dir_abs
->base_dir_rel
->name_module
->name_step
->ext_val
returns true if validate is true or if nothing to validate
->finalize (hook - defaults to true - ran if prepare and info_complete were true)
if ! ->prepare || ! ->info_complete || ! ->finalize {
->prepared_print
->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)
->file_print (hook - uses base_dir_rel, name_module, name_step, ext_print)
->swap_template (hook - processes the file with CGI::Ex::Template)
->template_args (hook - passed to CGI::Ex::Template->new)
->fill_template (hook - fills the any forms with CGI::Ex::Fill)
->fill_args (hook - passed to CGI::Ex::Fill::fill)
->print_out (hook - print headers and the content to STDOUT)
->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
It is important to learn the function and placement of each of the
hooks in the process flow in order to make the most of CGI::Ex::App.
It is enough to begin by learning a few common hooks - such as
hash_validation, hash_swap, and finalize, and then learn about other
hooks as needs arise. Sometimes, it is enough to simply override the
run_step hook and take care of processing the entire step yourself.
Because of the hook based system, and because CGI::Ex::App uses
sensible defaults, it is very easy to override a little or a lot which
ends up giving the developer a lot of flexibility.
Consequently, it should be possible to use CGI::Ex::App with the other
frameworks such as CGI::Application or CGI::Prototype. For these you
could simple let each "runmode" call the run_step hook of CGI::Ex::App
and you will instantly get all of the common process flow for free.
=head1 AVAILABLE METHODS / HOOKS
CGI::Ex::App's dispatch system works on the principles of hooks (which
are essentially glorified method lookups). When the run_hook method
is called, CGI::Ex::App will look for a corresponding method call for
that hook for the current step name. It is perhaps easier to show than
to explain.
If we are calling the "print" hook for the step "edit" we would call
run_hook like this:
$self->run_hook('print', 'edit', $template, \%swap, \%fill);
This would first look for a method named "edit_print". If it is unable to
find a method by that name, it will look for a method named "print". If it
is unable to find this method - it will die.
If allow_morph is set to true, the same methods are searched for but it becomes
possible to move some of those methods into an external package.
See the discussions under the methods named "find_hook" and "run_hook" for more details.
The following is the alphabetical list of methods and hooks.
=over 4
=item allow_morph (method)
Should return true if this step is allowed to "morph" the current App
object into another package. Default is false. It is passed a single
argument of the current step. For more granularity, if true value is
a hash, the step being morphed to must be in the hash.
To enable morphing for all steps, add the following:
sub allow_morph { 1 }
To enable morph on specific steps, do either of the following:
sub allow_morph {
return {
edit => 1,
delete => 1,
};
}
# OR
sub allow_morph {
my ($self, $step) = @_;
return $step =~ /^(edit|delete)$/;
}
See the morph "hook" for more information.
=item allow_nested_morph (method)
Similar to the allow_morph hook, but allows for one more level of morphing.
This is useful in cases where the base class was morphed early on, or
if a step needs to call a sub-step but morph first.
See the allow_morph and the morph method for more information.
Should return a boolean value or hash of allowed steps - just as the
allow_morph method does.
=item append_path (method)
Arguments are the steps to append. Can be called any time. Adds more
steps to the end of the current path.
=item auth_args (method)
Should return a hashref that will be passed to the new method of CGI::Ex::Auth.
It is augmented with arguments that integrate it into CGI::Ex::App.
See the get_valid_auth method and the CGI::Ex::Auth documentation.
sub auth_args {
return {
login_header => 'My login header
',
login_footer => '[% TRY %][% INCLUDE login/login_footer.htm %][% CATCH %][% END %]',
secure_hash_keys => [qw(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccc 2222222222222)],
# use_blowfish => 'my_blowfish_key',
};
}
=item auth_data (method)
Contains authentication data stored during the get_valid_auth method.
The data is normally blessed into the CGI::Ex::Auth::Data package which
evaluates to false if there was an error and true if the authentication
was successful - so this data can be defined but false.
See the get_valid_auth method.
=item cleanup_user (method)
Installed as a hook during get_valid_auth. Allows for cleaning
up the username. See the get_valid_auth method.
sub cleanup_user {
my ($self, $user) = @_;
return lc $user;
}
=item current_step (method)
Returns the current step that the nav_loop is functioning on.
=item default_step (method)
Step to show if the path runs out of steps. Default value is the
'default_step' property which defaults to 'main'.
If nav_loop runs of the end of the path (runs out of steps), this
method is called, the step is added to the path, and nav_loop calls
itself recursively.
=item destroy (method)
Called at the end of navigate after all other actions have run. Can
be used for undoing things done in the ->init method called during
the ->new method.
=item dump_history (method)
Show simplified trace information of which steps were called, the
order they were called in, the time they took to run, and a brief list
of the output (to see the full response returned by each hook, pass a
true value as the only argument to dump_history -
$self->dump_history(1)). Indentation is also applied to show which
hooks called other hooks.
The first line shows the amount of time elapsed for the entire
navigate execution. Subsequent lines contain:
Step - the name of the current step.
Hook - the name of the hook being called.
Found - the name of the method that was found.
Time - the total elapsed seconds that method took to run.
Output - the response of the hook - shown in shortened form.
Note - to get full output responses - pass a true value to
dump_history - or just call ->history. Times displayed are to 5
decimal places - this accuracy can only be provided if the Time::HiRes
module is installed on your system (it will only be used if installed).
It is usually best to print this history during the post_navigate
method as in the following:
use CGI::Ex::Dump qw(debug);
sub post_navigate { debug shift->dump_history }
The following is a sample output of dump_history called from the
sample recipe application at the end of this document. The step
called is "view".
debug: admin/Recipe.pm line 14
shift->dump_history = [
"Elapsed: 0.00562",
"view - run_step - run_step - 0.00488 - 1",
" view - pre_step - pre_step - 0.00003 - 0",
" view - skip - view_skip - 0.00004 - 0",
" view - prepare - prepare - 0.00003 - 1",
" view - info_complete - info_complete - 0.00010 - 0",
" view - ready_validate - ready_validate - 0.00004 - 0",
" view - prepared_print - prepared_print - 0.00441 - 1",
" view - hash_base - hash_base - 0.00009 - HASH(0x84ea6ac)",
" view - hash_common - view_hash_common - 0.00148 - HASH(0x8310a20)",
" view - hash_form - hash_form - 0.00004 - HASH(0x84eaa78)",
" view - hash_fill - hash_fill - 0.00003 - {}",
" view - hash_swap - hash_swap - 0.00003 - {}",
" view - hash_errors - hash_errors - 0.00003 - {}",
" view - print - print - 0.00236 - 1",
" view - file_print - file_print - 0.00024 - recipe/view.html",
" view - name_module - name_module - 0.00007 - recipe",
" view - name_step - name_step - 0.00004 - view",
" view - swap_template - swap_template - 0.00161 - ...",
" view - template_args - template_args - 0.00008 - HASH(0x865abf8)",
" view - fill_template - fill_template - 0.00018 - 1",
" view - fill_args - fill_args - 0.00003 - {}",
" view - print_out - print_out - 0.00015 - 1",
" view - post_print - post_print - 0.00003 - 0"
];
=item exit_nav_loop (method)
This method should not normally used but there is no problem with
using it on a regular basis. Essentially it is a "goto" that 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. Also - the post_navigate method will
still be called.
=item first_step (method)
Returns the first step of the path. Note that first_step may not be the same
thing as default_step if the path was overridden.
=item form (method)
Returns a hashref of the items passed to the CGI. Returns
$self->{form} which defaults to CGI::Ex::get_form.
=item handle_error (method)
If anything dies during execution, handle_error will be called with
the error that had happened. Default action is to die with that error.
=item history (method)
Returns an arrayref which contains trace history of which hooks of
which steps 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.
The dump_history method shows a short condensed version of this
history which makes it easier to see what path was followed.
In general, the arrayref is free for anything to push onto which will
help in tracking other occurrences in the program as well.
=item init (method)
Called by the default new method. Allows for any object
initilizations that may need to take place. Default action does
nothing.
=item fill_args (hook)
Returns a hashref of args that will be passed to the CGI::Ex::Fill::fill.
It is augmented with the template to swap and the fill hash. This
could be useful if you needed to only swap a particular form on the template
page. Arguments are passed directly to the fill function.
sub fill_args { {target => 'my_form'} }
=item fill_template (hook)
Arguments are a template and a hashref. Takes the template that was
prepared using swap_template, and swaps html form fields using the
passed hashref. Overriding this method can control the fill behavior.
Calls the fill_args hook prior to calling CGI::Ex::Fill::fill
=item file_print (hook)
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 property $self->{ext_print} which will default to
".html". Should return a filename relative to base_dir_abs that can be
swapped using CGI::Ex::Template, or should be a scalar reference to
the template content that can be swapped. This will be used by the
hook print.
sub base_dir_abs { '/var/www/templates' }
sub base_dir_rel { 'content' }
sub name_module { 'recipe' }
sub ext_print { 'html' } # default
# ->file_print('this_step')
# would return 'content/recipe/this_step.html'
# the template engine would look in '/var/www/templates'
# for a file by that name
It may also return a reference to a string containing the html template.
This is useful for prototyping applications and/or keeping all of
the data for the application in a single location.
=item file_val (hook)
Returns a filename containing the validation. Performs the same
as file_print, but uses ext_val to get the extension, and it adds
base_dir_abs onto the returned value (file_print is relative to
base_dir_abs, while file_val is fully qualified with base_dir_abs)
The file should be readable by CGI::Ex::Validate::get_validation.
This hook is only necessary if the hash_validation hook has not been
overridden.
This method an also return a hashref containing the validation - but
then you may have wanted to override the hash_validation hook.
=item finalize (hook)
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.
This is normally were there core logic of a script will occur (such as
adding to a database, or updating a record). At this point, the data
should be validated. It is possible to do additional validation
and return errors using code such as the following.
if (! $user_is_unique) {
$self->add_errors(username => 'The username was already used');
return 0;
}
=item find_hook (method)
Called by run_hook. Arguments are a hook name, a step name. It
should return an arrayref containing the code_ref to run, and the
name of the method looked for. It uses ->can to find the appropriate
hook.
my $code = $self->hook('finalize', 'main');
### will look first for $self->main_finalize;
### will then look for $self->finalize;
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 desired).
If the application has successfully morphed via the morph method and
allow_morph 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").
See the run_hook method and the morph method for more details.
=item forbidden_step (method)
Defaults to "__forbidden". The name of a step to run should the current
step name be invalid, or if a step found by the default path method
is invalid. See the path method.
=item form_name (hook)
Return the name of the form to attach the js validation to. Used by
js_validation.
=item get_pass_by_user (method)
This method is passed a username and the authentication object. It
should return the password for the given user. See the get_pass_by_user
method of CGI::Ex::Auth for more information. Installed as a hook
to the authentication object during the get_valid_auth method.
=item get_valid_auth (method)
If require_auth is true at either the application level or at the
step level, get_valid_auth will be called.
It will call auth_args to get some default args to pass to
CGI::Ex::Auth->new. It augments the args with sensible defaults that
App already provides (such as form, cookies, and template facilities).
It also installs hooks for the get_pass_by_user, cleanup_user, and verify_user
hooks of CGI::Ex::Auth.
It stores the $auth->last_auth_data in $self->auth_data for later use. For
example, to get the authenticated user:
sub require_auth { 1 }
sub cleanup_user {
my ($self, $user) = @_;
return lc $user;
}
sub get_pass_by_user {
my ($self, $user) = @_;
my $pass = $self->some_method_to_get_the_pass($user);
return $pass;
}
sub auth_args {
return {
login_header => 'My login header
',
login_footer => '[% TRY %][% INCLUDE login/login_footer.htm %][% CATCH %][% END %]',
};
}
sub main_hash_swap {
my $self = shift;
my $user = $self->auth_data->{'user'};
return {user => $user};
}
Successful authentication is cached for the duration of the
nav_loop so multiple steps will run the full authentication routine
only once.
Full customization of the login process and the login template can
be done via the auth_args hash. See the auth_args method and
CGI::Ex::Auth perldoc for more information.
=item hash_base (hook)
A hash of base items to be merged with hash_form - such as pulldown
menus, javascript validation, etc. It will now also be merged with
hash_fill, so it can contain default fillins as well. It can be
populated by passing a hash to ->add_to_base. By default a sub
similar to the following is what is used for hash_common. 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 hash_common (hook)
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 hash_errors (hook)
Called in preparation for print after failed prepare, info_complete,
or finalize. Should contain a hash of any errors that occurred. Will
be merged into hash_form before the pass to print. Each error that
occurred 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 in case the
default validate was not used. Can be populated by passing a hash to
->add_to_errors or ->add_errors.
=item hash_fill (hook)
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 together. 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. You can use the fill_template hook to disable
templating on a single page or on all pages.
This method can be used to pre-populate the form as well (such as on an
edit step). If a form fails validation, hash_fill will also be called
and will only want the submitted form fields to be sticky. You can
use the ready_validate hook to prevent pre-population in these cases as
follows:
sub edit_hash_fill {
my $self = shift;
my $step = shift;
return {} if $self->run_hook('ready_validate', $step);
my %hash;
### get previous values from the database
return \%hash;
}
=item hash_form (hook)
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 hash_swap (hook)
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.
The hash will be passed as the second argument to swap_template.
=item hash_validation (hook)
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 info_complete (hook)
Calls the ready_validate hook to see if data is ready to validate. If
so it calls the validate hook to validate the data. Should make
sure the data is ready and valid. Will not be run unless
prepare returns true (default).
=item insert_path (method)
Arguments are the steps to insert. Can be called any time. Inserts
the new steps at the current path location.
=item is_authed (method)
Returns true if the object has successful authentication data. It
returns false if the object has not been authenticated.
=item js_uri_path (method)
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. js_run_step will work properly with the
default "path" handler.
=item js_validation (hook)
Requires JSON or YAML. 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 either json or 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 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 the
variables returned by either the hash_base, hash_common, hash_swap or
hash_form hook (see examples of hash_base used in this doc).
By default it will try and use JSON first and then fail to YAML and
then will fail to returning an html comment that does nothing.
=item jump (method)
This method should not normally be used but is fine to use it on a
regular basis. 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
to do this).
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 strings
"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 returned by ->path is modified to allow for the
jumping (the path is modified so that the path history is not destroyed
- if we were on step 3 and jumped to one, that path would contain
1, 2, 3, *1, 2, 3, 4, etc and we would be at the *).
### 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 last_step (method)
Returns the last step of the path. Can be used to jump to the last step.
=item morph (method)
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. Morph also passes
the step name to allow_morph.
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 morph_package hook (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.
Call the fixup_after_morph method.
Continue on with the run_step for the current step.
At any exit point of the loop, the unmorph call is made which
re-blesses the object into the original package.
Samples of allowing morph:
sub allow_morph { 1 }
sub allow_morph { {edit => 1} }
sub allow_morph { my ($self, $step) = @_; return $step eq 'edit' }
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;
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).
Since it is complicated to explain - it may be a bit complicated to
those who will try to follow your code later. CGI::Ex::App provides
many ways to do things, but use the best one for your situation.
=item morph_package (hook)
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.
Because of the way that run_hook works, it is possible that several
steps could be located in the same external file and overriding morph_package
could allow for this to happen.
See the morph method.
=item name_module (hook)
Return the name (relative path) that should be pre-pended to name_step
during the default file_print and file_val lookups. Defaults to
the value in $self->{name_module} which in turn defaults to the name
of the current script.
cgi-bin/my_app.pl => my_app
cgi/my_app => my_app
This method is provided so that each cgi or mod_perl application can
have its own directory for storing html for its steps.
See the file_print method for more information.
=item name_step (hook)
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 nav_loop (method)
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 because there is either no hash_validation,
or the data is valid but the set_ready_validate(0) method was not called).
=item navigate (method)
Takes a class name or a CGI::Ex::App object as arguments. If a class
name is given it will call the "new" method to instantiate an object
by that class (passing any extra arguments to the new method). 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.
This starts the process flow for the path and its steps.
=item navigate_authenticated (method)
Same as the method navigate but sets require_auth(1) before
running. See the require_auth method.
=item new (class method)
Object creator. Takes a hashref of arguments that will become the
initial properties of the object. Calls the init method once the
object has been blessed to allow for any other initilizations.
my $app = MyApp->new({name_module => 'my_app'});
=item next_step (hook and method)
Returns the next step in the path. If there is no next step, it
returns the default_step.
It can be used as a method to return the next step in the path
to pass to a method such as ->jump.
It is also used as a hook by the refine_path hook. If there is no
more steps, it will call the next_step hook to try and find a step to
append to the path.
=item path (method)
Return an arrayref (modifiable) of the steps in the path. For each
step the run_step hook and all of its remaining hooks will be run.
Hook methods are looked up and ran using the method "run_hook" which
uses the method "find_hook" to lookup the hook. A history of ran
hooks is stored in the array ref returned by $self->history.
If path has not been defined, the method will look first in the form
for a key by the name found in ->step_key. It will then look in
$ENV{'PATH_INFO'}. It will use this step to create a path with that
one step as its contents. If a step is passed in via either of these
ways, the method will call valid_steps to make sure that the step
is valid (by default valid_steps returns undef - which means that
any step is valid). Any step beginning with _ can not be passed in
and are intended for use on private paths. If a non-valid step is
found, then path will be set to contain a single step of ->forbidden_step.
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. This is what allows for us to default
to the "main" step for many applications.
=item post_loop (method)
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 post_navigate (method)
Called from within navigate. Called after the nav_loop has finished
running but within the eval block to catch errors. Will only run if
there were no errors which died during the nav_loop process.
It can be disabled from running by setting the _no_post_navigate
property.
If per-step authentication is enabled and authentication fails,
the post_navigate method will still be called (the post_navigate
method can check the ->is_authed method to change behavior). If
application level authentication is enabled and authentication
fails, none of the pre_navigate, nav_loop, or post_navigate methods
will be called.
=item post_print (hook)
A hook which occurs after the printing has taken place. Is only run
if the information was not complete. Useful for cases such as
printing rows of a database query after displaying a query form.
=item post_step (hook)
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 pre_loop (method)
Called right before the navigation loop is started (at the beginning
of nav_loop). 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 pre_navigate (method)
Called at the very beginning of the navigate method, but within the
eval block to catch errors. 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 pre_step (hook)
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 prepare (hook)
Defaults to true. A hook before checking if the info_complete is true.
Intended to be used to cleanup the form data.
=item prepared_print (hook)
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 previous_step (method)
List the step previous to this one. Will return '' if there is no previous step.
=item print (hook)
Take the information generated by prepared_print, format it, and print it out.
Default incarnation uses CGI::Ex::Template which is compatible with
Template::Toolkit. Arguments are: step name (used to call the
file_print hook), swap hashref (passed to call swap_template), and
fill hashref (passed to fill_template).
During the print call, the file_print hook is called which should
return a filename or a scalar reference to the template content is
=item ready_validate (hook)
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'} }
Changing the behavior of ready_validate can help in making wizard type
applications.
=item refine_path (hook)
Called at the end of nav_loop. Passed a single value indicating
if there are currently more steps in the path.
The default implementation returns if there are still more steps
in the path. Otherwise, it calls the next_step hook and appends
it to the path with the append_path method, and then calls
the set_ready_validate hook and passes it 0.
This allows you to simply put
sub edit_next_step { '_edit_success' }
In your code and it will automatically do the right thing and
go to the _edit_success step.
=item recurse_limit (method)
Default 15. Maximum number of times to allow nav_loop to call itself.
The recurse level will increase every time that ->jump is called, or if
the end of the nav_loop is reached and the process tries to add the
default_step and run it again.
If ->jump is used often - 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.
Often the limit is reached if a step did not have a validation hash,
or if the set_ready_validate(0) method was not called once the data
had been successfully validated and acted upon.
=item replace_path (method)
Arguments are the steps used to replace. Can be called any time.
Replaces the remaining steps (if any) of the current path.
=item require_auth (method)
Default undef. Can return either a true value or a hashref of step names.
If a hashref of stepnames is returned, authentication will be turned on
at the step level. In this mode if any step is accessed, the get_valid_auth
method will be called. If it fails, then the nav_loop will be stopped
(the post_navigate method will be called - use the is_authed method to perform
different functions). Any step of the path not in the hash will not require
authentication. For example, to add authentication to add authentication
to the add, edit and delete steps you could do:
sub require_auth { {add => 1, edit => 1, delete => 1} }
If a non-hash true value is returned from the require_auth method then
authentication will take place before the pre_navigation or the nav_loop methods.
If authentication fails the navigation process is exited (the post_navigate
method will not be called).
sub require_auth { 1 }
Alternatively you can also could do either of the following:
__PACKAGE__->navigate_authenticated; # instead of __PACKAGE__->navigate;
# OR
sub init { shift->require_auth(1) }
# OR
__PACKAGE__->new({require_auth => 1}->navigate;
If get_valid_auth returns true, in either case, the is_authed method will
return true and the auth_data will contain the authenticated user's data.
If it returns false, auth_data may possibly contain a defined but false
data object with details as to why authentication failed.
See the get_valid_auth method.
=item run_hook (method)
Arguments are a hook name and the step to find the hook for. Calls
the find_hook method to get a code ref which it then calls and returns
the result passing any extra arguments to run_hook as arguments to the
code ref.
Each call to run_hook is logged in the arrayref returned by the
history method. This information is summarized in the dump_history
method and is useful for tracing the flow of the program.
The run_hook method is part of the core of CGI::Ex::App. It allows
for an intermediate layer in normal method calls. Because of
run_hook, it is possible to logically override methods on a step by
step basis, or override a method for all of the steps, or even to
break code out into separate modules.
=item run_step (hook)
Runs all of the hooks specific to each step, beginning with pre_step
and ending with post_step (for a full listing of steps, see the
section on process flow). Called after ->morph($step) has been run.
If this hook returns true, the nav_loop is exited (meaning the
run_step hook displayed a printed page). If it returns false, the
nav_loop continues on to run the next step.
This hook performs the same base functionality as a method defined in
CGI::Applications ->run_modes. The default run_step method provides
much more granular control over the flow of the CGI.
=item set_path (method)
Arguments are the steps to set. Should be called before navigation
begins. This will set the path arrayref to the passed steps.
This method is not normally used.
=item set_ready_validate (hook and method)
Sets that the validation is ready (or not) 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;
my ($step, $is_ready) = (@_ == 2) ? @_ : (undef, shift);
if ($is_ready) {
$self->form->{'processing'} = 1;
} else {
delete $self->form->{'processing'};
}
return $is_ready;
}
Note that 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."
Also note that this method may be called as a hook as in
$self->run_hook('set_ready_validate', $step, 0)
# OR
$self->set_ready_validate($step, 0);
Or it can take a single argument and should set the ready status
regardless of the step as in:
$self->set_ready_validate(0);
=item skip (hook)
Ran at the beginning of the loop before prepare, info_complete, and
finalize are called. If it returns true, nav_loop moves on to the
next step (the current step is skipped).
=item stash (method)
Returns a hashref that can store arbitrary user space data without
worrying about overwriting the internals of the application.
=item step_key (method)
Should return the keyname that will be used by the default "path"
method to look for in the form. Default value is 'step'.
=item swap_template (hook)
Takes the template and hash of variables prepared in print, and processes them
through the current template engine (default engine is CGI::Ex::Template).
Arguments are the template and the swap hashref. The template can be either a
scalar reference to the actual content, or the filename of the content. If the
filename is specified - it should be relative to base_dir_abs.
=item template_args (hook)
Returns a hashref of args that will be passed to the "new" method of CGI::Ex::Template.
By default this hashref contains INCLUDE_PATH which is set equal to base_dir_abs.
It can be augmented with any arguments that CGI::Ex::Template would understand.
sub template_args {
return {
INCLUDE_PATH => '/my/own/include/path',
WRAPPER => 'wrappers/main_wrapper.html',
};
}
=item unmorph (method)
Allows for returning an object back to its previous blessed state if
the "morph" method was successful in morphing the App object. This
only happens if the object was previously morphed into another object
type. Before the object is re-blessed the method fixup_before_unmorph
is called.
See allow_morph and morph.
=item valid_steps (method)
Called by the default path method. Should return a hashref of path
steps that are allowed. If the current step is not found in the hash
(or is not the default_step or js_step) the path method will return a
single step of ->forbidden_step 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.
Any steps that begin with _ are also "not" valid for passing in via the form
or path info. See the path method.
Also, the pre_step, skip, prepare, and info_complete hooks allow for validating
the data before running finalize.
=item validate (hook)
Passed the form from $self->form. Runs validation on the information
contained in the passed form. Uses CGI::Ex::Validate for the default
validation. Calls the hook hash_validation to load validation hashref
(an empty hash means to pass validation). Should return true if the
form passed validation and false otherwise. 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).
There are many ways and types to validate the data. Please see the
L module.
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 verify_user (method)
Installed as a hook to CGI::Ex::App during get_valid_auth. Should return
true if the user is ok. Default is to always return true. This can be
used to abort early before the get_pass_by_user hook is called.
sub verify_user {
my ($self, $user) = @_;
return 0 if $user eq 'paul'; # don't let paul in
return 1; # let anybody else in
}
=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 than some and tries to get in the way less than others.
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:: (although that is fine too).
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 compatible CGI::Ex::Template by default
CGI::Ex::App can easily use another toolkit by simply
overriding the ->swap_template method.
CGI::Application uses HTML::Template.
* Offers integrated data validation.
CGI::Application has had custom plugins created that
add some of this functionality. CGI::Ex::App has the benefit
that validation is automatically available in javascript as well.
* 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 provides only ->runmode
which is only a dispatch.
* 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 similarities. 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::Toolkit as the default template
system (CGI::Ex::App uses CGI::Ex::Template which is TT compatible).
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 only some steps in another package.
=back
=head1 SIMPLE EXTENDED EXAMPLE
The following example shows the creation of a basic recipe
database. It requires the use of DBD::SQLite, but that is all.
Once you have configured the db_file and base_dir_abs methods
of the "recipe" file, you will have a working script that
does CRUD for the recipe table. The observant reader may ask - why
not use Catalyst or Ruby on Rails? The observant programmer will
reply that making a framework do something simple is easy, but making
it do something complex is complex and any framework that tries to
do the those complex things for you is too complex. CGI::Ex::App
lets you write the complex logic but gives you the ability to
not worry about the boring details such as template engines,
or sticky forms, or cgi parameters, or data validation. Once
you are setup and are running, you are only left with providing
the core logic of the application.
### File: /var/www/cgi-bin/recipe (depending upon Apache configuration)
### --------------------------------------------
#!/usr/bin/perl -w
use lib qw(/var/www/lib);
use Recipe;
Recipe->navigate;
### File: /var/www/lib/Recipe.pm
### --------------------------------------------
package Recipe;
use strict;
use base qw(CGI::Ex::App);
use CGI::Ex::Dump qw(debug);
use DBI;
use DBD::SQLite;
###------------------------------------------###
sub post_navigate {
# show what happened
debug shift->dump_history;
}
sub base_dir_abs { '/var/www/templates' }
sub base_dir_rel { 'content' }
sub db_file { '/var/www/recipe.sqlite' }
sub dbh {
my $self = shift;
if (! $self->{'dbh'}) {
my $file = $self->db_file;
my $exists = -e $file;
$self->{'dbh'} = DBI->connect("dbi:SQLite:dbname=$file", '', '',
{RaiseError => 1});
$self->create_tables if ! $exists;
}
return $self->{'dbh'};
}
sub create_tables {
my $self = shift;
$self->dbh->do("CREATE TABLE recipe (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(50) NOT NULL,
ingredients VARCHAR(255) NOT NULL,
directions VARCHAR(255) NOT NULL,
date_added VARCHAR(20) NOT NULL
)");
}
###----------------------------------------------------------------###
sub main_info_complete { 0 }
sub main_hash_swap {
my $self = shift;
my $s = "SELECT id, title, date_added
FROM recipe
ORDER BY date_added";
my $data = $self->dbh->selectall_arrayref($s);
my @data = map {my %h; @h{qw(id title date_added)} = @$_; \%h} @$data;
return {
recipies => \@data,
};
}
###----------------------------------------------------------------###
sub add_name_step { 'edit' }
sub add_hash_validation {
return {
'group order' => [qw(title ingredients directions)],
title => {
required => 1,
max_len => 30,
},
ingredients => {
required => 1,
max_len => 255,
},
directions => {
required => 1,
max_len => 255,
},
};
}
sub add_finalize {
my $self = shift;
my $form = $self->form;
my $s = "SELECT COUNT(*) FROM recipe WHERE title = ?";
my ($count) = $self->dbh->selectrow_array($s, {}, $form->{'title'});
if ($count) {
$self->add_errors(title => 'A recipe by this title already exists');
return 0;
}
$s = "INSERT INTO recipe (title, ingredients, directions, date_added)
VALUES (?, ?, ?, ?)";
$self->dbh->do($s, {}, $form->{'title'},
$form->{'ingredients'},
$form->{'directions'},
scalar(localtime));
$self->add_to_form(success => "Recipe added to the database");
return 1;
}
###----------------------------------------------------------------###
sub edit_skip { shift->form->{'id'} ? 0 : 1 }
sub edit_hash_common {
my $self = shift;
return {} if $self->ready_validate;
my $sth = $self->dbh->prepare("SELECT * FROM recipe WHERE id = ?");
$sth->execute($self->form->{'id'});
my $hash = $sth->fetchrow_hashref;
return $hash;
}
sub edit_hash_validation { shift->add_hash_validation(@_) }
sub edit_finalize {
my $self = shift;
my $form = $self->form;
my $s = "SELECT COUNT(*) FROM recipe WHERE title = ? AND id != ?";
my ($count) = $self->dbh->selectrow_array($s, {}, $form->{'title'}, $form->{'id'});
if ($count) {
$self->add_errors(title => 'A recipe by this title already exists');
return 0;
}
my $s = "UPDATE recipe SET title = ?, ingredients = ?, directions = ? WHERE id = ?";
$self->dbh->do($s, {}, $form->{'title'},
$form->{'ingredients'},
$form->{'directions'},
$form->{'id'});
$self->add_to_form(success => "Recipe updated in the database");
return 1;
}
###----------------------------------------------------------------###
sub view_skip { shift->edit_skip(@_) }
sub view_hash_common { shift->edit_hash_common(@_) }
###----------------------------------------------------------------###
sub delete_skip { shift->edit_skip(@_) }
sub delete_info_complete { 1 }
sub delete_finalize {
my $self = shift;
$self->dbh->do("DELETE FROM recipe WHERE id = ?", {}, $self->form->{'id'});
$self->add_to_form(success => "Recipe deleted from the database");
return 1;
}
1;
__END__
File: /var/www/templates/content/recipe/main.html
### --------------------------------------------
Recipe DB
Recipe DB
[% IF success %][% success %]
[% END %]
File: /var/www/templates/content/recipe/edit.html
### --------------------------------------------
[% step == 'add' ? "Add" : "Edit" %] Recipe
[% step == 'add' ? "Add" : "Edit" %] Recipe
(Main Menu)
[% IF step != 'add' ~%]
(Delete this recipe)
[%~ END %]
[% js_validation %]
File: /var/www/templates/content/recipe/view.html
### --------------------------------------------
[% title %] - Recipe DB
[% title %]
Date Added: [% date_added %]
Ingredients
[% ingredients %]
Directions
[% directions %]
(Main Menu)
(Edit this recipe)
### --------------------------------------------
Notes:
The dbh method returns an SQLite dbh handle and auto creates the
schema. You will normally want to use MySQL or Oracle, or Postgres
and you will want your schema to NOT be auto-created.
This sample uses hand rolled SQL. Class::DBI or a similar module
might make this example shorter. However, more complex cases that
need to involve two or three or four tables would probably be better
off using the hand crafted SQL.
This sample uses SQL. You could write the application to use whatever
storage you want - or even to do nothing with the submitted data.
We had to write our own HTML (Catalyst and Ruby on Rails do this for
you). For most development work - the HTML should be in a static
location so that it can be worked on by designers. It is nice that
the other frameworks give you stub html - but that is all it is. It
is worth about as much as copying and pasting the above examples. All
worthwhile HTML will go through a non-automated design/finalization
process.
The add step used the same template as the edit step. We did
this using the add_name_step hook which returned "edit". The template
contains IF conditions to show different information if we were in
add mode or edit mode.
We reused code, validation, and templates. Code and data reuse is a
good thing.
The edit_hash_common returns an empty hashref if the form was ready to
validate. When hash_common is called and the form is ready to
validate, that means the form failed validation and is now printing
out the page. To let us fall back and use the "sticky" form fields
that were just submitted, we need to not provide values in the
hash_common method.
We use hash_common. Values from hash_common are used for both
template swapping and filling. We could have used hash_swap and
hash_fill independently.
The hook main_info_complete is hard coded to 0. This basically says
that we will never try and validate or finalize the main step - which
is most often the case.
=head1 SEPARATING STEPS INTO SEPARATE FILES
It may be useful sometimes to separate some or all of the steps of an
application into separate files. This is the way that CGI::Prototype
works. This is useful in cases were some steps and their hooks are
overly large - or are seldom used.
The following modifications can be made to the previous "recipe db"
example that would move the "delete" step into its own file. Similar
actions can be taken to break other steps into their own file as well.
### File: /var/www/lib/Recipe.pm
### Same as before but add the following line:
### --------------------------------------------
sub allow_morph { 1 }
### File: /var/www/lib/Recipe/Delete.pm
### Remove the delete_* subs from lib/Recipe.pm
### --------------------------------------------
package Recipe::Delete;
use strict;
use base qw(Recipe);
sub skip { shift->edit_skip(@_) }
sub info_complete { 1 }
sub finalize {
my $self = shift;
$self->dbh->do("DELETE FROM recipe WHERE id = ?", {}, $self->form->{'id'});
$self->add_to_form(success => "Recipe deleted from the database");
return 1;
}
Notes:
The hooks that are called (skip, info_complete, and finalize) do not
have to be prefixed with the step name because they are now in their
own individual package space. However, they could still be named
delete_skip, delete_info_complete, and delete_finalize and the
run_hook method will find them (this would allow several steps with
the same "morph_package" to still be stored in the same external
module).
The method allow_morph is passed the step that we are attempting to
morph to. If allow_morph returns true every time, then it will try
and require the extra packages every time that step is ran. You could
limit the morphing process to run only on certain steps by using code
similar to the following:
sub allow_morph { return {delete => 1} }
# OR
sub allow_morph {
my ($self, $step) = @_;
return ($step eq 'delete') ? 1 : 0;
}
The CGI::Ex::App temporarily blesses the object into the
"morph_package" for the duration of the step and re-blesses it into the
original package upon exit. See the morph method and allow_morph for more
information.
=head1 RUNNING UNDER MOD_PERL
The previous samples are essentially suitable for running under flat CGI,
Fast CGI, or mod_perl Registry or mod_perl PerlRun type environments. It
is very easy to move the previous example to be a true mod_perl handler.
To convert the previous recipe example, simply add the following:
### File: /var/www/lib/Recipe.pm
### Same as before but add the following lines:
### --------------------------------------------
sub handler {
Recipe->navigate;
return;
}
### File: apache2.conf - or whatever your apache conf file is.
### --------------------------------------------
SetHandler perl-script
PerlHandler Recipe
Notes:
Both the /cgi-bin/recipe version and the /recipe version can co-exist.
One of them will be a normal cgi and the other will correctly use
mod_perl hooks for headers.
Setting the location to /recipe means that the $ENV{SCRIPT_NAME} will
also be set to /recipe. This means that name_module method will
resolve to "recipe". If a different URI location is desired such as
"/my_cool_recipe" but the program is to use the same template content
(in the /var/www/templates/content/recipe directory), then we would
need to explicitly set the "name_module" parameter. It could be done
in either of the following ways:
### File: /var/www/lib/Recipe.pm
### Same as before but add the following line:
### --------------------------------------------
sub name_module { 'recipe' }
# OR
sub init {
my $self = shift;
$self->{'name_module'} = 'recipe';
}
In most use cases it isn't necessary to set name_module, but it also
doesn't hurt and in all cases it is more descriptive to anybody who is
going to maintain the code later.
=head1 ADDING AUTHENTICATION TO THE ENTIRE APPLICATION
Having authentication is sometimes a good thing. To force
the entire application to be authenticated (require a valid username
and password before doing anything) you could do the following.
### File: /var/www/lib/Recipe.pm
### Same as before but add
### --------------------------------------------
sub get_pass_by_user {
my $self = shift;
my $user = shift;
my $pass = $self->lookup_and_cache_the_pass($user);
return $pass;
}
### File: /var/www/cgi-bin/recipe (depending upon Apache configuration)
### Change the line with ->navigate; to
### --------------------------------------------
Recipe->navigate_authenticated;
# OR
### File: /var/www/lib/Recipe.pm
### Same as before but add
### --------------------------------------------
sub require_auth { 1 }
# OR
### File: /var/www/lib/Recipe.pm
### Same as before but add
### --------------------------------------------
sub init { shift->require_auth(1) }
See the require_auth, get_valid_auth, and auth_args methods for more information.
Also see the L perldoc.
=head1 ADDING AUTHENTICATION TO INDIVIDUAL STEPS
Sometimes you may only want to have certain steps require
authentication. For example, in the previous recipe example we
might want to let the main and view steps be accessible to anybody,
but require authentication for the add, edit, and delete steps.
To do this, we would do the following to the original example (the
navigation must start with ->navigate. Starting with ->navigate_authenticated
will cause all steps to require validation):
### File: /var/www/lib/Recipe.pm
### Same as before but add
### --------------------------------------------
sub get_pass_by_user {
my $self = shift;
my $user = shift;
my $pass = $self->lookup_and_cache_the_pass($user);
return $pass;
}
sub require_auth { {add => 1, edit => 1, delete => 1} }
That's it. The add, edit, and delete steps will now require authentication.
See the require_auth, get_valid_auth, and auth_args methods for more information.
Also see the L perldoc.
=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