The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

App::devguide - App Developer's Guide

INTRODUCTION

This is the Developer's Guide to the App (Perl 5 Enterprise Environment). You can find out more background to the project on the web.

  http://www.officevision.com/pub/p5ee
  http://p5ee.perl.org

App DESIGN PHILOSOPHY

When the App project was begun, there were already

 * many outstanding Perl packages on CPAN
 * an excellent systems architecture for Perl webapps (mod_perl)
 * several excellent web application development frameworks

So why was App needed?

For a variety of reasons, there was never sufficient unity of purpose or direction within the Perl community to provide a coherent blueprint of what Enterprise Programming in Perl is or how to do it.

 * What are the pieces?
 * What pieces are default? standard? mandatory? optional?
 * How do they fit together?
 * How can I override or customize them?
 * What techniques do I need to use to assemble them effectively?

After "Enterprise Systems" were defined (see website), the field of existing frameworks, solutions, and components was surveyed. The goal was to examine everything that people were already doing, divide it into pieces that seemed interchangeable, and come up with a unified blueprint. Pieces that people often like to do differently (template systems, persistence frameworks, configuration files) were allowed to vary, while their essential contribution to the working system was standardized in the framework.

It should be noted that most of the work on what people might term "Enterprise Systems" in Perl was actually focused on "Web Systems" in Perl with a relational database.

The goal of the App design is to unify the Perl community on a framework for cooperation, while providing flexibility in the areas that might otherwise divide the community.

Essentially, everything in App is overridable and customizable, but good defaults are provided for everything as well.

On the practical side, App was also designed to allow for gradual adoption and incorporation into existing projects.

App Execution Flow: CGI

The first step to understanding the flow of execution through the App framework is to understand the flow in the CGI Context.

This is one of the most challenging contexts to develop for because of the stateless nature of HTTP, the need to initialize all resources before accessing them, and the need to properly shut down all resources after using them or in case of user abort.

cgi-bin/app and the Initialization Config File

All usage of App from the web can be driven through the CGI program, "cgi-bin/app". (Actually, depending on the settings in the Initialization Config File, the "app" program might not be a CGI program at all.) The app program should be installed at a location which is executable as a CGI program such as the following.

  http://www.officevision.com/cgi-bin/pub/p5ee/app

The "app" progam runs with the "-wT" switches turned on for maximum safety, security, and enforcement of programming rigor.

Then a BEGIN block is executed to read the Initialization Config File to get low level config settings and perhaps modify the @INC variable. Modification of the @INC variable in the BEGIN block through configuration is critical so that you can have multiple versions of modules installed (at various stages of development through testing, production, and support). and access the correct ones.

The Initialization Config File (.conf) is located in the following way. First, the PATH_INFO is checked and a corresponding .conf file is opened. Thus, the following URL

  http://www.officevision.com/cgi-bin/pub/p5ee/app/ecommerce/shop

would open "ecommerce_shop.conf" in the directory of the "app" program.

If it is not found, "$0.conf" is opened. That would be "app.conf" in this case. However, you can see that this allows for the "app" program to be renamed, and (according to its configuration) it will behave like a completely different application.

If it is still not found, "app.conf" is opened. If this is not found, no Initialization Config information will be used and all defaults will be used.

Whichever .conf file was first opened, it is read for simple configuration variables of the form "variable = value". Anything following a "#" is considered a comment. Leading and trailing spaces are removed, and blank lines are ignored. Spaces may precede or follow the "=" sign without affecting the "variable" or the "value". The "variable" must be a sequence of alphabetic characters and ".", "_", or "-" (i.e. matches /[a-zA-Z_.-]+/). The value may be any string of characters (including none), but leading and trailing spaces are stripped. The variable/value pairs are saved in a global hash in %main::conf.

If the "perlinc" variable is set, it is understood to be a comma-separated list of directories to search for Perl modules. This list is placed at the beginning of the special Perl @INC variable.

Some sample lines in the .conf file are:

  perlinc = /usr/ov/acoc/dev/src/Appx-Blue, /usr/ov/acoc/dev/lib/perl5/5.6.0
  debugmode = record
  showsession = 1
  gzip = 1
  configFile = app.pl

The meanings of these variables are:

  perlinc      - directories to search for perl modules
  debugmode    - (off|record|replay) useful for recording a failed CGI request and replaying it for debugging
  debug        - (0|1,App::Context::CGI|6,App::Repository::DBI.select_rows)
  showsession  - (0|1) show the contents of the session in an HTML comment
  gzip         - (0|1) allows compression of HTML output if the client browser supports it

Additional variables may be provided. However, suitable defaults are usually detected if they are not supplied.

  contextClass - (i.e. App::Context::CGI) class for the context
  configClass  - (i.e. App::Config::File) class for the config
  configFile   - (i.e. "config.pl") name of the main configuration file
  configSerializerClass - (i.e. App::Serializer::Dumper) class for config deserialization
  sessionClass - (i.e. App::Session::HTMLHidden) class for the session
  defaultWname - widget name to be the first current_widget to be displayed
  scriptUrlDir - URL of App script directory (i.e. "/cgi-bin")
  scriptDir    - Directory corresponding to the scriptUrlDir
  htmlUrlDir   - URL of App docs directory
  htmlDir      - Directory corresponding to the htmlUrlDir

After the Initialization Config File is read into %main::conf, the command line arguments are scanned for options of the form "-variable" or "-variable=value". (Options may also start with double-dashes, "--".) Such options are removed from the command line, and the variable/value pair is added to the %main::conf hash, thus overriding any values from the Initialization Config File. The CGI environment never passes options to the program in this way (with a preceding dash), so this is mainly useful for debugging at the command line (i.e. "app -debugmode=replay -debug=1 -gzip=0").

use App

Next, the App module is included, which does the following:

  * disable "Use of uninitialized value" warnings
  * use Exception::Class;   # enable Exception inheritance
  * use App::Exceptions;  # define App exceptions

The base class of all App exceptions is "App::Exception". Derived from this base exception, each component service of App has its own base class as follows.

  App::Exception::Context
  App::Exception::Config
  App::Exception::Serializer
  App::Exception::Repository
  App::Exception::Security
  App::Exception::Session
  App::Exception::Widget
  App::Exception::TemplateEngine
  App::Exception::Procedure
  App::Exception::Messaging
  App::Exception::LogChannel

cgi-bin/app and bootstrapping the environment

The "app" program then executes the following line.

  my $context = App->context(\%main::conf);

This instantiates a $context object, passing the global %main::conf hash as an argument.

Note that in other Context implementations other than CGI, the $context may survive to serve more than a single request or to dispatch many events coming from the network or from a user. In that case, the App->context() method may return an already-instantiated $context object. The $context is a singleton per process.

App->context()

If the "contextClass" variable is in the argument hash, it is used. Otherwise, if the "app" program is running in the CGI context (HTTP_USER_AGENT environment variable is set) or at the command line, the App::Context::CGI class will be assumed.

The code for the selected class is loaded and a $context of the appropriate class is instantiated. For the CGI context, the App::Context::CGI->new() method is called, and the arguments of the context() method call (%main::conf, in this case) are passed on to it.

App::Context::CGI->new()

The constructor (new()) for App::Context::CGI is actually provided by its parent class, App::Context.

If the "configClass" was not specified in the arguments (%main::conf, in this case), App::Config::File is assumed. It is instantiated, once again passing on the hash of initialization args, and the result is stored in $self->{config};

Then the App::Context::CGI->init() method is called to complete the $context constructor. A CGI object is created and added to the %args to be passed on to Session instantiation.

If the "sessionClass" was not specified in the arguments, App::Session::HTMLHidden is assumed. It is instantiated, once again passing on the hash of initialization args, and the result is stored in $self->{session};

App::Config::File->new()

The constructor (new()) for App::Config::File is actually provided by its grand-parent class, App::Reference. It creates a reference by calling App::Config::File->create(), blesses it into the class, calls init() (App::Reference->init()) which does nothing, and then returns the constructed Config::File object.

The Config::File->create() method loads data from a configuration file and returns the reference to a hash. But first it has to find the file and deserialize it.

If the "configFile" was not specified in the arguments (%main::conf, in this case), the following files are searched for in order. (If the script were renamed to "foo", it would look for "foo" variants of the files instead of "app" variants of the files.)

   1. app.pl           2. config.pl
   3. app.xml          4. config.xml
   5. app.ini          6. config.ini
   7. app.properties   8. config.properties
   9. app.perl        10. config.perl
  11. app.conf        12. config.conf

By convention, "config.pl" is the config file for App CGI scripts. However, the first file that is found is assumed to be the relevant config file. If no config file is found or if it cannot be opened, an exception is thrown. Otherwise, the text is read in from the file and deserialized into a hash reference.

If the "configSerializerClass" was not specified in the arguments (%main::conf, in this case), the file suffix for the config file is used to determine the Serializer class to use for deserialization.

  pl          # use "eval" instead of a serializer
  perl        # App::Serializer::Dumper (like "pl" but use a serializer)
  xml         # App::Serializer::XMLSimple
  ini         # App::Serializer::Ini
  properties  # App::Serializer::Properties
  conf        # App::Serializer::Properties
  stor        # App::Serializer::Storable

If the file is a .pl file, no serializer is implied. It is just eval'ed. (It must have "$var =" as the first non-whitespace text in the file, where "var" is any variable name.)

If a Serializer is specified or implied, a serializer is instantiated and the config file data is deserialized into a hash reference.

The resulting hash reference is returned and stored in $context->{config}.

App::Context::CGI->init()

The init() method is where the CGI object is created, parsing the environment variables and STDIN which are part of the CGI runtime environment.

For debugging purposes, a "debugmode" variable is checked in the %args (i.e. %main::conf) to see if special processing with the CGI object should be performed. If "debugmode" is "record", the CGI objects and the %ENV hash will be saved to files ("debug.vars" and "debug.env", respectively). If "debugmode" is "replay", the current %ENV and CGI will be cleared and loaded from the files from a previously recorded request.

Another sort of debugging is initialized if the "debug" variable is supplied in the %args. This turns on a global debug flag ($App::DEBUG) and sets the debug scope (which classes or methods should produce debug output).

To support migration of code, the CGI object can also be passed into the App->context() method as an argument, and it will be used rather than trying to create a new one. However, this is not the execution path being described here.

App::Session::HTMLHidden->new()

The constructor (new()) for App::Session::HTMLHidden is actually provided by its grand-parent class, App::Reference. It creates a hash reference by calling App::Reference->create(), blesses it into the class, calls init() (App::Session::HTMLHidden->init()), and then returns the constructed Session::HTMLHidden object.

The init() method looks at the CGI variables in the request and restores the session state information from the variable named "app.sessiondata" (and "app.sessiondata[2..n]"). When the values of these variables are concatenated, they form a Base64-encoded, gzipped, frozen multi-level hash of session state data. To retrieve the state data, the text is therefore decoded, gunzipped, and thawed (a la Storable). This state is stored in $session->{state} and the session cache is initialized to an empty hashref.

cgi-bin/app and dispatching events

The "app" program finally executes the following line.

  $context->dispatch_events();

This does everything necessary to dispatch events which are implied in the HTTP request, loading whatever data is needed, modifying it, and saving it again to await the next request.

Please note that other Context implementations (i.e. Context::Modperl) use the same API, but they may already have database connections initialized, data already loaded, etc. However, the basic CGI Context (Context::CGI) described here must initialize everything at the outset and shut it all down at the completion of each request.

App::Context::CGI->dispatch_events()

The CGI variables are examined. Variables which start with "app.event" are identified as events to be handled, and they are saved for later. These are called "event variables".

All other variables are understood to be widget attributes and they are saved to their respective widgets.

Variables with any of the "{}[]" "indexing" characters (such as "table_editor{data}[1][5]") are called "indexed variables". The value is saved to the "table_editor" widget, which evidently has an attribute called "data" which is a two dimensional array.

Variables without the indexing characters but with at least one dot (".") in them are "dotted variables" of the form "widgetname.attributename". (This is a synonym for "widgetname{attributename}", but it is handled more efficiently.) Widget names may include dots, but attribute names may not. Thus, the last dot separates the widget name from the attribute name. So "app.toolbar.savebutton.width" is the "width" attribute on the "app.toolbar.savebutton" widget.

Variables without indexing characters or dots are "plain variables". If the special variable "wname" was also supplied, all plain variables are understood to be attributes of the $wname widget. Otherwise, all plain variables are stored in the "session" widget.

After all variables are stored in the Session, events are handled. There are two kinds of events, "user events" (such as come from <input type=submit> and <input type=image> tags) and "callback events" (such as come from <input type=hidden> tags).

User events have a name that looks like one of the following.

  app.event.widgetname.eventname
  app.event.widgetname.eventname(arg1)
  app.event.widgetname.eventname(arg1,arg2,...)

These events are parsed, the appropriate widget is summoned, and the widget is instructed to handle the event (i.e. $w->handle_event()).

Callback events have a name that looks simply like "app.event". The "widgetname.eventname(args)" is in the value of the variable.

After all events have been handled, the context calls $self->display_current_widget() and then calls $self->shutdown() which gracefully shuts down all connections to repositories.

App::Context::CGI->display_current_widget()

The display_current_widget() method is implemented in the parent class, App::Context::HTML.

A attribute "session.current_widget" contains the name of the current widget to display.

If this is not set, check the CGI variable "wname" (and set session.current_widget if found). If this is not set, check the Initialization Config (i.e. the copy of %main::conf, stored in the $context) for a variable named "defaultWname". If this is not set, use the $PATH_INFO, with internal "/'s" converted to dots (i.e. "/app/selector" => "app.selector"). Otherwise, use "default" as the current widget (and set session.current_widget).

Then the current widget is summoned and handed to the display_items() method.

App::Context::CGI->display_items()

The job of display items is to take the list of args, convert them to HTML, and print them to STDOUT with the appropriate HTTP headers and wrapper HTML. Also, depending on the %main::conf and the browser capabilities, the content may be compressed (gzipped).

If the first argument to display_items() is a widget (which it is in this flow of execution), the widget's attributes are checked to see if any of the following are specified:

  title
  bgcolor
  text
  link
  vlink
  alink
  leftmargin
  topmargin
  rightmargin
  bottommargin
  class

Any of those attributes which are set in the widget will be propagated into the appropriate places in the <head> and <body> tags of the HTML which wraps the widget HTML.

That's it. All of the processing for a single CGI request is complete. All application code is wrapped up in user interface widgets (App::Widget::HTML) and entity widgets (App::Widget::Entity). The user interface widgets are mostly prepackaged, but are configured through the configuration file, allowing for a data-driven programming style on the user interface. The entity widgets store most of their state in Repositories. They also get configured through the config file, but they are frequently subclassed to add additional functionality.