Magpie(3pm)

πŸ“¦
version 1.163200 Β· 3 versions
, in debian bookworm, debian trixie, ubuntu noble

πŸ’– NAME πŸ’–

Magpie β€” the Pipelined State Machine Plack Middleware Framework, bestie. This is the Magpie(3pm) page, straight out of the User Contributed Perl Documentation section, and yes she is that girl. ✨


🌸 VERSION 🌸

version 1.163200 β€” that's the one, no cap. πŸ’…


πŸ¦‹ SYNOPSIS πŸ¦‹

Okay so here's the vibe, two flavors of .psgi file for you:

 -----
 # 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' }
   ];
 };

That one is the static pipeline β€” it loads the exact same components for literally every single request. Simple, reliable, iconic. πŸ‘‘


And then we have the glow-up version:

 -----
 # 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'}];
       }
   ];
 };

This one uses machine, match, and match_env sugar to load components conditionally. It's giving main character energy fr fr. 🫢


🧚 How Does Magpie Work? 🧚

A typical Magpie application has three parts, and honestly? A clean three-part structure. Obsessed.

  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 up, the interface component, bestie:

 # 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 little module implements a handler() method that gets called whenever the URI associated with this module is requested. Here, that method creates a brand-new Magpie application by calling the new() constructor of the Magpie::Machine class β€” because all Magpie applications are an instance of that class. Periodt.

By itself, this instance of the Machine class does nothing useful, ngl. You gotta load the Application Class (or classes) that will perform the required operations, plus the Output Class that will generate the appropriate response. How? You call the Machine instance's pipeline() method and pass in a list containing a mix of either the Perl package names of the classes you 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 β€” just by passing in the class names. Easy. πŸ’…

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 you pass 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, don't worry 🫢). When run() is called, each class that was passed in via the pipeline() method gets loaded in the order they were passed, and all event handler methods implemented in those classes that match the current application state get 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. And that's the whole ✨journey✨.


πŸŽ€ 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 figures out 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 just implement and register the events your application needs and let Magpie do the rest. Iconic behavior honestly. πŸ’œ

 # 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 β€” peep 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. Slay. ✨


πŸ’… 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
 }

okay so bestie, Magpie is all about letting you do you, so it's not gonna be all up in your business about what $ctxt can be or how you use it. The only rule? The context member has to be a scalar or a reference. That's it. Usually it's a reference to a Perl hash, an anonymous hash reference, or a blessed object. Here are some examples that might pop up in your interface module or 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 => [
       ...
    ];
};

If you don't pass in a context member, Magpie is like "no worries" and just uses an anonymous hash reference 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
}

So like, the role of the context member is gonna be totally different depending on your coding style and what your app needs. But generally? The most common use of $ctxt is to gather up all the data you need to render the right output for the current request. For example, if you're using the Template Toolkit Transformer class (Magpie::Transformer::TT2), you might use a plain hash reference as your context member, and then use it to stash the template name and variables your templates need to deliver the content. Slay. πŸ’…


Event Handler Return Codes πŸ’€

Every event handler method has to return one of these event handler return codes, bestie. These codes tell Magpie's internal event loop what to do after your method finishes. The codes are numbers, but they're implemented as super convenient Perl constants via the Magpie::Constants module, so you don't have to memorize a bunch of random numbers (it's kinda like how Apache::Constants works for HTTP return codes).

use Magpie::Constants;

sub myevent {
    my $self = shift;
    my $ctxt = shift;

    # do a little dance...

    return OK;
}

Here are the most common return codes and what they do to your app's behavior:

  • OK β€” Returning OK from your event handler method tells Magpie that everything went exactly as planned and it's safe to keep going. ✨
sub event_init {
    my $self = shift;
    my $ctxt = shift;

    # actual application behavior here

    # everything went well, continue on...
    return OK;
}
  • DECLINED β€” Returning DECLINED tells Magpie's internal event queue to skip ahead to the next application or output class in the pipeline. Any other methods in the current application class that would normally fire based on the current state? Yeah, they get skipped. Bye. πŸ‘‹
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 β€” So where DECLINED makes Magpie skip to the very next class in the pipeline, returning OUTPUT from your event handler method tells Magpie's internal event queue to skip all the way to the very last class in the application pipeline (which is usually the Output class for the current application). Like, straight to the end. 🏁
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 in the pipeline are skipped. It's rarely used, because 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 πŸ“š

1;

AUTHORS πŸ‘‘

  • Kip Hampton <kip.hampton@tamarou.com>
  • Chris Prather <chris.prather@tamarou.com>

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 86: alternative text 'interface script/module' contains non-escaped | or /


This page was brought to you by perl v5.36.0, dated 2022-11-19, and it's all about Magpie(3pm). Stay slay, bestie! πŸ’–πŸ¦‹πŸŒΈ

πŸ“¦ where this page comes from