CGI::Ex::App(3)
β¨ CGI::Ex::App β¨
okay bestie, welcome to the CGI::Ex::App(3) page β this is the User Contributed Perl Documentation one, and yes it's a whole journey, but it's a good journey. π
πΈ NAME πΈ
CGI::Ex::App β the anti-framework application framework. yes. that's literally the name. it refuses to be a framework while absolutely being one. main character energy fr. π
π VERSION π
version 2.55
π¦ SYNOPSIS π¦
A basic example, i.e. the "hello world but make it CGI":
-------- 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!";
}
Then, because we are not chaotic, we put content in an external file properly...
-------- 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!
Now we're adding substitutions, aka making the page actually say things:
-------- 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 %])
And then the glow-up: forms and validation (including javascript validation, because we're thorough π ):
-------- 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, no cap. There's a longer "SYNOPSIS" after the process flow discussion, plus more examples near the end of this document. And okay, interesting note: 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. it's giving "you bring the model, i'll bring the vibes". π¦
π DESCRIPTION π
Fill in the blanks and get a ready made web application. literally that's it, that's the pitch. β¨
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. (that's the official disclaimer bestie, we're being honest here π)
So like β 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. every single one of those? still for you. we're inclusive here. π«Ά
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. periodt. π
π 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. trust the process bestie, it's lowkey elegant once you see it. β¨
π 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
}
π§ nav_loop
The 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. no pressure, start small, glow up later. π±β¨
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. free stuff?? obsessed. π
πΊοΈ MAPPING URI TO STEP πΊοΈ
okay so out of the box, with the default config, CGI::Ex::App maps your URIs to steps like this π bestie this is the whole vibe:
# 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.
Now the rest of these examples are assuming you've got this method installed in your script, mmkay:
sub my_step_path_info_map {
return [
[qr{^/\w+/(\w+)/(\d+)$}, 'foo', 'id'],
[qr{^/\w+/(\w+)$}, 'foo'],
[qr{^/\w+/(.+)$}, 'anything_else'],
];
}
and now look what happens π¦
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.
ngl this next bit is so useful for figuring out what hooks actually fired. literally a lifesaver π
use CGI::Ex::Dump qw(debug);
sub post_navigate {
my $self = shift;
debug $self->dump_history, $self->form;
}
β ADDING DATA VALIDATION TO A STEP β
CGI::Ex::App uses CGI::Ex::Validate for its data validation, periodt. See CGI::Ex::Validate for more information about the many ways you can validate your data β there's a lot, bestie.
The default hash_validation hook returns an empty hashref. What does that mean? It means all the data you passed in is treated as valid, and the script will automatically call the step's finalize method. no thoughts, just vibes β¨
Here's how you'd add some (contrived, sure) 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 just keep showing that html form until all of the fields pass validation. the way it won't let you through until you're validβ¦ iconic π
See the hash_validation hook and validate hook for more information about how this takes place.
π ADDING JAVASCRIPT DATA VALIDATION TO A STEP π
first things first β you must provide a hash_validation hook, as explained in the previous section. no hook, no js validation, that's the deal.
Once you've got a hash_validation hook, you'd drop these 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. it's giving teamwork π«Ά
See the hash_validation, js_validation, and form_name hooks for more information.
Also β and this is lowkey the best part β 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 "". flawless πΈ
π ADDING ADDITIONAL TEMPLATE VARIABLES π
All variables returned by the hash_base, hash_common, hash_form, hash_swap, and hash_errors hooks are available for swapping in templates. yes really, all of them.
Here's 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. two-for-one, we love to see it π
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. it's giving range π
π ADDING ADDITIONAL FORM FILL VARIABLES π
All variables returned by the hash_base, hash_common, hash_form, and hash_fill hooks are available for filling html fields in on templates.
Here's 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. same deal as before, obsessed π
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 FILES π¦
okay so CGI::Ex::App is literally out here trying to help your app have a cute lil template directory layout, bestie. but also?? it lets you override everything. no gatekeeping.
so. external template files are where your html templates live, and also where your validation files live (if you're doing the externally-stored-validation-files thing).
the default file_print hook will go looking for content on your file system β but plot twist, you can completely override it to return a reference to a scalar holding the contents of your file instead (and starting with version 2.14, string references can be cached, which makes templates passed this way "first class" citizens π
). honestly it can return anything that Template::Alloy (Template::Toolkit compatible, ily) will treat as input. this templated html gets shown to the user during any step that enters the "print" phase. main character moment fr.
similarly, the default file_val hook goes looking for a validation file on the file system, but bestie it too can return a reference to a scalar containing the contents of a validation file. it can 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 checking whether the submitted info passes its specific checks. a more common way of inlining validation is to return a validation hash from a hash_validation hook override. slay.
if you are using the default file_print and file_val hooks, here are the methods it employs for finding templates and validation files on your filesystem (they're also documented more in the HOOKS AND METHODS section, iykyk):
template_pathβ absolute path or arrayref of paths to the base templates directory. defaults tobase_dir_abswhich defaults to['.']. πbase_dir_relβ relative path inside of thetemplate_pathdirectory where content can be found. default is"".name_moduleβ directory inside ofbase_dir_relwhere files for the current CGI (module) will be stored. default value is$ENV{SCRIPT_NAME}with the path and extension removed. iconic.name_stepβ used withext_printandext_valfor creating the filename that will be looked for inside of thename_moduledirectory. default value is the current step.ext_printandext_valβ filename extensions added toname_stepto create the filename looked for inside of thename_moduledirectory. default is"html"forext_printand"val"forext_val.
ngl it might be easier to get how each of these methods works if we do a lil contrived example. imagine this 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 like this:
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's wise to set the value anyway though, so that we can change the name of the cgi script without affecting where template content should be stored.
continuing with the example and assuming that the name of the step the user requested is "step1", then these 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 this:
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 set up using these 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. periodt. β¨
π 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, no judgement π). honestly the best way to get started is to paste the following code into a cgi script (such as cgi-bin/my_app) and just try it out. there's a detailed walk-through coming in the next section. there's 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's 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 STEP β¨
Okay bestie, let's walk through that whole example line by line, no skipping, no skimming, we're doing this properly π
So we start off with the classic CGI intro, the little ritual that opens every script:
#!/usr/bin/perl -w
use strict;
use base qw(CGI::Ex::App);
use CGI::Ex::Dump qw(debug);
Note: that use base thing is not normally used in the "main" portion of a script. But it does let us just do __PACKAGE__->navigate and vibe, so here we are π¦
Now we gotta actually invoke the process:
__PACKAGE__->navigate;
# OR
# my $obj = __PACKAGE__->new;
# $obj->navigate;
exit;
Note: that exit isn't strictly necessary β but it's lowkey nice because it makes it obvious that the process flow does not go beyond the ->navigate call. Slay π
The navigate routine is now gonna try and "run" through a series of steps. Navigate will call the ->path method, which should return an arrayref holding the valid steps. By default β like, if you never overrode the path method β path will first default to the step found in the form key named ->step_name, and then it'll fall back to the contents of $ENV{'PATH_INFO'}. If navigation runs out of steps to run, it'll 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. And the URI /cgi-bin/my_app/bar would run the step 'bar' first. Iconic β¨
CGI::Ex::App lets you run steps in a preset path, OR each step can choose the next step that should follow. The navigate method goes through one step of the path at a time and checks if it's completed (and yeah, there's a bunch of methods that decide what "completed" even means). This preset type of path can also be automated using the CGI::Path module. But rather than using a preset path, CGI::Ex::App also has methods that allow for dynamic changing of the path, so each step can decide which step to do next (go peep 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, right? That info is stored in the arrayref contained in ->history. There's a method that gets called after all the navigation has taken place, called "post_navigate". This chunk will display history after we've printed the content:
sub post_navigate {
debug shift->dump_history;
} # show what happened
Ok. Finally we're 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's 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's 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 info 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 lets us 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 defaulthash_basehook 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's 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's 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've 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's normally a good idea to set this, because 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 / 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's maybe easier to show than to explain, so watch this β¨
If we're 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 can't find a method by that name, it'll look for a method named "print". If it can't 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're intuitive, but sometimes it's 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.
allow_morph (hook)
Should return true if this step is allowed to "morph" the current App object into another package. Default is false. It's passed a single argument of the current step. For more granularity, if the true value is a hash, the step being morphed to must be in the hash.
If the returned value is "1", and the module doesn't exist, then the App will continue to run blessed into the current package. If there is an error requiring the module, or if the module doesn't exist and the return value is "2" (true but not 1), then App will die with the appropriate error π
To enable morphing for all steps, add the following: (packages that don't exists won't be morphed to)
sub allow_morph { 1 }
To force morphing for all steps add the following:
sub allow_morph { 2 }
To enable morph on specific steps, do either of the following:
sub allow_morph {
return {
edit => 1,
delete => 2, # must morph
};
}
# OR
sub allow_morph {
my ($self, $step) = @_;
return 1 if $step eq 'edit';
return 2 if $step eq 'delete';
return;
}
See the morph "hook" for more information.
append_path (method)
Arguments are the steps to append. Can be called any time. Adds more steps to the end of the current path πΈ
auth_args (method)
Should return a hashref that will be passed to the auth_obj method, which should return a CGI::Ex::Auth compatible object. 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 => '<h1>My login header</h1>',
login_footer => '[% TRY %][% INCLUDE login/login_footer.htm %][% CATCH %]<!-- [% error %] -->[% END %]',
secure_hash_keys => [qw(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccc 2222222222222)],
# use_blowfish => 'my_blowfish_key',
};
}
auth_data (method) π
Okay so this one holds the authentication data that got stashed while get_valid_auth was doing its thing. Bestie, listen β this data is normally blessed into the CGI::Ex::Auth::Data package, and here's the lil plot twist: that package evaluates to false if there was an error, and true if the auth actually worked. So likeβ¦ this data can be defined and still be false. It's giving mysterious. It's giving "defined but lying to you." π
Go peep the get_valid_auth method for the full tea.
auth_obj (method) π¦
Passed auth_args. Should hand you back a CGI::Ex::Auth compatible object. The default move is just to call CGI::Ex::Auth->new with whatever args got passed in. Easy, no cap.
base_dir_abs (method) β¨
This is the absolute base directory used to find template, validation and conf files. It can return a single value, or an arrayref of values, or even a coderef that returns an arrayref or coderef of values. Very flexible, very main character. π
You can also pass base_dir_abs as a parameter in the arguments you hand to the new method.
Default value is ['.'].
Say you wanna pass multiple paths β you'd do something like this:
sub base_dir_abs {
return ['/my/path/one', '/some/other/path'];
}
The base_dir_abs value gets used by template_path, along with base_dir_rel, name_module, name_step, ext_print and ext_values, to figure out the values that the default file_print and file_val hooks return. Go read those methods for the deeper convo.
Also peep the section on FINDING TEMPLATES for more.
And psst β base_dir_abs is also used as the default value for conf_path and vob_path. Multitasking icon.
base_dir_rel (method) π
This gets added as a relative base directory to content under the base_dir_abs directory.
Default value is "".
The template_path method is the top level where template includes may pull from, while base_dir_rel is the directory relative to the template_path where the content files will actually be stored.
A value for base_dir_rel can be passed as a parameter in the arguments passed to the new method.
See the template_path and base_dir_abs methods for more discussion. And see the section on FINDING TEMPLATES too. β¨
cleanup_user (method) πΈ
Used as a hook during get_valid_auth. It lets you clean up the username. Check the get_valid_auth method for context.
sub cleanup_user {
my ($self, $user) = @_;
return lc $user;
}
clear_app (method) π
If you're reusing the same CGI::Ex::App based object to run multiple navigate sessions, you should call the clear_app method β it'll try to clear as much session info as it possibly can. Spring cleaning era. π§Ή
Here's everything that gets cleared:
cgix
vob
form
cookies
stash
path
path_i
history
_morph_lineage_start_index
_morph_lineage
hash_errors
hash_fill
hash_swap
hash_common
conf (method) β
Used by default in init_from_conf if load_conf returns true. It'll try to read the file returned by the conf_file method using the object returned by conf_obj, using that object's read method. And if conf_validation returns a non-empty hashref, the conf hash gets validated using $self->vob->validate (see the validate method).
This method can be used for other stuff too β including when load_conf is false. Versatile queen.
Caches results in $self->{'conf'}.
And heads up: if the conf_file can't be found, this method will die β unless conf_die_on_fail returns 0 (it defaults to true). So like, by default it's dramatic about it. π
conf_args π
Used by conf_obj.
Defaults to $self->{'conf_args'} which defaults to {}. It'll have paths => $self->conf_path added before being passed to CGI::Ex::Conf->new.
conf_file (method) π¦
Used by conf for finding the config file to load. Defaults to $self->{'conf_file'}, which defaults to $self->name_module with the extension returned by $self->ext_conf added on. So like, if name_module returns "my_app" and ext_conf returns "ini", the value returned will be "my_app.ini".
The value returned can be absolute. If it's not, it'll be searched for in the paths passed to conf_obj.
The ext_conf can be any of those extensions understood by CGI::Ex::Conf.
conf_obj π₯
Used by the conf method to load the file returned by conf_file. Defaults to conf_obj, which defaults to loading args from conf_args, adding in paths returned by conf_path, and calling CGI::Ex::Conf->new.
Any object that provides a read method that returns a hashref can be used here. Your fave, honestly.
conf_path π
Defaults to $self->{'conf_path'}, which defaults to base_dir_abs. Should be a path, or an arrayref of paths, to look for the configuration file returned by conf_file β but only when that file is not absolute. iykyk.
conf_validation π
Used by the default conf method. Defaults to an empty hashref. If a non-empty hashref is passed, the hashref returned by conf_obj->read will be validated using the hashref returned by conf_validation.
current_step (method) π
Returns the current step that the nav_loop is functioning on. Live footage of where you're at. π
default_step (method) π
The 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 off the end of the path (runs out of steps), this method gets called, the step gets added to the path, and nav_loop calls itself recursively. Recursion, but make it cute. β¨
destroy (method) π
Called at the end of navigate, after all other actions have run. You can use it for undoing things done in the ->init method that got called during the ->new method. Cleanup era.
dump_history (method) β
Shows simplified trace info of which steps were called, the order they were called in, how long they took to run, and a brief list of the output. (Wanna see the full response returned by each hook? Pass a true value as the only argument to dump_history β like $self->dump_history(1).) Indentation also gets applied to show which hooks called other hooks. It's giving family tree. π³
The first line shows the amount of time elapsed for the entire navigate execution. The lines after that 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.
Heads up bestie β to get full output responses, pass a true value todump_historyβ or just call->history. The times displayed go to 5 decimal places, and that accuracy can only be provided if theTime::HiResmodule is installed on your system (it'll only be used if it's installed).
It's usually best to print this history during the post_navigate method, like this:
use CGI::Ex::Dump qw(debug);
sub post_navigate { debug shift->dump_history }
Here's 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 - require_auth - require_auth - 0.00001 - 0",
"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 - <html> ...",
" 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"
];
error_step (method) π₯
Defaults to "__error". It's the name of a step to run should a dying error get caught by the default handle_error method. See the handle_error method. π
exit_nav_loop (method) π
Okay so this method shouldn't normally be used, but there's genuinely no problem with using it on a regular basis either. Basically it's a "goto" that lets you long-jump to the end of all nav_loops β even if they're recursively nested. It effectively short circuits all remaining hooks for the current and remaining steps. It's what allows the ->goto_step functionality to work. If the application has morphed, it'll be unmorphed before returning. And also β the post_navigate method will still be called. No skipping that. periodt. π
ext_conf π¦
Used by the default conf_file method. Defaults to $self->{'ext_conf'}, which defaults to 'pl' β meaning the read configuration file should return a valid perl hashref.
ext_print (method) π
Added as a suffix to "name_step" during the default file_print hook.
Default value is 'html'.
For example, if name_step returns "foo" and ext_print returns "html", then the file "foo.html" will be searched for.
See the section on FINDING TEMPLATES for further discussion. β¨
ext_val (method) β¨
Okay so bestie, this one is a suffix. It gets tacked onto name_step while the default file_val hook is doing its thing.
Default value? 'val'. That's it, that's the whole vibe.
So like β if name_step hands back "foo" and ext_val hands back "val", then the app goes hunting for a file called foo.val. Iconic behavior honestly.
See the FINDING TEMPLATES section for the full tea on how that search goes down. π«Ά
fill_args (hook) π
Returns a hashref of args that get passed straight into CGI::Ex::Fill::fill. It gets augmented with the template to swap and the fill hash, so it's already got stuff in it.
When is this useful? Say you only wanna swap one particular form on the template page β yeah, this is your girl. The arguments go directly to the fill function, no filter, no middleman.
sub fill_args { {target => 'my_form'} }
fill_template (hook) π
Arguments are a template and a hashref. It takes the template that was prepped using swap_template, and swaps the html form fields using the hashref you handed it. If you override this method you get to control the fill behavior entirely β your house, your rules.
Heads up: it calls the fill_args hook before it calls CGI::Ex::Fill::fill. Order matters, bestie. πΈ
file_print (hook) π¦
Returns a filename of the content that the default print hook is gonna use.
Here's the recipe it follows: it adds the method base_dir_rel to the hook name_module, and to name_step, and then sticks on the default file extension it finds in $self->ext_print β which defaults to the property $self->{ext_print}, which defaults to ".html".
What it should return is a filename relative to template_path that can be swapped using Template::Alloy. Or! It can return a scalar reference to the template content that can be swapped. Either works. This is what the print hook ends up using.
sub template_path { '/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. That one's lowkey a lifesaver for prototyping applications and/or keeping all of the data for the application in a single location. Single file app? Periodt. π₯
file_val (hook) π
Returns a filename containing the validation. Does the same dance as file_print, BUT:
- it uses
ext_valto get the extension, and - it adds
vob_path(which defaults totemplate_path, which defaults tobase_dir_abs) onto the returned value.
Why the difference, you ask? Because file_print is relative to template_path, while file_val is fully qualified with vob_path. If vob_path returns an arrayref of paths, then each path gets checked for the existence of the file. One by one, no cap.
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. Also β this method can also return a hashref containing the validation... but if that's the route you're going, you probably wanted to override the hash_validation hook instead. Just saying. π
finalize (hook) π
Defaults to true. This is where you 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 where the core logic of a script lives β think adding to a database, or updating a record. At this point, the data should be validated already.
You can do additional validation and return errors from here, using code like this:
if (! $user_is_unique) {
$self->add_errors(username => 'The username was already used');
return 0;
}
find_hook (method) π
Called by run_hook. Arguments are a hook name and a step name. It should return an arrayref containing the code_ref to run, and the name of the method that was looked for. It uses ->can to find the appropriate hook.
my $code = $self->find_hook('finalize', 'main');
### will look first for $self->main_finalize;
### will then look for $self->finalize;
Why does this system exist? So that multiple steps can chill in the same file, and you still have the option of moving some steps out to external sub classed packages if you want to. Best of both worlds, fr fr.
If the application has successfully morphed via the morph method and allow_morph, then you do not need to add the step name to the beginning of the method name, because the morphed packages method will override the base package. (It's still OK to use the full method name "${step}_hookname" though β no one's stopping you. π)
See therun_hookmethod and themorphmethod for more details.
first_step (method) β
Returns the first step of the path.
Watch out though β first_step may not be the same thing as default_step if the path was overridden. Sneaky little difference, but important. π
forbidden_step (method) π«
Defaults to "__forbidden". This is 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 for the full picture.
form (method) π
Returns a hashref of the items passed to the CGI. Specifically it returns $self->{form}, which defaults to CGI::Ex::get_form. Easy peasy. β¨
form_name (hook) π
Return the name of the form to attach the js validation to. Used by js_validation. That's the whole assignment, bestie.
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 theget_pass_by_usermethod ofCGI::Ex::Authfor more information.
It gets installed as a hook to the authentication object during the get_valid_auth method. Very behind-the-scenes, very iconic.
get_valid_auth (method) π§
If the require_auth hook returns true on any given step, then get_valid_auth will be called. Simple as that.
Here's what it does:
- It calls
auth_argsto get some default args to pass toCGI::Ex::Auth->new. - It augments those args with sensible defaults that App already provides β like form, cookies, and template facilities.
- It also installs hooks for the
get_pass_by_user,cleanup_user, andverify_userhooks ofCGI::Ex::Auth.
It stores $auth->last_auth_data in $self->auth_data for later use. So like, 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 => '<h1>My login header</h1>',
login_footer => '[% TRY %][% INCLUDE login/login_footer.htm %][% CATCH %]<!-- [% error %] -->[% 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. Efficiency queen. π
Full customization of the login process and the login template can be done via the auth_args hash.
See theauth_argsmethod andCGI::Ex::Authperldoc for more information.
goto_step (method) πΈ
Okay so this method is not normally used, but it can solve some difficult issues, and when you need it you need it. It provides for moving to another step at any point during the nav_loop.
Once a goto_step has been called, the entire nav_loop will be exited. (If you just wanna replace a portion of a step, you can simply run run_hook('run_step', 'other_step') instead.) So goto_step 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). Watch out. π
Normally you would allow the other hooks in the loop to carry on their normal functions and avoid goto_step. Essentially, this hook behaves like a goto method β bypassing everything else and continuing at a different location in the path. There are times when it is necessary or useful to do this. You'll know when you know. iykyk.
The method jump is an alias for this method.
Goto_step 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 goto_step).
- A value of
0would repeat the current step (watch out for recursion π). - A value of
-1would 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 *.)
If a step name is not currently on the path, it will be replace any remaining steps of the path.
# goto previous step (repeat it)
$self->goto_step($self->previous_step);
$self->goto_step('PREVIOUS');
$self->goto_step(-1);
# goto next step
$self->goto_step($self->next_step);
$self->goto_step('NEXT');
$self->goto_step(1);
$self->goto_step;
# goto current step (repeat)
$self->goto_step($self->current_step);
$self->goto_step('CURRENT');
$self->goto_step(0);
# goto last step
$self->goto_step($self->last_step);
$self->goto_step('LAST');
# goto first step (repeat it)
$self->goto_step($self->first_step);
$self->goto_step('FIRST');
handle_error (method) π©Ή
If anything dies during execution, handle_error will be called with the error that had happened. Default action is to try running the step returned by the error_step method. Catching strays, one error at a time. π
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. Glow up. π
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) },
};
}
πΈ hash_common (hook)
okay so this one is like, basically hash_base's twin sister π
almost identical in function and purpose. The vibe is: hash_base is for the common stuff you use across various scripts that inherit from a shared CGI::Ex::App-type parent. hash_common is more the step-level queen β she's for populating both swap and fill per step. two different jobs, both iconic β¨
π hash_errors (hook)
Called in preparation for print after a failed prepare, info_complete, or finalize. She should hold a hash of any errors that happened. She gets merged into hash_form before the pass to print. Each error that occurred gets handed off to the method format_error before it's added to the hash. And if an error has occurred, the default validate will automatically add <code>{has_errors =>1}</code> to the error hash at the time of validation. has_errors also gets added during the merge, just in case the default validate wasn't used. You can populate her by passing a hash to ->add_to_errors or ->add_errors. π
π¦ hash_fill (hook)
Called in preparation for print after a failed prepare, info_complete, or finalize. Holds a hash of any items that need to be filled into the html form during print. Items from hash_form, hash_base, and hash_common get layered together here. Populate her by passing a hash to ->add_to_fill.
By default β forms are sticky and data from previous requests will try and populate the form. If you want to turn that off you can use the fill_template hook to disable templating on a single page or on all pages.
This method can also be used to pre-populate the form (like on an edit step, bestie π). But heads up: if a form fails validation, hash_fill will also be called and will only want the submitted form fields to be sticky. So to prevent pre-population in those cases, use the ready_validate hook like this:
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;
}
π hash_form (hook)
Called in preparation for print after a failed prepare, info_complete, or finalize. Defaults to ->form. Populate her by passing a hash to ->add_to_form. πΈ
β hash_swap (hook)
Called in preparation for print after a failed prepare, info_complete, or finalize. Holds a hash of any items that need to be swapped into the html during print. She gets merged with hash_base, hash_common, hash_form, and hash_errors. Populate her by passing a hash to ->add_to_swap.
The hash gets passed as the second argument to swap_template. iykyk π
π₯ hash_validation (hook)
Returns a hash of the validation info to check the form against. By default she'll look for a filename using the hook file_val and pass it to CGI::Ex::Validate::get_validation. If no file_val comes back, or if the get_validation fails, an empty hash gets returned. Validation is implemented by ->vob, which loads a CGI::Ex::Validate object. periodt β¨
π§ history (method)
Returns an arrayref holding the trace history of which hooks of which steps got ran. Super useful for seeing what actually happened. In general, each line of the history shows 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 way easier to see what path was followed. π€οΈ
And in general? The arrayref is free real estate β anything can push onto it, which helps track other occurrences in the program too. no cap.
π info_complete (hook)
Calls the ready_validate hook to see if the data is ready to validate. If it is, she calls the validate hook to validate the data. Basically her whole job is making sure the data is ready and valid. She will not be run unless prepare returns true (that's the default). π«Ά
π init (method)
Called by the default new method. Lets any object initializations happen that might need to happen. Default action doesβ¦ nothing. literally nothing. iconic behavior honestly π
π init_from_conf (method)
Called by the default new method. If load_conf is true, then the conf method gets called and the keys it returns get added to the $self object.
This method runs after the init method. And if you need to further fix up values added during init_from_conf, you can use the pre_navigate method. π
π insert_path (method)
Arguments are the steps to insert. Can be called any time. Inserts the new steps at the current path location. β¨
πΈ is_authed (method)
Returns true if the object has successful authentication data. Returns false if the object has not been authenticated. that's it, that's the method π¦
β js_uri_path (method)
Returns the URI path where the CGI/Ex/yaml_load.js and CGI/Ex/validate.js files can be found. This defaults to <code>"$ENV{SCRIPT_NAME}/js"</code> if the path method has not been overridden. Otherwise it defaults to <code>"$ENV{SCRIPT_NAME}?step=js&js="</code> β and ngl that latter one 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). And js_run_step will work properly with the default path handler. π
π¦ js_validation (hook)
Will return Javascript that is capable of validating the form. This is done using the capabilities of CGI::Ex::Validate and CGI::Ex::JSONDump. She'll call the hook hash_validation, which then gets encoded into json and placed in a javascript string. She'll also call the hook form_name to figure out 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.
And in order to actually make use of js_validation, it must be added to the variables returned by either the hash_base (default), hash_common, hash_swap, or hash_form hook (see examples of hash_base used in this doc). don't forget it bestie π
π₯ jump (method)
Alias for the goto_step method. that's the whole thing β¨
π last_step (method)
Returns the last step of the path. π
π load_conf (method)
Defaults to ->{load_conf}, which defaults to false. If true, it will allow keys returned by the conf method to be added to $self during the init_from_conf method.
Enabling this method allows for out-of-the-box file-based configuration. slay. π
π§ morph (method)
Allows for temporarily "becoming" another object type for the execution of the current step. This lets you separate some steps out into their own packages. main character energy tbh.
morph will only run if the method allow_morph returns true. And additionally β if 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. 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 back into the original package. π«Ά
Samples of allowing morph:
sub allow_morph { 1 } # value of 1 means try to find package, ok if not found
sub allow_morph { {edit => 1} }
sub allow_morph { my ($self, $step) = @_; return $step eq 'edit' }
β morph_package (hook)
Used by morph. Returns the package name to morph into during a morph call. Defaults to using the current object type as a base. So like, 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 run_hook works, it's possible that several steps could live in the same external file, and overriding morph_package could allow for that to happen. π
See the morph method.
π name_module (hook)
Returns 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 exists 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 info.
See the section on FINDING TEMPLATES for further discussion.
π name_step (hook)
Returns the step (appended to name_module) that should be used when looking up the file in file_print and file_val lookups. Defaults to the current step.
See the section on FINDING TEMPLATES for further discussion. πΈ
π¦ nav_loop (method)
This is the main loop runner. She figures out the current path and runs all 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's a method ->recurse_limit (default 15) that will catch logic errors β like inadvertently running the same step over and over and over because there's either no hash_validation, or the data is valid but the set_ready_validate(0) method was not called. π ask me how I know.
π 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 basically a glow-up safety wrapper around ->nav_loop π
bestie. It catches any dies and hands them over to ->handle_error so nothing explodes in your face π.
This is the thing that kicks off the whole process flow for the path and all its steps β¨.
navigate_authenticated (method)
Lowkey identical to navigate, except it calls ->require_auth(1) before running. It only actually works if the navigate_authenticated method hasn't been overwritten β so heads up, don't clobber it π. Go read the require_auth method for the tea π.
new (class method)
The object creator, the origin story, the main character birth moment π. It takes a hashref of arguments that become the initial properties of the object. Once the object is blessed, it calls the init method so you can do any other initilizations you need β¨.
my $app = MyApp->new({name_module => 'my_app'});
next_step (hook and method)
As a method it returns the next step in the path β but only if the path has more steps left πΈ.
It's also used as a hook by the refine_path hook. If there are no more steps, it calls the next_step hook to try and find a step to append to the path π«Ά.
path (method)
Returns an arrayref (modifiable, yes you can touch it) of the steps in the path. For each step, the run_step hook and all of its remaining hooks get run π«.
Hook methods are looked up and run using the method run_hook, which uses the method find_hook to look up the hook. A whole history of ran hooks is stored in the array ref returned by $self->history π.
If path hasn't been defined, the method looks first in the form for a key by the name found in ->step_key. Then it looks in $ENV{'PATH_INFO'}. It uses this step to create a path with that one step as its contents. If a step gets passed in via either of those ways, the method calls valid_steps to make sure the step is valid (by default valid_steps returns undef β which means literally any step is valid, slay). Any step beginning with _ can not be passed in β those are intended for use on private paths π. If a non-valid step is found, then path gets 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 makes sure other methods can add to the path (and it will most likely break if the arrayref is not the same). Periodt π
.
If navigation runs out of steps to run, the default step found in default_step gets run. This is what allows us to default to the "main" step for many applications π .
path_info_map (hook)
Used to map path_info parts to form variables. Similar to the path_info_map_base method. Go see the path_info_map_base method for a whole discussion on how to use this hook π.
path_info_map_base (method)
Called during the default path method. It's used to custom map portions of $ENV{'PATH_INFO'} to form values β¨. It should return an arrayref of arrayrefs, where each child arrayref contains a regex qr with match parens as the first element of the array. Subsequent elements of the array are the key names to store the corresponding matched value from the regex under π. The outer arrayref gets iterated until one of the child arrayrefs matches against $ENV{'PATH_INFO'}. The matched values are only added to the form if there is not already a defined value for that key in the form π.
The default value returned by this method looks something like this:
sub path_info_map_base {
return [[qr{^/(\w+)}, $self->step_key]];
}
This example would map the following PATH_INFO string like so:
/my_step
# $self->form->{'step'} now equals "my_step"
Here's another example, because we love options π:
sub path_info_map_base {
return [
[qr{^/([^/]+)/(\w+)}, 'username', $self->step_key],
[qr{^/(\w+)}, $self->step_key],
];
}
# the PATH_INFO /my_step
# still results in
# $self->form->{'step'} now equals "my_step"
# but with the PATH_INFO /my_user/my_step
# $self->form->{'step'} now equals "my_step"
# and $self->form->{'username'} equals "my_user"
In most cases there is no need to override the path_info_map_base method β instead override the path_info_map hook for a particular step π¦. When the step is being run, just before the run_step hook is called, the path_info_map hook is called. The path_info_map hook is similar to the path_info_map_base method, but it's used to allow step level manipulation of form based on elements in the $ENV{'PATH_INFO'} π.
sub my_step_path_info_map {
return [[qr{^/my_step/(\w+)$}, 'username']];
}
# the PATH_INFO /my_step/my_user
# results in
# $self->form->{'step'} equal to "my_step" because of default path_info_map_base
# and $self->form->{'username'} equals "my_user" because of my_step_path_info_map
The section on mapping URIs to steps has additional examples if you want more π.
post_loop (method)
Ran after all of the steps in the loop have been processed (that's only if prepare, info_complete, and finalize were true for each of the steps) π«. If it returns a true value, the navigation loop gets aborted π. If it does not return true, navigation continues by then inserting the step $self->default_step and running $self->nav_loop again (it recurses) to fall back to the default step π.
post_navigate (method)
Called from within navigate. Called after the nav_loop has finished running, but within the eval block to catch errors π§. It only runs 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, then none of the pre_navigate, nav_loop, or post_navigate methods will be called π«.
post_print (hook)
A hook which occurs after the printing has taken place π¨οΈ. It's only run if the information was not complete. Useful for cases such as printing rows of a database query after displaying a query form ποΈ.
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 π.
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 it could be modified π). The only argument is a reference to the path array. If it returns a true value β the navigation routine is aborted π.
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) π.
pre_step (hook)
Ran at the beginning of the loop before prepare, info_complete, and finalize are called πΈ. If it returns true, execution of nav_loop is returned and no more steps are processed π.
prepare (hook)
Defaults to true β
. A hook before checking if the info_complete is true. Intended to be used to cleanup the form data π§Ή.
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 π.
previous_step (method)
Lists the step previous to this one βͺ. Will return '' if there is no previous step (rip) π« .
print (hook)
Take the information generated by prepared_print, format it using swap_template, fill it using fill_template and print it out using print_out π¨οΈβ¨. The default incarnation uses Template::Alloy which is compatible with Template::Toolkit to do the swapping π. 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 π.
print_out (hook)
Called with the finished document π. Should print out the appropriate headers. The default method calls $self->cgix->print_content_type and then prints the content β¨.
The print_content_type is passed $self->mimetype (which defaults to $self->{'mimetype'} which defaults to 'text/html') and $self->charset (which defaults to $self->{'charset'} which defaults to '') π.
ready_validate (hook)
Should return true if enough information is present to run validate β
. The 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 β something like this:
sub ready_validate { shift->form->{'processing'} }
Changing the behavior of ready_validate can help in making wizard type applications π§β¨.
You can also use the validate_when_data hook to change the behavior of ready_validate. If validate_when_data returns true, then ready_validate will look for keys in the form matching keys that are in hash_validation β if they exist ready_validate will be true π. If there are no hash_validation keys, ready_validate uses its default behavior π.
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 π₯. No cap.
recurse_limit (method)
Default 15. Maximum number of times to allow nav_loop to call itself π. The recurse level will increase every time that ->goto_step 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 π.
Okay so real talk: if you're hitting ->goto_step a whole bunch, your recurse_limit is gonna get reached way faster. It's totally safe to raise it as high as you need β just make sure it's intentional, bestie. π
And usually the limit gets reached because a step didn't have a validation hash, or because the set_ready_validate(0) method never got called after the data was successfully validated and acted upon. That's the tea. β
replace_path (method)
Args are the steps used to replace. Can be called literally any time. It replaces the remaining steps (if any) of the current path. β¨
require_auth (hook)
Defaults to self->{require_auth}, which defaults to undef. If you call it as a method and pass it a single value of 1, 0, or undef, it will set the value of $self->{require_auth} to that value. If it's set to a true value, then every subsequent step will require authentication β unless that step's hook has been overwritten. π
Any of the following ways can be used to require authentication on every step:
# Example one
sub require_auth { 1 }
# Example two
__PACKAGE__->navigate_authenticated; # instead of __PACKAGE__->navigate;
# Example three
__PACKAGE__->new({require_auth => 1}->navigate;
# Example four
sub init { shift->require_auth(1) }
Since it gets called as a hook, the current step is passed as the first argument. If the hook returns false, no authentication will be required on this step. If the hook returns a true, non-hashref value, authentication will be required via the get_valid_auth method. If the method returns a hashref of stepnames to require authentication on, then the step will require authentication via the get_valid_auth method only if the current step is in that hashref. If authentication is required and succeeds, the step will proceed. If authentication is required and fails at the step level, the current step will be aborted, authentication will be asked for (and the post_navigate method will still be called). π
So for example, you could add authentication to the add, edit, and delete steps in any of these ways:
# Example one
sub require_auth { {add => 1, edit => 1, delete => 1} }
# Example two
sub add_require_auth { 1 }
sub edit_require_auth { 1 }
sub delete_require_auth { 1 }
# Example three
sub require_auth {
my ($self, $step) = @_;
return 1 if $step && $step =~ /^(add|edit|delete)$/;
return 0;
}
If however you wanted to require authentication on all but one or two methods (like requiring authentication on everything except a forgot_password step), you could do either of these:
# Example one
sub require_auth {
my ($self, $step) = @_;
return 0 if $step && $step eq 'forgot_password';
return 1; # require auth on all other steps
}
# Example two
sub require_auth { 1 } # turn it on for all steps
sub forgot_password_require_auth { 0 } # turn it off
Go peep the get_valid_auth method for what happens should authentication be required. π
There is one key difference from the 2.14 version of App. In 2.14 and previous versions, the pre_navigate and post_navigate methods would not be called if require_auth returned a true non-hashref value. In version 2.15 and later, the 2.15 pre_navigate and post_navigate methods are always called β even if authentication fails. Also in 2.15 and later, the method is called as a hook, meaning the step is passed in. π«Ά
run_hook (method)
Args are a hook name and the step to find the hook for. It calls the find_hook method to get a code ref, which it then calls and returns the result of, passing any extra arguments to run_hook as arguments to the code ref. π₯
Every single call to run_hook gets logged in the arrayref returned by the history method. That info is summarized in the dump_history method and it is genuinely 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's 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. Iconic behaviour tbh. π
run_hook_as (method)
Similar to run_hook β but it allows for temporarily running a hook in another package. π«
sub blah_morph_package { 'SomeOther::Module' }
my $hash = $self->run_hook_as('hash_swap', 'blah'); # runs as SomeOther::Module
# OR
my $hash = $self->run_hook_as('hash_swap', 'step', 'SomeOther::Module');
Heads up though: the second form will use 'SomeOther::Module' as the step name, which will be somewhat misleading when it goes looking up names. π¬
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, go see the section on process flow). It's 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's ->run_modes. The default run_step method provides so much more granular control over the flow of the CGI. Slay.
set_path (method)
Args 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. (It's giving "you probably don't need me" energy, fr.) π
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. Has no affect if the validate_when_data flag is set. π
The following would complement the "processing" flag example given in the ready_validate description:
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. That's so 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);
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 (aka the current step is skipped). Bye bestie πβ¨
stash (method)
Returns a hashref that can store arbitrary user space data without worrying about overwriting the internals of the application. It's your lil safe space. π«Ά
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'. π
swap_template (hook)
Takes the template and hash of variables prepared in print, and processes them through the current template engine Template::Alloy. π§
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 template_path (which will be used to initialize INCLUDE_PATH by default).
The default method will create a template object by calling the template_args hook and passing the returned hashref to the template_obj method. The default template_obj method returns a Template::Alloy object, but could easily be swapped to use a Template::Toolkit based object. If a non-Template::Toolkit compatible object is to be used, then the swap_template hook can be overridden to use another templating engine. π¨
For example, to use the HTML::Template engine you could override the swap_template method as follows:
use HTML::Template;
sub swap_template {
my ($self, $step, $file, $swap) = @_;
my $type = UNIVERSAL::isa($file, 'SCALAR') ? 'scalarref'
: UNIVERSAL::isa($file, 'ARRAY') ? 'arrayref'
: ref($file) ? 'filehandle'
: 'filename';
my $t = HTML::Template->new(source => $file,
type => $type,
path => $self->template_path,
die_on_bad_params => 0,
);
$t->param($swap);
return $t->output;
}
Uou β and yes, that's literally how it's spelled in the source, we respect the typo era β could also simply do the following to parse the templates using HTML::Template::Expr syntax.
sub template_args {
return {SYNTAX => 'hte'};
}
For a listing of the available syntaxes, see the current Template::Alloy documentation. π
template_args (hook)
Returns a hashref of args that will be passed to the "new" method of Template::Alloy. The method is normally called from the swap_template hook. The swap_template hook will add a value for INCLUDE_PATH which is set equal to template_path, if the INCLUDE_PATH value is not already set. β¨
The returned hashref can contain any arguments that Template::Alloy would understand.
sub template_args {
return {
PRE_CHOMP => 1,
WRAPPER => 'wrappers/main_wrapper.html',
};
}
See the Template::Alloy documentation for a listing of all possible configuration arguments. π
template_obj (method)
Called from swap_template. It is passed the result of template_args that have had a default INCLUDE_PATH added via template_path. The default implementation uses Template::Alloy but can easily be changed to use Template::Toolkit by using code similar to the following:
use Template;
sub template_obj {
my ($self, $args) = @_;
return Template->new($args);
}
template_path(method) β defaults to$self->{'template_path'}, which itself defaults tobase_dir_abs. Yeah. It's the thingtemplate_objpeeks at when it's doing its lil thing. πunmorph(method) β okay so this one's the glow-down π . If themorphmethod successfully morphed the App object into something else, this lets you put it back into its previous blessed state. Heads up though: it only happens if the object was previously morphed into another object type in the first place β otherwise there's nothing to un-morph, bestie. And before the object gets re-blessed, the methodfixup_before_unmorphgets called. Sneaky lil pre-step. β¨
See also allow_morph and morph.
valid_steps(method) β called by the defaultpathmethod, and it should hand back a hashref of the path steps that are actually allowed. Here's the tea: if the current step isn't found in that hash (or it's not thedefault_steporjs_step), thenpathwill return a single step of->forbidden_stepand run its hooks instead. If you return no hash, orundef? Then all paths are allowed β that's the default, no gatekeeping. πͺ When a step is rejected, a key"forbidden_step"gets dropped into the stash holding the step that wasn't valid. Ngl, you often don't even need to definevalid_stepsat all, because arbitrary method calls just aren't possible with CGI::Ex::App β the framework already has your back.
Also also: any steps that begin with _ are not valid for passing in via the form or path info either. See the path method for that whole situation.
And one more thing β the pre_step, skip, prepare, and info_complete hooks all let you validate the data before running finalize. So you got options. π
validate(hook) β you get passed the form from$self->form, and it runs validation on the info inside it. The default validation uses CGI::Ex::Validate. It calls thehash_validationhook to load up a validation hashref (an empty hash means "pass validation", easy). It should return true if the form passed and false if it didn't. Errors get stored as a hash in$self->{hash_errors}via theadd_errorsmethod, and you can check them later withhas_errors(that's if you used the default validate, obvi).
There are so many ways and types to validate the data β go read the CGI::Ex::Validate module, it's giving very thorough.
Once it succeeds, it'll look through all the items that were validated, and if any of them contain the keys append_path, insert_path, or replace_path, that method gets called with the value as its arguments. Which means validation can literally apply redirection to the path. Iconic behaviour fr fr. So a validation item like:
{field => 'foo', required => 1, append_path => ['bar', 'baz']}
would append 'bar' and 'baz' to the path if all validation succeeds. Slay. β¨
validate_when_data(hook) β defaults to the"validate_when_data"property, which defaults to false. It's called during theready_validatehook. If it returns true,ready_validatewill go looking for keys in the form that match keys inhash_validationβ and if those exist,ready_validatewill be true. And if there are nohash_validationkeys at all? Thenready_validatejust does its default thing. Simple as. πΈverify_user(method) β installed as a hook to CGI::Ex::App duringget_valid_auth. Should return true if the user is ok. Default is to always return true (everyone's welcome π«Ά). This one lets you bail early, before theget_pass_by_userhook even gets called.
sub verify_user {
my ($self, $user) = @_;
return 0 if $user eq 'paul'; # don't let paul in
return 1; # let anybody else in
}
πͺ HOW DO I SET COOKIES, REDIRECT, ETC
Bestie, this is the part you came for. Often in your program you're gonna wanna set cookies or bounce to a differnt URL. You can do that with either the builtin CGI::Ex object or the builtin CGI object. But real talk β it is suggested that you only use the CGI::Ex methods, because that bad boy will automatically send headers and method calls under cgi, mod_perl1, or mod_perl2. All the vibes, none of the compatibility panic. π
The following shows how to do the basic items using the CGI::Ex object returned by the ->cgix method.
printing content-type headers
### CGI::Ex::App prints headers for you,
### but if you are printing custom types, you can send your own
$self->cgix->print_content_type;
# SAME AS
# $self->cgix->print_content_type('text/html');
setting a cookie
$self->cgix->set_cookie({
-name => "my_key",
-value => 'Some Value',
-expires => '1y',
-path => '/',
});
redirecting to another URL
$self->cgix->location_bounce("http://somewhereelse.com");
$self->exit_nav_loop; # normally should do this to long jump out of navigation
making a QUERY_STRING
my $data = {foo => "bar", one => "two or three"};
my $query = $self->cgix->make_form($data);
# $query now equals "foo=bar&one=two%20or%20three"
getting form parameters
my $form = $self->form;
In this example $form would now contain a hashref of all POST and GET parameters passed to the server. The form method calls $self->cgix->get_form which in turn uses CGI->param to parse values. And fields with multiple passed values? Those show up in the form of an arrayref. π¦
getting cookies
my $cookies = $self->cookies;
In this example $cookies would be a hashref of all the cookies that got passed in. The cookies method calls $self->cgix->get_cookies, which in turn uses CGI->cookie to parse values. πͺβ¨
See the CGI::Ex and CGI documentation for more information.
π COMPARISON TO OTHER APPLICATION MODULES
Okay so the concepts used in CGI::Ex::App are not novel or unique. Not claiming to have invented the wheel here. However β they are all commonly used and very useful, and every single application builder out there was built because somebody observed that there are common design patterns in CGI building. CGI::Ex::App differs in that it found more of those common design patterns than the other application builders, and it tries to get in the way less than the others. Humble brag, but it's true. π
CGI::Ex::App is intended to be sub classed, and sub sub classed, and each step can choose to be sub classed or not. It tries to stay simple while still providing "more than one way to do it." And it also tries to avoid making any sub classes have to call ->SUPER:: (although that's fine too, if that's your thing).
And if what you're doing on a particular [step] is far too complicated or custom for what CGI::Ex::App provides? It makes it trivial to override all behaviour. No fighting the framework, bestie. π¦
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.
"CGI::Application"
Seemingly the most well know of application builders. CGI::Ex::App is different in that it:
- Uses Template::Toolkit compatible Template::Alloy by default.
- CGI::Ex::App can easily use another toolkit by simply overriding the
->swap_templatemethod. - CGI::Application uses HTML::Template.
- CGI::Ex::App can easily use another toolkit by simply overriding the
- 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->runmodewhich is only a dispatch. - Support for easily jumping around in navigation steps.
- Support for storing some steps in another package.
- Integrated authentication
- Integrated form filling
- Integrated PATH_INFO mapping
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.
"CGI::Prototype"
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 Template::Alloy which is TT compatible). CGI::Ex::App is differrent in that it:
- 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.
- Integrated data validation
- Integrated authentication
- Integrated form filling
- Integrated PATH_INFO mapping
π§ SIMPLE EXTENDED EXAMPLE
Okay bestie, here's the moment. 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 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? And 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 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. That's the whole vibe. π
### 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' }
...and honestly? this next chunk is the rest of that recipe app, the part where it actually does stuff. buckle up bestie π
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__
β¨ and now the templates, because the app is nothing without its fits β¨
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, bestie (read them, they're lowkey important)
The dbh method hands back an SQLite dbh handle and auto-creates the schema for you. Fr fr though β in real life you're gonna want MySQL or Oracle, or Postgres, and you're gonna 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. But the messy, complex cases that need to pull in two or three or four tables are probably better off with the hand crafted SQL. iykyk.
This sample uses SQL. You could write the application to use literally whatever storage you want β or even to do nothing with the submitted data. no cap.
We had to write our own HTML (Catalyst and Ruby on Rails do this for you π«Ά). For most development work, the HTML should live in a static location so designers can actually work on it. It's nice that the other frameworks give you stub html β but that's all it is. It's worth about as much as copying and pasting the above examples. All worthwhile HTML goes through a non-automated design/finalization process. periodt.
The add step used the same template as the edit step. We pulled that off with the add_name_step hook returning "edit". The template has IF conditions so it can show different info depending on whether we're in add mode or edit mode. π¦
We reused code, validation, and templates. Code and data reuse is a good thing. slay.
edit_hash_common returns an empty hashref if the form was ready to validate. When hash_common gets called and the form is ready to validate, that means the form failed validation and is now printing out the page. So 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 get used for both template swapping and filling. We could have used hash_swap and hash_fill independently instead. options, bestie π
The hook main_info_complete is hard coded to 0. Which basically says we will never try and validate or finalize the main step β which is most often the case. β
π SEPARATING STEPS INTO SEPARATE FILES π
okay bestie, so sometimes you're gonna want to break some β or literally all β of your steps into their own lil separate files. That's lowkey how CGI::Prototype does it, and it's giving very useful when a step and its hooks are massive, or when they're barely ever used. So like, let them live in their own apartment, y'know? π
Here's the vibe: take that previous "recipe db" example and move the "delete" step into its own file. You can do the same trick to break any other step out too.
### 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, bestie, because there's always notes π:
- The hooks that get called β
skip,info_complete, andfinalizeβ don't have to be prefixed with the step name anymore, because they're chillin' in their own individual package space now. Iconic. But ngl, you could still name themdelete_skip,delete_info_complete, anddelete_finalize, and therun_hookmethod will still find them. That's actually the move if you want several steps with the same"morph_package"to all be stored in the same external module. - The method
allow_morphgets passed the step we're trying to morph to. Ifallow_morphreturns true every single time, then it'll try torequirethe extra packages every time that step is ran. You could limit the morphing to only run on certain steps with code like this:
sub allow_morph { return {delete => 1} }
# OR
sub allow_morph {
my ($self, $step) = @_;
return ($step eq 'delete') ? 1 : 0;
}
Also, CGI::Ex::App temporarily blesses the object into the "morph_package" for the duration of the step, and then re-blesses it back into the original package when it exits. See the morph method and allow_morph for more deets. β¨
π§ RUNNING UNDER MOD_PERL π§
The previous samples? Yeah, they're basically ready to run under flat CGI, Fast CGI, or mod_perl Registry / mod_perl PerlRun type environments. But it is very easy to move the previous example into being a true mod_perl handler. So slay. π
To convert the previous recipe example, just add this:
### 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, again π«Ά:
- Both the
/cgi-bin/recipeversion and the/recipeversion can totally co-exist. One of them will be a normal cgi, and the other will correctly use mod_perl hooks for headers. Best of both worlds, no cap. - Setting the location to
/recipemeans that$ENV{SCRIPT_NAME}will also be set to/recipe. That meansname_modulewill resolve to"recipe". But if you want a different URI location, like"/my_cool_recipe", while still using the same template content (in the/var/www/templates/content/recipedirectory), then you'd need to explicitly set the"name_module"parameter. Either of these ways works:
### 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 you don't need to set name_module, but it also doesn't hurt, and honestly it's more descriptive for whoever's gonna maintain the code later. Future-you says thank you. πΈ
π ADDING AUTHENTICATION TO THE ENTIRE APPLICATION π
Having authentication is sometimes a good thing β a whole vibe, even. To force the entire application to be authenticated (like, require a valid username and password before doing anything), you could do this:
### 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 info. And also peep the CGI::Ex::Auth perldoc. π
π ADDING AUTHENTICATION TO INDIVIDUAL STEPS π
Sometimes you only want certain steps to require authentication. For example, in the previous recipe example, maybe you want to let the main and view steps be accessible to absolutely anybody, but require auth for the add, edit, and delete steps. Main character steps only, yk? π
To do that, we'd change the original example like so β and heads up, the navigation has to start with ->navigate. Starting with ->navigate_authenticated will cause all steps to require validation, so don't do that here:
### 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, like this:
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 sneak 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 }
And that's it, bestie. The add, edit, and delete steps will now require authentication. See the require_auth, get_valid_auth, and auth_args methods for more info. Also see the CGI::Ex::Auth perldoc. β¨
π THANKS π
The following corporation and individuals contributed in some part to the original versions. Shoutout to the OGs π«Ά:
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.
π LICENSE π
This module may be distributed under the same terms as Perl itself.
π AUTHOR π
Paul Seamons <perl at seamons dot com>
And that's a wrap on the whole page, babe ππ
This bad boy is from perl v5.42.0 and was dated 2025-07-25 β that's the source/version and date down in the footer, for the record. Go forth and build something iconic with CGI::Ex::App. periodt. πβ¨
π¦ where this page comes from
- fedora 43: perl-CGI-Ex 2.55-6.fc43 Β· GPL-1.0-or-later OR Artistic-1.0-Perl
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms