|
NAMECGI::Ex::App - Anti-framework application framework. VERSIONversion 2.55 SYNOPSISA basic example: -------- File: /cgi-bin/my_cgi --------
#!/usr/bin/perl -w
use strict;
use base qw(CGI::Ex::App);
__PACKAGE__->navigate;
exit;
sub main_file_print {
return \ "Hello World!";
}
Properly put content in an external file... -------- File: /cgi-bin/my_cgi --------
#!/usr/bin/perl -w
use strict;
use base qw(CGI::Ex::App);
__PACKAGE__->navigate;
sub template_path { '/var/www/templates' }
-------- File: /var/www/templates/my_cgi/main.html --------
Hello World!
Adding substitutions... -------- File: /cgi-bin/my_cgi --------
#!/usr/bin/perl -w
use strict;
use base qw(CGI::Ex::App);
__PACKAGE__->navigate;
sub template_path { '/var/www/templates' }
sub main_hash_swap {
my $self = shift;
return {
greeting => 'Hello',
date => sub { scalar localtime },
};
}
-------- File: /var/www/templates/my_cgi/main.html --------
[% greeting %] World! ([% date %])
Add forms and validation (inluding javascript validation)... -------- File: /cgi-bin/my_cgi --------
#!/usr/bin/perl -w
use strict;
use base qw(CGI::Ex::App);
__PACKAGE__->navigate;
sub template_path { '/var/www/templates' }
sub main_hash_swap { {date => sub { scalar localtime }} }
sub main_hash_fill {
return {
guess => 50,
};
}
sub main_hash_validation {
return {
guess => {
required => 1,
compare1 => '<= 100',
compare1_error => 'Please enter a value less than 101',
compare2 => '> 0',
compare2_error => 'Please enter a value greater than 0',
},
};
}
sub main_finalize {
my $self = shift;
my $form = $self->form;
$self->add_to_form({was_correct => ($form->{'guess'} == 23)});
return 0; # indicate to show the page without trying to move along
}
-------- File: /var/www/templates/my_cgi/main.html --------
<h2>Hello World! ([% date %])</h2>
[% IF was_correct %]
<b>Correct!</b> - The number was [% guess %].<br>
[% ELSIF guess %]
<b>Incorrect</b> - The number was not [% guess %].<br>
[% END %]
<form name="[% form_name %]" method="post">
Enter a number between 1 and 100: <input type="text" name="guess"><br>
<span id="guess_error" style="color:red">[% guess_error %]</span><br>
<input type="submit">
</form>
[% js_validation %]
There are infinite possibilities. There is a longer "SYNOPSIS" after the process flow discussion and more examples near the end of this document. It is interesting to note that there have been no databases so far. It is very, very difficult to find a single database abstraction that fits every model. CGI::Ex::App is Controller/Viewer that is somewhat Model agnostic and doesn't come with any default database abstraction. DESCRIPTIONFill 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. However, there are various design patterns for CGI applications that CGI::Ex::App handles for you that the other frameworks require you to bring in extra support. The entire CGI::Ex suite has been taylored to work seamlessly together. Your mileage in building applications 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 you 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 your key actions and html. One of the great benefits of CGI::Ex::App vs. Catalyst or Rails style frameworks is that the model of CGI::Ex::App can be much more abstract. And models often are abstract. DEFAULT PROCESS FLOWThe 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. navigateThe process starts off by calling ->navigate. navigate {
eval {
->pre_navigate
->nav_loop
->post_navigate
}
# dying errors will run the ->handle_error method
->destroy
}
nav_loopThe nav_loop method will run as follows: nav_loop {
->path (get the array of path steps)
# ->path_info_map_base (method - map ENV PATH_INFO to form)
# 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 {
->require_auth (hook)
# exits nav_loop if true
->morph (hook)
# check ->allow_morph (hook)
# ->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
->path_info_map (hook - map PATH_INFO to form)
->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 (hook)
# ->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
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)
# skips this step if true and exit nav_loop
->skip (hook)
# skips this step if true and stays in nav_loop
->prepare (hook - defaults to true)
->info_complete (hook - ran if prepare was true)
->ready_validate (hook)
->validate_when_data (hook)
# returns false from info_complete if ! ready_validate
->validate (hook - uses CGI::Ex::Validate to validate form info)
->hash_validation (hook)
->file_val (hook)
->vob_path (defaults to template_path)
->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 Template::Alloy)
->template_args (hook - passed to Template::Alloy->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. Additionally, 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. MAPPING URI TO STEPThe default out of the box configuration will map URIs to steps as follows: # Assuming /cgi-bin/my_app is the program being run
URI: /cgi-bin/my_app
STEP: main
FORM: {}
WHY: No other information is passed. The path method is
called which eventually calls ->default_step which
defaults to "main"
URI: /cgi-bin/my_app?foo=bar
STEP: main
FORM: {foo => "bar"}
WHY: Same as previous example except that QUERY_STRING
information was passed and placed in form.
URI: /cgi-bin/my_app?step=my_step
STEP: my_step
FORM: {step => "my_step"}
WHY: The path method is called which looks in $self->form
for the key ->step_key (which defaults to "step").
URI: /cgi-bin/my_app?step=my_step&foo=bar
STEP: my_step
FORM: {foo => "bar", step => "my_step"}
WHY: Same as before but another parameter was passed.
URI: /cgi-bin/my_app/my_step
STEP: my_step
FORM: {step => "my_step"}
WHY: The path method is called which called path_info_map_base
which matched $ENV{'PATH_INFO'} using the default regex
of qr{^/(\w+)$} and place the result in
$self->form->{$self->step_key}. Path then looks in
$self->form->{$self->step_key} for the initial step. See
the path_info_map_base method for more information.
URI: /cgi-bin/my_app/my_step?foo=bar
STEP: my_step
FORM: {foo => "bar", step => "my_step"}
WHY: Same as before but other parameters were passed.
URI: /cgi-bin/my_app/my_step?step=other_step
STEP: other_step
FORM: {step => "other_step"}
WHY: The same procedure took place, but when the PATH_INFO
string was matched, the form key "step" already existed
and was not replaced by the value from PATH_INFO.
The remaining examples in this section are based on the assumption that the following method is installed in your script. sub my_step_path_info_map {
return [
[qr{^/\w+/(\w+)/(\d+)$}, 'foo', 'id'],
[qr{^/\w+/(\w+)$}, 'foo'],
[qr{^/\w+/(.+)$}, 'anything_else'],
];
}
URI: /cgi-bin/my_app/my_step/bar
STEP: my_step
FORM: {foo => "bar"}
WHY: The step was matched as in previous examples using
path_info_map_base. However, the form key "foo"
was set to "bar" because the second regex returned
by the path_info_map hook matched the PATH_INFO string
and the corresponding matched value was placed into
the form using the keys specified following the regex.
URI: /cgi-bin/my_app/my_step/bar/1234
STEP: my_step
FORM: {foo => "bar", id => "1234"}
WHY: Same as the previous example, except that the first
regex matched the string. The first regex had two
match groups and two form keys specified. Note that
it is important to order your match regexes in the
order that will match the most data. The third regex
would also match this PATH_INFO.
URI: /cgi-bin/my_app/my_step/some/other/type/of/data
STEP: my_step
FORM: {anything_else => 'some/other/type/of/data'}
WHY: Same as the previous example, except that the third
regex matched.
URI: /cgi-bin/my_app/my_step/bar?bling=blang
STEP: my_step
FORM: {foo => "bar", bling => "blang"}
WHY: Same as the first sample, but additional QUERY_STRING
information was passed.
URI: /cgi-bin/my_app/my_step/one%20two?bar=three%20four
STEP: my_step
FORM: {anything_else => "one two", bar => "three four"}
WHY: The third path_info_map regex matched. Note that the
%20 in bar was unescaped by CGI::param, but the %20
in anything_else was unescaped by Apache. If you are
not using Apache, this behavior may vary. CGI::Ex::App
doesn't decode parameters mapped from PATH_INFO.
See the path method for more information about finding the initial step of the path. The form method calls CGI::Ex::form which uses CGI::param to retrieve GET and POST parameters. See the form method for more information on how GET and POST parameters are parsed. See the path_info_map_base method, and path_info_map hook for more information on how the path_info maps function. Using the following code is very useful for determing what hooks have taken place: use CGI::Ex::Dump qw(debug);
sub post_navigate {
my $self = shift;
debug $self->dump_history, $self->form;
}
ADDING DATA VALIDATION TO A STEPCGI::Ex::App uses CGI::Ex::Validate for its data validation. See CGI::Ex::Validate for more information about the many ways you can validate your data. The default hash_validation hook returns an empty hashref. This means that passed in data is all valid and the script will automatically call the step's finalize method. The following shows how to add some contrived validation to a step called "my_step". sub my_step_hash_validation {
return {
username => {
required => 1,
match => 'm/^(\w+)$/',
match_error => 'The $field field may only contain word characters',
max_len => 20,
},
password => {
required => 1,
max_len => 15,
},
password_verify => {
validate_if => 'password',
equals => 'password',
},
usertype => {
required => 1,
enum => [qw(animal vegetable mineral)],
},
};
}
The step will continue to display the html form until all of the fields pass validation. See the hash_validation hook and validate hook for more information about how this takes place. ADDING JAVASCRIPT DATA VALIDATION TO A STEPYou must first provide a hash_validation hook as explained in the previous section. Once you have a hash_validation hook, you would place the following tags into your HTML template. <form name="[% form_name %]" method="post">
...
</form>
[% js_validation %]
The "form_name" swap-in places a name on the form that the javascript returned by the js_validation swap-in will be able to find and check for validity. See the hash_validation, js_validation, and form_name hooks for more information. Also, CGI::Ex::validate.js allows for inline errors in addition to or in replacement of an alert message. To use inline errors, you must provide an element in your HTML document where this inline message can be placed. The common way to do it is as follows: <input type="text" name="username"><br>
<span class="error" id="username_error">[% username_error %]</span>
The span around the error allows for the error css class and it provides a location that the Javascript validation can populate with errors. The [% username_error %] provides a location for errors generated on the server side to be swapped in. If there was no error the [% username_error %] tag would default to "". ADDING ADDITIONAL TEMPLATE VARIABLESAll variables returned by the hash_base, hash_common, hash_form, hash_swap, and hash_errors hooks are available for swapping in templates. The following shows how to add variables using the hash_swap hook on the step "main". sub main_hash_swap {
return {
color => 'red',
choices => [qw(one two three)],
"warn" => sub { warn @_ },
};
}
You could also return the fields from the hash_common hook and they would be available in both the template swapping as well as form filling. See the hash_base, hash_common, hash_form, hash_swap, hash_errors, swap_template, and template_args hooks for more information. The default template engine used is Template::Alloy. The default interface used is TT which is the Template::Toolkit interface. Template::Alloy allows for using TT documents, HTML::Template documents, HTML::Template::Expr documents, Text::Tmpl documents, or Velocity (VTL) documents. See the Template::Alloy documentation for more information. ADDING ADDITIONAL FORM FILL VARIABLESAll variables returned by the hash_base, hash_common, hash_form, and hash_fill hooks are available for filling html fields in on templates. The following shows how to add variables using the hash_fill hook on the step "main". sub main_hash_fill {
return {
color => 'red',
choices => [qw(one two three)],
};
}
You could also return the fields from the hash_common hook and they would be available in both the form filling as well as in the template swapping. See the hash_base, hash_common, hash_form, hash_swap, hash_errors, fill_template, and fill_args hooks for more information. The default form filler is CGI::Ex::Fill which is similar to HTML::FillInForm but has several benefits. See the CGI::Ex::Fill module for the available options. FINDING TEMPLATES AND VALIDATION FILESCGI::Ex::App tries to help your applications use a good template directory layout, but allows for you to override everything. External template files are used for storing your html templates and for storing your validation files (if you use externally stored validation files). The default file_print hook will look for content on your file system, but it can also be completely overridden to return a reference to a scalar containing the contents of your file (beginning with version 2.14 string references can be cached which makes templates passed this way "first class" citizens). Actually it can return anything that Template::Alloy (Template::Toolkit compatible) will treat as input. This templated html is displayed to the user during any step that enters the "print" phase. Similarly the default file_val hook will look for a validation file on the file system, but it too can return a reference to a scalar containing the contents of a validation file. It may actually return anything that the CGI::Ex::Validate get_validation method is able to understand. This validation is used by the default "info_complete" method for verifying if the submitted information passes its specific checks. A more common way of inlining validation is to return a validation hash from a hash_validation hook override. If the default file_print and file_val hooks are used, the following methods are employed for finding templates and validation files on your filesystem (they are also documented more in the HOOKS AND METHODS section.
It may be easier to understand the usage of each of these methods by showing a contrived example. The following is a hypothetical layout for your templates: /home/user/templates/
/home/user/templates/chunks/
/home/user/templates/wrappers/
/home/user/templates/content/
/home/user/templates/content/my_app/
/home/user/templates/content/my_app/main.html
/home/user/templates/content/my_app/step1.html
/home/user/templates/content/my_app/step1.val
/home/user/templates/content/another_cgi/main.html
In this example we would most likely set values as follows: template_path /home/user/templates
base_dir_rel content
name_module my_app
The name_module method defaults to the name of the running program, but with the path and extension removed. So if we were running /cgi-bin/my_app.pl, /cgi-bin/my_app, or /anypath/my_app, then name_module would default to "my_app" and we wouldn't have to hard code the value. Often it is wise to set the value anyway so that we can change the name of the cgi script without effecting where template content should be stored. Continuing with the example and assuming that name of the step that the user has requested is "step1" then the following values would be returned: template_path /home/user/templates
base_dir_rel content
name_module my_app
name_step step1
ext_print html
ext_val val
file_print content/my_app/step1.html
file_val /home/user/templates/content/my_app/step1.val
The call to the template engine would look something like the following: my $t = $self->template_obj({
INCLUDE_PATH => $self->template_path, # defaults to base_dir_abs
});
$t->process($self->file_print($step), \%vars);
The template engine would then look for the relative file inside of the absolute paths (from template_path). The call to the validation engine would pass the absolute filename that is returned by file_val. The name_module and name_step methods can return filenames with additional directories included. The previous example could also have been setup using the following values: template_path /home/user/templates
base_dir_rel
name_module content/my_app
In this case the same values would be returned for the file_print and file_val hooks as were returned in the previous setup. SYNOPSIS (A LONG "SYNOPSIS")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 \ "<h1>Main Step</h1>
<form method=post name=[% form_name %]>
<input type=hidden name=step>
<table>
<tr>
<td><b>Username:</b></td>
<td><input type=text name=username><span style='color:red' id=username_error>[% username_error %]</span></td>
</tr><tr>
<td><b>Password:</b></td>
<td><input type=text name=password><span style='color:red' id=password_error>[% password_error %]</span></td>
</tr><tr>
<td><b>Verify Password:</b></td>
<td><input type=text name=password2><span style='color:red' id=password2_error>[% password2_error %]</span></td>
</tr>
<tr><td colspan=2 align=right><input type=submit></td></tr>
</table>
</form>
[% 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 {
\ "<div style=background:lightblue>
<h1>Success Step - [% success_msg %]</h1>
Username: <b>[% username %]</b><br>
Password: <b>[% password %]</b><br>
</div>
";
}
__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 Template::Alloy). SYNOPSIS STEP BY STEPThis 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 or each step may choose the next step that should follow. The navigate method will go through one 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 goto_step, 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 automatically at a later point with the current step we are on. We provide locations to swap in inline errors. <span style="color:red" id="username_error">[% username_error %]</span> 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. AVAILABLE METHODS / HOOKSCGI::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. Some hooks expect "magic" values to be replaced. Often they are intuitive, but sometimes it is easy to forget. For example, the finalize hook should return true (default) to indicate the step is complete and false to indicate that it failed and the page should be redisplayed. You can import a set of constants that allows for human readible names. use CGI::Ex::App qw(:App__finalize);
OR
use MyAppPkg qw(:App__finalize); # if it is a subclass of CGI::Ex::App
This would import the following constants: App__finalize__failed_and_show_page (0), App__finalize__finished_and_move_to_next_step => (1 - default), and App__finalize__finished_but_show_page ("" - still false). These constants are provided by CGI::Ex::App::Constants which also contains more options for usage. The following is the alphabetical list of methods and hooks.
HOW DO I SET COOKIES, REDIRECT, ETCOften in your program you will want to set cookies or bounce to a differnt URL. This can be done using either the builtin CGI::Ex object or the built in CGI object. It is suggested that you only use the CGI::Ex methods as it will automatically send headers and method calls under cgi, mod_perl1, or mod_perl2. The following shows how to do basic items using the CGI::Ex object returned by the ->cgix method.
See the CGI::Ex and CGI documentation for more information. COMPARISON TO OTHER APPLICATION MODULESThe 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 other application builders 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). And if what you are doing on a particular is far too complicated or custom for what CGI::Ex::App provides, CGI::Ex::App makes it trivial to override all behavior. 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.
SIMPLE EXTENDED EXAMPLEThe 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 template_path 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 template_path { '/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;
}
$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
### --------------------------------------------
<html>
<head>
<title>Recipe DB</title>
</head>
<h1>Recipe DB</h1>
[% IF success %]<span style="color:darkgreen"><h2>[% success %]</h2></span>[% END %]
<table style="border:1px solid blue">
<tr><th>#</th><th>Title</th><th>Date Added</th></tr>
[% FOR row IN recipies %]
<tr>
<td>[% loop.count %].</td>
<td><a href="[% script_name %]/view?id=[% row.id %]">[% row.title %]</a>
(<a href="[% script_name %]/edit?id=[% row.id %]">Edit</a>)
</td>
<td>[% row.date_added %]</td>
</tr>
[% END %]
<tr><td colspan=2 align=right><a href="[% script_name %]/add">Add new recipe</a></td></tr>
</table>
</html>
File: /var/www/templates/content/recipe/edit.html
### --------------------------------------------
<html>
<head>
<title>[% step == 'add' ? "Add" : "Edit" %] Recipe</title>
</head>
<h1>[% step == 'add' ? "Add" : "Edit" %] Recipe</h1>
<form method=post name=[% form_name %]>
<input type=hidden name=step>
<table>
[% IF step != 'add' ~%]
<tr>
<td><b>Id:</b></td><td>[% id %]</td></tr>
<input type=hidden name=id>
</tr>
<tr>
<td><b>Date Added:</b></td><td>[% date_added %]</td></tr>
</tr>
[% END ~%]
<tr>
<td valign=top><b>Title:</b></td>
<td><input type=text name=title>
<span style='color:red' id=title_error>[% title_error %]</span></td>
</tr>
<tr>
<td valign=top><b>Ingredients:</b></td>
<td><textarea name=ingredients rows=10 cols=40 wrap=physical></textarea>
<span style='color:red' id=ingredients_error>[% ingredients_error %]</span></td>
</tr>
<tr>
<td valign=top><b>Directions:</b></td>
<td><textarea name=directions rows=10 cols=40 wrap=virtual></textarea>
<span style='color:red' id=directions_error>[% directions_error %]</span></td>
</tr>
<tr>
<td colspan=2 align=right>
<input type=submit value="[% step == 'add' ? 'Add' : 'Update' %]"></td>
</tr>
</table>
</form>
(<a href="[% script_name %]">Main Menu</a>)
[% IF step != 'add' ~%]
(<a href="[% script_name %]/delete?id=[% id %]">Delete this recipe</a>)
[%~ END %]
[% js_validation %]
</html>
File: /var/www/templates/content/recipe/view.html
### --------------------------------------------
<html>
<head>
<title>[% title %] - Recipe DB</title>
</head>
<h1>[% title %]</h1>
<h3>Date Added: [% date_added %]</h3>
<h2>Ingredients</h2>
[% ingredients %]
<h2>Directions</h2>
[% directions %]
<hr>
(<a href="[% script_name %]">Main Menu</a>)
(<a href="[% script_name %]/edit?id=[% id %]">Edit this recipe</a>)
</html>
### --------------------------------------------
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. SEPARATING STEPS INTO SEPARATE FILESIt 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. RUNNING UNDER MOD_PERLThe 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.
### --------------------------------------------
<Location /recipe>
SetHandler perl-script
PerlHandler Recipe
</Location>
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. ADDING AUTHENTICATION TO THE ENTIRE APPLICATIONHaving 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 CGI::Ex::Auth perldoc. ADDING AUTHENTICATION TO INDIVIDUAL STEPSSometimes 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} }
We could also enable authentication by using individual hooks as in: sub add_require_auth { 1 }
sub edit_require_auth { 1 }
sub delete_require_auth { 1 }
Or we could require authentication on everything - but let a few steps in: sub require_auth { 1 } # turn authentication on for all
sub main_require_auth { 0 } # turn it off for main and view
sub view_require_auth { 0 }
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 CGI::Ex::Auth perldoc. THANKSThe following corporation and individuals contributed in some part to the original versions. 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.
Krassimir Berov - feedback and some warnings issues with POD examples.
LICENSEThis module may be distributed under the same terms as Perl itself. AUTHORPaul Seamons <perl at seamons dot com>
|