We've got an initial version which hasn't completed development, but should actually be functional to a large extent. Comments, etc., welcome.
The basic idea that we started with was that we were dealing with two separate objects/classes - a Page and a Collection.
The Page contains all of the knowledge about how the inputs should be processed, which page should be "next" and how the output should be generated.
Meanwhile, the Collection contains multiple Pages and provides the command and control logic on top of the Pages.
A sample snippet (very sample) would be:
use Framework;
use CGI;
my $coll = new Framework::Collection;
$coll->templater(new TemplatingEngine);
my $p1 = new Framework::Page("login",
-description => 'Login page');
$p1->validator(sub {
my $page = shift;
my $pobj = $page->paramobj;
return 1 if ($pobj->param('name') eq 'bmcatt');
$page->add_error('name', 'invalid name given');
return 0;
} );
$p1->traverse(sub {
my $page = shift;
return 'login' if ($page->errors(':any'));
return 'main';
}
$p1->template('login.tmpl');
my $p2 = new Framework::Page("main",
-description => 'Main Page'
-hparams => [ 'name' ]);
$p2->output( sub {
my ($page, $paramref, $output) = @_;
my $s = "Content-type: text/html\n\n";
$s .= "<html><body>You have logged in as " .
$paramref->{'name'} . "</body></html>";
return $s;
}
$coll->page($p1);
$coll->page($p2);
$coll->defpage($p1);
$coll->run(new CGI);
print $coll->output;
While this is a contrived example, it should (hopefully) show some of the underlying power of the module.
The parts that we feel still definitely need to be implemented are:
- Better error handling
- Better parameter passing (that doesn't always go through the paramobj (now a CGI))
- Creation of a derived Page that provides prepackaged validation routines
- Creation of a sample template wrapper class. Currently, we're based on TT2, but would like to be able to support HTML::Template (and others?) through a wrapper on the templating engine instance.
- Probably other things that we haven't thought of yet.
As mentioned above, we'd definitely appreciate thoughts, comments, suggestions on improvements, etc.
#! perl -w
package Framework;
# XXX Add small working example to POD
=pod
=head1 NAME
Framework - Perl module to provide command and control functionality
over templatized web pages
=head1 SYNOPSIS
use CGI;
use Framework;
$p = new Framework::Page("name", -description => "Initial page",
-template => "name.tmpl");
$p->traverse(\&traverse_name);
$c = new Framework::Collection;
$c->page($p);
$c->templater($templating_engine_instance);
$c->run(new CGI);
print $c->output;
=head1 DESCRIPTION
Framework provides a way to maintain, display and arbitrate the
selection of templatized web pages. It does this through the paradigm
of individual I<Page>s which are bundled together into a
I<Collection>.
The Collection controls selection of which Page is to be executed
(based on parameter analysis). The selected Page controls parameter
validation and selection of the next Page to be processed. Once a
target page has been determiend, that Page is processed for its
template and is run through the templating engine.
=cut
use strict;
use vars qw($VERSION);
use constant DEBUG => 1;
$VERSION = '0.02';
my $DEFAULT_ERROR_SEP = '|';
package Framework::Collection;
=pod
=head2 Framework::Collection
=over 4
=cut
sub debugprint {
my $level = shift;
print @_ if (Framework::DEBUG >= $level);
}
my $output;
=pod
=item new()
Create a new Collection. Usually called as:
my $coll = new Framework::Collection();
=cut
sub new {
my $this = shift; my $class = ref($this) || $this;
my $self = {
_PAGES => {},
_DEFAULT_PAGE => undef,
};
bless $self, $class;
}
=pod
=item page($page)
=item page($name)
When called with a Framework::Page, adds C<$page> to the list of pages
to be considered for processing.
When called with a string C<$name>, returns the Framework::Page with
the name C<$name> or C<undef>.
The first C<$page> added to a Collection is the default page unless
overriden through a call to C<defpage()>.
=cut
sub page {
my $self = shift;
my ($page) = @_;
if ($page->isa("Framework::Page")) {
# Add to _PAGES{}
$self->{_PAGES}->{$page->{_NAME}} = $page;
$self->defpage($page) unless ($self->defpage());
return $page; # Just to have something useful returned
} else {
# $page is a page *NAME*, not an actual page...
# Return _PAGES{$page};
if (exists $self->{_PAGES}->{$page}) {
return $self->{_PAGES}->{$page};
}
return undef;
}
}
=pod
=item defpage($page)
=item defpage($name)
Sets the default page to be processed, either by name or by
Framework::Page object. The default page is the page of I<last resort>
and is used if the Collection cannot determine the calling page. This
is typically used during the first invocation of the application.
=cut
sub defpage {
my $self = shift;
my ($page) = @_;
if (!$page) { return $self->{_DEFAULT_PAGE}; }
$self->{_DEFAULT_PAGE} = ( $page->isa('Framework::Page')
? $page->{_NAME}
: $page );
}
=pod
=item templater(I<$templating_instance>)
Assigns / returns the instance of the templating object which is to be
used for processing the template of the destination page. If called
without arguments, the current value is returned. The
C<$templating_instance> will eventually be called as:
&$templating_instance($template, \%params);
=cut
sub templater {
my $self = shift;
if (@_) { $self->{_TEMPLATER} = shift; }
return $self->{_TEMPLATER};
}
# We need this here because both run and output() need to have access
# to it...
my ($targpage_id, $targpage);
=pod
=item run($paramobj)
Uses C<$paramobj> (usually an instance of a CGI object) to process the
Collection and determine the correct target Page. At a minimum,
C<$paramobj> must support a scalar and list context param() for
retrieval and assignment and a hidden() method. The processing flow
during a run is:
=over 4
=item 1
Determine the current Page name
=item 2
Invoke current page's validator subref (if defined)
=item 3
If no validator or the validator returned C<TRUE>, invoke current
page's postsubmit subref (if defined)
=item 4
Invoke current page's traverse subref (if defined). The traverse
subref is expected to return the name of the target page. If there is
no traverse subref or it returns C<FALSE>, the current page is used as
the target page.
=item 5
Invoke target page's preprocess subref (if defined).
=back
The return value from the target page's preprocess subref will be used
as the return value from C<run()>.
=cut
sub run {
my $self = shift;
my ($pobj) = @_;
# Determine originating page
my $curpage_id = $pobj->param("_PAGE_CURRENT")
|| $self->{_DEFAULT_PAGE};
debugprint 1, "Starting at page $curpage_id\n";
# Get Framework::Page for current page
my $curpage = $self->page($curpage_id);
# Clear out any old cruft
$curpage->_phase(':runconfig');
$curpage->paramobj($pobj);
# Run $curpage->validator->()
$curpage->_phase(':valid');
my $val_return = 1;
if ($curpage->validator) {
$val_return = &{$curpage->validator()}($curpage);
}
# Run $curpage->postsub->() if validator->() returned TRUE
$curpage->_phase(':postsub');
if ($val_return && $curpage->postsubmit) {
&{$curpage->postsubmit()}($curpage);
}
# Run $curpage->traverse->()
$curpage->_phase(':traverse');
$targpage_id = $curpage_id;
if ($curpage->traverse) {
$targpage_id = &{$curpage->traverse()}($curpage);
if (!$targpage_id) { $targpage_id = $curpage_id; }
}
# Save error list and retract parameter object from old page
my $errlist = $curpage->errors(':all');
$curpage->_phase(':undef');
# Get Framework::Page for target page
debugprint 1, "Transitioning to $targpage_id\n";
$targpage = $self->page($targpage_id);
$targpage->paramobj($pobj);
# Transfer error set from original to target
$targpage->_phase(':seterrors', $errlist);
# Clear out the output
$output = '';
# Run $targpage->preprocess->()
$targpage->_phase(':preproc');
if ($targpage->preprocess) {
$output = &{$targpage->preprocess()}($targpage);
}
# Return output from preprocess->()
return $output;
}
sub _get_params {
my $p = shift;
my $pobj = $p->paramobj;
my %paramhash;
if ($p->aparams) {
foreach ($p->aparams) {
my @param = $pobj->param($_);
my $pval = ( @param == 1 ? $param[0] : \@param );
$paramhash{$_} = $pval;
}
}
my $hidden;
if ($p->hparams) {
foreach ($p->hparams) {
my @param = $pobj->param($_);
$hidden .= $pobj->hidden($_, @param);
}
}
$hidden .= $pobj->hidden('_PAGE_CURRENT', $p->{_NAME});
$paramhash{_PAGE_HIDDEN} = $hidden;
$paramhash{_PAGE_DESC} = $p->{_DESC};
my $errs = $p->errors;
foreach (keys %$errs) {
$paramhash{'_ERROR_' . $_} = $errs->{$_};
}
# XXX Add other parameters (errors, etc).
return \%paramhash;
}
=pod
=item output()
Invokes the templating engine on the template for the target page.
Also invokes the target page's output subref (if defined). The
templating engine is only invoked if both it and the target page's
template are defined.
The return value is the output of the templating engine, possibly
after it has been subsequently passed through the target page's output
subref.
=cut
sub output {
my $self = shift;
my $params = _get_params($targpage);
if ($targpage->template && $self->{_TEMPLATER}) {
$output = $self->templater->($targpage->template, $params);
}
if ($targpage->output) {
$output = $targpage->output->($targpage, $params, $output);
}
# Retract parameter object from targpage
$targpage->_phase(':undef');
return $output;
}
=pod
=back
=cut
package Framework::Page;
=pod
=head2 Framework::Page
=over 4
=cut
sub debugprint {
my $level = shift;
print @_ if (Framework::DEBUG >= $level);
}
# Externally visible methods that are intended to be called by those w
+anting
# to create/modify a page.
my %PAGE_KEY_NAMES = (
-description => '_DESC',
-validator => '_VALID',
-postsubmit => '_POST',
-traverse => '_TRAV',
-preprocess => '_PREPROC',
-template => '_TMPL',
-output => '_OUTPUT',
-hparams => '_HPARAMS',
-aparams => '_APARAMS',
-error_sep => '_ERROR_SEP',
);
my @ERROR_CLASSES = qw(:valid :postsub :traverse :preproc);
my %PAGE_CTOR_DEFAULTS = (
_DESC => "",
_VALID => undef,
_POST => undef,
_TRAV => undef,
_PREPROC => undef,
_TMPL => undef,
_OUTPUT => undef,
_HPARAMS => [],
_APARAMS => [],
_ERROR_SEP => $DEFAULT_ERROR_SEP,
_PHASE => ':valid',
);
sub _check_params {
my %params = @_;
my %outparams;
foreach my $pname (keys %params) {
# XXX Better error handling - bad parameter specified.
die "A horrible death" if (!exists($PAGE_KEY_NAMES{$pname}));
$outparams{$PAGE_KEY_NAMES{$pname}} = $params{$pname};
}
return %outparams;
}
=pod
=item new($name, ...)
=item $page->new($newname, ...)
Creates a new Page instance using C<$name> as the page's name. When
called as a method on an existing Page, the new Page will be
configured exactly the same as the existing page, except for having a
new name.
Optional parameters may be included to "short-circuit" the process of
creating a new Page. An example of creating a Page with all
short-circuit parameters used is:
$p = new Framework::Page('TestPage',
-description => "This is a test page",
-validator => \&valid_sub,
-postsubmit => \&postsubmit_sub,
-traverse => \&traverse_sub,
-preprocess => \&preprocess_sub,
-template => "TestPage.tmpl",
-output => \&output_sub,
-hparams => [ 'hidden1', 'hidden2' ],
-aparams => [ 'avail1', 'avail2' ],
-error_sep => '<br>'
);
=cut
sub new {
my $this = shift; my $class = ref($this) || $this;
my $name = shift;
# XXX Better error handling - force a name to be given.
die "You silly rabbit" if (!defined($name));
my %params = _check_params(@_);
if (ref($this) && $this->isa('Framework::Page')) {
# called as a copy constructor...
my $self = {
%$this,
%params,
};
bless $self, $class;
$self->_phase(':runconfig');
return $self;
}
my $self = {
_NAME => $name,
%PAGE_CTOR_DEFAULTS,
%params,
};
bless $self, $class;
$self->_phase(':runconfig');
return $self
}
=pod
=item description($desc)
Assigns / retrieves a textual description for this Page. This is not
used internally, but is passed to the templating engine as the
parameter C<_PAGE_DESC>.
=cut
sub description ($$) {
my $self = shift;
if (@_) { $self->{_DESC} = shift; }
return $self->{_DESC};
}
=pod
=item validator(\&validator)
Assigns / retrieves the subref for the validation routine to be used
to determine whether the parameters for this page were valid. Called
as:
&validator($curpage);
If C<&validator> returns C<TRUE>, it indicates that the subsequent
postsubmit subref is to be called. Otherwise, the postsubmit is
skipped and the traverse subref is called.
B<Note>: It was the authors' intention that &validator not mutate any
of the back-end data or system state. Any changes to the system state
were intended to be done in the postsubmit routine following
successful validation.
=cut
sub validator ($\&) {
my $self = shift;
if (@_) { $self->{_VALID} = shift; }
return $self->{_VALID};
}
=pod
=item postsubmit(\&postsubmit)
Assigns / retrieves the subref for the postsubmit routine to be used
to perform post-validation processing. Called as:
&postsubmit($curpage);
The return value from C<&postsubmit> is ignored by the Collection.
=cut
sub postsubmit ($\&) {
my $self = shift;
if (@_) { $self->{_POST} = shift; }
return $self->{_POST};
}
=pod
=item traverse(\&traverse)
Assigns / retrieves the subref for the traversal routine to be used to
determine what the appropriate target page is. Called as:
&traverse($curpage);
The return value from C<&traverse> should be the name of the target
page to be processed or C<FALSE> if the current page should be used as
the target page.
=cut
sub traverse ($\&) {
my $self = shift;
if (@_) { $self->{_TRAV} = shift; }
return $self->{_TRAV};
}
=pod
=item preprocess(\&preprocess)
Assigns / retrieves the subref for the preprocessing routine to be
used as the last stage of a Collection's C<run()>. Called as:
&preprocess($targetpage);
The return value from C<&preprocess> is used as the return value from
Collection's C<run()>.
=cut
sub preprocess ($\&) {
my $self = shift;
if (@_) { $self->{_PREPROC} = shift; }
return $self->{_PREPROC};
}
=pod
=item template($template_filename)
Assigns / retrieves the name of the file to be passed to the
templating engine when the Collection's C<output()> is called.
=cut
sub template ($$) {
my $self = shift;
if (@_) { $self->{_TMPL} = shift; }
return $self->{_TMPL};
}
=pod
=item output(\&output)
Assigns / retrieves the subref for the output generation or
postprocessing. Called as:
&output($targetpage, \%params, $output);
C<$output> is either the output of the templating engine (if it is
defined and a template exists for this page) or the output of the
preprocessing step.
If this subref is provided, the return value will be used as the
return value from the Collection's C<output()>, otherwise the return
value from the templating engine will be used.
=cut
sub output ($\&) {
my $self = shift;
if (@_) { $self->{_OUTPUT} = shift; }
return $self->{_OUTPUT};
}
=pod
=item hparams(I<\@hidden_params>)
Assigns / retrieves an arrayref containing the list of parameters
which are to be turned into "hidden" fields for the templating or
output processing. All of the hidden fields will be provided as a
single parameter named C<_PAGE_HIDDEN>.
In addition to the user-specified hidden parameters, the Collection
will also be providing at least one other hidden parameter. At a
minimum, the Collection will be providing C<_PAGE_CURRENT> which is
used for page determination at the beginning of the C<run()>.
If called in scalar context, C<hparams()> returns the arrayref. If
called in list context, C<hparams()> will dereference the array ref
and return the values as a list.
=cut
sub hparams {
my $self = shift;
if (@_) { $self->{_HPARAMS} = \@_; }
my $paramref = $self->{_HPARAMS};
return ( wantarray ? @$paramref : $paramref );
}
=pod
=item aparams(I<\@available_params>)
Assigns / retrieves an arrayref containing a list of parameters to be
"made available" to the templating engine and output processing. Any
parameters to be mmade available are copied verbatim into the
parameter hashref which is passed to the templating engine and the
page's output subref.
If called in scalar context, C<aparams()> returns the arrayref. If
called in list context, C<aparams()> will dereference the array ref
and return the values as a list.
=cut
sub aparams {
my $self = shift;
if (@_) { $self->{_APARAMS} = \@_; }
my $paramref = $self->{_APARAMS};
return ( wantarray ? @$paramref : $paramref );
}
# Externally visible methods that are intended to be called by the
# subrefs which are called through the Collection we belong to.
=pod
=item add_error($name, $message)
B<Subref call> - intended to be called only from the subrefs when
Collection invokes them.
Add an error message to the phase-specific set of error messages,
against the parameter C<$name>. Note that C<$name> does not have to
actually refer to a real parameter name. However C<$name> will be used
when the error messages are transfered to the templating engine or
output subref in the parameter hash. Error messages for C<$name> will
be passed in as the parameter C<_ERROR_$name>.
Each processing step (:valid, :postsub, :traverse, :preproc) has its
own set of error messages which are individually filled. Multiple
errors for an individual C<$name> during the same processing step will
be separated by the I<error separator>.
=cut
sub add_error {
my $self = shift;
my ($param, $message) = @_;
my $errlist = $self->{_ERROR_LIST}->{$self->{_PHASE}};
if (exists $errlist->{$param}) {
$errlist->{$param} .= $self->{_ERROR_SEP};
}
$errlist->{$param} .= $message;
}
=pod
=item error_sep($separator)
Assigns / retrieves the string to be used between multiple error
messages for the same parameter name.
Note that C<error_sep()> may be set separately for the current and
target pages and this may cause strange behaviour.
=cut
sub error_sep {
my $self = shift;
if (@_) { $self->{_ERROR_SEP} = shift; }
return $self->{_ERROR_SEP};
}
=pod
=item errors(':any')
=item errors(':any:valid') [or :any:postsub, :any:traverse, :any:prepr
+oc]
=item errors(':all')
=item errors(':valid') [or :postsub, :traverse, :preproc]
=item errors()
B<Subref call> - intended to be called only from the subrefs when
Collection invokes them.
Retrieves the list of errors (or presence of errors) for different
phases and in different forms.
C<errors(':any')> (and the similar :any:I<type> form) return a boolean
TRUE / FALSE indicating whether there are any errors present for all
processing phases or for the particular (specified) type.
C<errors(':all')> returns a hashref where the keys are the types
(':valid', etc) and the values are hashrefs. The contained hashrefs
are set up as C<$name =E<gt> $message(s)>.
C<errors(':valid')> (and the similar forms) return a hashref of
hashref where the single key in the containing hashref is the key that
was passed in.
C<errors()> returns a flattened single-level hash version of
C<errors(':all')> where the top level of keys is removed. All entries
are set up as C<$name =E<gt> $message(s)>. Multiple messages for the
same C<$name> will be combined as for C<add_error()>.
=cut
sub errors {
my $self = shift;
my ($type) = @_;
# Handle undef :type
if (!$type) {
my %errors;
foreach (@ERROR_CLASSES) {
my $sublist = $self->{_ERROR_LIST}->{$_};
foreach (keys %$sublist) {
if (exists($errors{$_})) {
$errors{$_} .= $self->{_ERROR_SEP};
}
$errors{$_} .= $sublist->{$_};
}
}
return \%errors;
}
# $type eq ':any' or in (:any:valid, :any:postsub, etc.)
if ($type =~ /^:any/) {
$type =~ s/^:any//;
if (!$type) {
# Simple :any check
foreach (@ERROR_CLASSES) {
return 1 if %{$self->{_ERROR_LIST}->{$_}}
};
return 0;
}
if (grep {/^$type$/} @ERROR_CLASSES) {
return (%{$self->{_ERROR_LIST}->{$type}} && 1);
}
die "Foolish mortal... :any with invalid type $type";
}
# :type eq ':all'
if ($type eq ':all') {
return $self->{_ERROR_LIST};
}
# :type in (:valid, :postsub, :traverse, :preproc)
if (grep {/^$type$/} @ERROR_CLASSES) {
my %out = ( $type => \%{$self->{_ERROR_LIST}->{$type}} );
return \%out;
}
# XXX Better error handling...
die "A pox on your errors call ($type)" if ($type);
}
#sub param ($$) {
# my $self = shift;
# my ($paramname) = @_;
#
# # XXX Implement the internal parameter passing.
#}
=pod
=item paramobj()
B<Subref call> - intended to be called only from the subrefs when
Collection invokes them.
Retrieves the parameter object which was originally passed in to the
Collection's C<run()>. This is intended to allow a subref access to
the parameters from the parameter object.
=cut
sub paramobj {
my $self = shift;
if (@_) { $self->{_PARAMOBJ} = shift; }
return $self->{_PARAMOBJ};
}
# Internally called routines. Don't look down here unless you really w
+ant to
# see some of the innards of Framework... Really... I warned you...
sub _phase {
my $self = shift;
my $phase = shift;
debugprint 2, "Page ", $self->{_NAME}, "->_phase change to $phase\
+n";
if ($phase eq ':undef') {
$self->{_PARAMOBJ} = undef;
$phase = ':runconfig'; # Force clear errors
}
if ($phase eq ':runconfig') {
$self->{_ERROR_LIST} = { map { $_ => {} } @ERROR_CLASSES };
}
if ($phase eq ':seterrors') {
$self->{_ERROR_LIST} = $_[0];
}
$self->{_PHASE} = $phase;
}
=pod
=back
=cut
1;
__END__
# Below is the stub of documentation for your module. You better edit
+it!
=pod
=cut