NAME¶
Magpie - Pipelined State Machine Plack Middleware Framework
VERSION¶
version 1.141660
SYNOPSIS¶
-----
# static.psgi
use Plack::Builder
use Plack::Middleware::Magpie;
# A static pipeline, will always load the same components
# for every request,
my $app = builder {
enable "Magpie", context => {}, pipeline => [
# main application logic
'MyApp::Core',
# transform the result using Template Toolkit
'Magpie::Transformer::TT2' => { template_root => '/my/template/dir' }
];
};
-----
# dynamic.psgi
use Plack::Builder
use Plack::Middleware::Magpie;
# use the machine and match, and match_env sugar to load components
# conditionally
my $app = builder {
enable "Magpie", context => {}, pipeline => [
machine {
# prepend an input transformer for POST and PUT requests
match_env { REQUEST_METHOD => qr/POST|PUT/ } => ['MyApp::InputHandler'];
# matches every request, generates core content
match qr|^/| => ['MyApp::XMLGenerator];
# apply different XSLT stylesheets based on the request path.
match qr|^/blog/| => [ 'Magpie::Transformer::XSLT'
=> { stylesheet => '/style/blog.xsl'}];
match qr|^/shop/| => [ 'Magpie::Transformer::XSLT'
=> { stylesheet => '/style/cart.xsl'}];
}
];
};
How Does Magpie Work?¶
A typical Magpie application has three parts:
- 1.
- One or more Application Classes that contain the event handler methods
that are fired during the application's run.
- 2.
- One or more Output Class that controls how to serialize the application
state into data that the requesting client can consume.
- 3.
- An interface script/module that constructs the Magpie::Machine application
pipeline and connects it to the wider world.
External Interfaces¶
First, the
interface component:
# sample.pm -- A Typical Magpie application as a modperl2 handler
package sample::handler;
use Magpie::Machine;
use Apache::Response;
use Apache::Const;
sub handler {
my $r = shift;
# load the Application and Output classes into
# the Magpie pipeline
my $app = Magpie::Machine->new();
$app->pipeline( qw(
My::Greetings
Magpie::Output::Scalar
) );
# can be any type of Perl reference or object
my $application_context = {};
# execute the application pipeline
return $app->run( $application_context );
}
1;
Like all ModPerl content handlers, this simple module implements a
"handler()" method that is called whenever the URI associated with
this module is requested. Here, that method creates a new Magpie application
by calling the "new()" constructor of the
"Magpie::Machine" class-- all Magpie applications are an instance of
that class.
By itself, this instance of the Machine class does nothing useful; we must load
the Application Class (or classes) that will perform required operations and
the Output Class that will generate the appropriate response. We do this by
calling the Machine instance's "pipeline()" method and passing in a
list containing a mix of either the
Perl package names of the classes
we want to load or
blessed instances of those classes. In the
example above, we loaded two classes into the Machine's application pipeline:
"My::Greetings" and "Magpie::Output::Scalar" by simply
passing in the class names.
Finally, we set the application in motion by calling the Machine instance's
"run()" method. This method takes a single argument that may contain
any sort of Perl reference (hash reference, array reference, blessed object).
and, whatever data structure or object that is passed as that argument is made
available to all methods in the Application and Output classes that are called
as the application runs (more about how this works in the next section). When
the "run()" method is called, each class that was passed in via the
"pipeline()" method is loaded in the order they we passed and all
event handler methods implemented in those classes that match the
current application state are called. When the event handlers from one
application class are completed the Machine loads the next class and calls its
methods-- and so on, until the last method in the Output class is called, at
which point the response has been sent and the application pipeline
terminates.
Application Classes¶
In Magpie, application classes are implemented as subclasses of an event model.
This underlying Event class is responsible for registering event handlers with
Magpie's internal queue and for determining which of those registered handlers
will be fired in response to the current state. In short, the Event class
determines which state the application is in, and which of the registered
event handler methods will be fired in response to that state. Usually, the
details of mapping states to events are never visible to the developer beyond
the initial choice of which Event model to use as a base class (different
Event classes use different conditions to determine application state). In
daily practice, you simply implement and register the events that you
application needs and let Magpie do the rest.
# Greetings.pm -- A Magpie Application class that greets the
# user based on application state
package MyApp::Pipeline::Greetings;
use Moose;
# inherit from the base Component class.
extends 'Magpie::Component';
# determines the application state based on the value
# of a specific form/query param ala CGI::Application.
with 'Magpie::Dispatcher::RequestParam' => { state_param => 'appstate' };
# Import handler
use Magpie::Constants;
# register the event handlers for this class
__PACKAGE__->register_events( qw( morning afternoon evening default) );
# implement the event handlers
sub morning {
my $self = shift;
my $ctxt = shift;
$ctxt->{message} = 'Good morning!';
return OK;
}
sub afternoon {
my $self = shift;
my $ctxt = shift;
$ctxt->{message} = 'Good afternoon!';
return OK;
}
sub evening {
my $self = shift;
my $ctxt = shift;
$ctxt->{message} = 'Good evening!';
return OK;
}
# will be called if no matching state is found
sub default {
my $self = shift;
my $ctxt = shift;
$ctxt->{message} = "I'm not sure what time of day it is!";
return OK;
}
1;
First, notice that the application class is a subclass of the
"Magpie::Component". Through this interface (and the roles it
consumes), you get access to the core attributes and methods of the Magpie
application framework (see the section titled 'know thy $self' below). In
"Magpie::Event::Simple" you register a list of state events via the
required
registerEvents() function. The event model then determines
which event handler method to fire by examining the value of a specific
querystring or POSTed form parameter. (The default param is named
"appstate" but that can be overridden by implementing the
"state_param()" method and returning some other value). If the
underlying Event model finds a registered event whose name matches the value
returned by the "state_param()" method, the event handler method
named "event_
<eventname"> is called. So, for example,
a request to the URI that exposes the class above like the following:
http://example.org/apps/greet?appstate=morning
would cause the "event_morning()" method to be called. If no matching
state is found, the "event_default()" event handler method is called
as a fallback. In addition, most Magpie Event model classes also implement an
"event_init()" handler and an "event_exit()" handler.
These methods are optional and, if implemented, "event_init()" will
be called after the event queue is initialized but before the first
state-determined event handler is fired while "event_exit()" is
called just before the application exits.
Note that "Magpie::Event::Simple" (that determines application state
based on a single form param) is only one possible event model and you are
free to choose another and/or write your own-- even mixing application classes
based on different models within the same application pipeline. This
flexibility is one of Magpie's key strengths.
Event Handler Methods¶
Every Magpie Application class will implement one or more
event handler
methods that will be called conditionally based on the state that is
determined by the underlying event model. It is within these methods that the
real business of the application takes place. In the above example, these
methods merely set a key in the application-wide $ctxt hash reference, but
there is no limit to what you can do. Remember, part of the point of Magpie is
to separate state detection from application code-- this is achieved by having
the event model parent class determine the state then call the event handler
methods that implement the code that should be run for that state.
Each event handler method is passed two arguments: the $self class instance
member, and a special
application context member (named $ctxt in the
examples above).
Know Thy $self
In Magpie Application classes the $self class instance member offers access to a
handful of common attributes and methods:
- "$self->request"
- This offers acccess to "Plack::Request" object representing the
current client request.
Example:
if ( $self->request->method eq 'POST' ) {
...
}
Keeping Things In $ctxt
The second argument passed to each event handler method is the
context
member (named $ctxt in these examples). The context member-- which can
be any kind of Perl object or reference to another data structure-- is passed
as the sole argument to the application pipeline's
run() method.
# in your interface module/script:
my $handler = builder {
enable "Magpie", context => $app_context, pipeline => [
...
];
};
...
# then later, in the event handler methods in your application classes
sub myevent {
my $self = shift;
my $ctxt = shift; # same object/data as $app_context above
}
In keeping with Magpie's general goal of letting developers do what makes the
most sense to them, Magpie does not enforce many rules about what the $ctxt
can be, or how it can be used. The only constraint is that the context member
must be a scalar or reference. Typically, the context member is a
reference to a Perl hash, an anonymous hash reference, or a blessed object.
Again, some examples that might appear in your in your interface
module/script:
# use a reference to an existing hash
my %context = (
template_dir => '/usr/local/my/app/templates',
default_template => 'index.xsl',
);
my $handler = builder {
enable "Magpie", context => \%context, pipeline => [
...
];
};
# some advanced apps do well to make the context an object
# that implements is own set of methods
my $context = My::Application::ContextMember->new( %args );
my $handler = builder {
enable "Magpie", context => $context, pipeline => [
...
];
};
When no context member is explicitly passed into the Magpie machine an anonymous
hash reference is used as a fallback.
# no $ctxt is passed in
my $handler = builder {
enable "Magpie", pipeline => [
...
];
};
# then later...
sub myevent {
my $self = shift;
my $ctxt = shift; # now an anonymous hashref
}
Obviously, the role of the context member will vary greatly depending upon your
coding style and the needs of the application. In general, though, the most
common use of the $ctxt is to accumulate the data needed to render the proper
output for the current request. For example, if you are using the Template
Toolkit Transformer class (Magpie::Transformer::TT2) you might use a plain
hash reference as the context member, then use it to capture the template name
and variables that your templates depend on to deliver the content.
Event Handler Return Codes¶
Each event handler method must return one of a number of
event handler
return codes. The codes signal Magpie's internal event loop about what
to do after the current event handler method is finished. The codes themselves
are numeric, but are implemented as convenient Perl constants via the
"Magpie::Constants" module so you do not have to try to remember
what the numeric codes are (this is similar to the way the
"Apache::Constants" module works for HTTP return codes).
use Magpie::Constants;
sub myevent {
my $self = shift;
my $ctxt = shift;
# do a little dance...
return OK;
}
The most common return codes and their effects on the application's behavior are
as follows:
- "OK"
- Returning "OK" from you event handler method signals Magpie that
everything went as expected during the method's run and that it is safe to
continue.
sub event_init {
my $self = shift;
my $ctxt = shift;
# actual application behavior here
# everything went well, continue on...
return OK;
}
- "DECLINED"
- Returning "DECLINED" from your event handler method tells
Magpie's internal event queue to skip to the next application or output
class in the pipeline. Any other methods in the current application class
that would usually be fired based on the current state will be skipped.
sub init {
my $self = shift;
my $ctxt = shift;
unless ( defined $ctxt->{some_required_data} ) {
# we don't have the data we need to continue
$ctxt->{error_message} = "Insufficient data for init event";
return DECLINED;
}
# otherwise continue on...
return OK;
}
- "OUTPUT"
- Where the "DECLINED" return code signals Magpie to skip to the
very next class in the pipeline, returning "OUTPUT" from
your event handler method tells Magpie's internal event queue to skip to
very last class in the application pipeline (which is presumed to
be the Output class for the current application).
sub event_init {
my $self = shift;
my $ctxt = shift;
unless ( my $user = $self->query->cookie('app_user') ) {
# like DECLINED above, but set a redirect
# header and skip directly to Output phase
$self->redirect('/login.xml');
return OUTPUT;
}
# otherwise continue on...
return OK;
}
- "DONE"
- Returning "DONE" from your event handler method stops the
application pipeline dead in its tracks. All subsequent classes that may
be in the pipeline are skippedIt is rarely used, given it typically stops
the application before any data is sent to the client, but it can be
useful for sending appropriate HTTP response codes.
sub event_init {
my $self = shift;
my $ctxt = shift;
unless ( -f $ctxt->{some_required_file} ) {
# we don't have the some crucial file needed to proceed
# so throw a 404 Not Found response while stopping the
# application
$self->response->status(404);
return DONE;
}
# otherwise continue on...
return OK;
}
References¶
- <http://foldoc.doc.ic.ac.uk/foldoc/foldoc.cgi?finite+state+machine>
- FOLDOC definition of Finite State Machines.
1;
AUTHORS¶
- •
- Kip Hampton <kip.hampton@tamarou.com>
- •
- Chris Prather <chris.prather@tamarou.com>
COPYRIGHT AND LICENSE¶
This software is copyright (c) 2011 by Tamarou, LLC.
This is free software; you can redistribute it and/or modify it under the same
terms as the Perl 5 programming language system itself.
POD ERRORS¶
Hey!
The above document had some coding errors, which are explained
below:
- Around line 83:
- alternative text 'interface script/module' contains non-escaped | or
/