Skip to content

Instantly share code, notes, and snippets.

@jjn1056
Last active August 29, 2015 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jjn1056/fe91964282ac94ca332f to your computer and use it in GitHub Desktop.
Save jjn1056/fe91964282ac94ca332f to your computer and use it in GitHub Desktop.
=head1 PSGI Middleware and the Future of Catalyst
The PSGI specification describes middleware as:
=over 4
"A middleware component takes another PSGI application and runs it. From the perspective of a server, a middleware component is a PSGI application. From the perspective of the application being run by the middleware component, the middleware is the server. Generally, this will be done in order to implement some sort of pre-processing on the PSGI environment hash or post-processing on the response."
=back
The idea here is that your request / response cycle could pass through a number of middleware tranformations as it moves throught your application 'onion' one layer at a time. Ideally each middleware element in the stack is absolutely independent from any other. This would promote an application design that was strongly decoupled and yet because the PSGI specification it so simple, relatively easy to understand and follow and maintain.
In this ideal setup middleware would not be dependent on where it sits in the stack (it would not be dependent of another bit of middleware earlier in the cycle, for example).
In practice its become common to build middleware that is a small unit of common functionality intended to be consumed at some later point in the application. For example we have middleware like L<Plack::Middleware::Session> which creates a session object typically used in your programming logic as part of your larger application. Although doing this violates the purity of middleware, there's a tremendous seduction to take this approach, since it offers the possibility to share common bits of funtionality across different web development frameworks. This reduces the need for every framework to build its own common services and promotes interoperability, and pools scarce developer resources for maintainance tasks. For example, one in theory could have a L<Web::Simple> application and a L<Catalyst::Runtime> application running together, and using L<Plack::Middleware::Session> to share a logged in user session.
In this way we tangle the notion of middleware with your application at large, and in a real sense your application become dependent on the middleware in a way that can easily turn into a nasty structual dependency.
I've lately said that the future of L<Catalyst::Runtime> is middleware. If so, what can be do to make sure our approach minimized the problems outlined above? One approach we can take to mitigate this risk is to make sure that your middleware alone is responsible for providing an interface to the functionality it encapsulates. For example, lets look at some recent middleware written for L<Catalyst::Runtime> which is middleware intended to encapsulate the functionality of the Catalyst stash. Here's a naive version:
use strict;
use warnings;
package Catalyst::Middleware::Stash;
use base 'Plack::Middleware';
use Carp 'croak';
sub call {
my ($self, $env) = @_;
$env->{"Catalyst.Stash"} ||= +{};
return $self->app->($env);
}
and in L<Catalyst::Runtime> we'd change the stash method to look like this:
sub stash {
my $c = shift;
my $stash = $c->request->env->{"Catalyst.Stash"};
if (@_) {
my $new_stash = @_ > 1 ? {@_} : $_[0];
croak('stash takes a hash or hashref') unless ref $new_stash;
foreach my $key ( keys %$new_stash ) {
$c->request->env->{"Catalyst.Stash"}->{$key} = $new_stash->{$key};
}
}
return $c->request->env->{"Catalyst.Stash"};
}
So in this version we just move the stash hashred to the PSGI env. We expose the raw hashref and expect the consuming application to use it properly. This would work for Catalyst, but is poor encapulated and prone to misuse. Lets improve it a bit.
use strict;
use warnings;
package Catalyst::Middleware::Stash;
use base 'Plack::Middleware';
use Carp 'croak';
sub generate_stash_closure {
my $stash = shift || +{};
return sub {
if(@_) {
my $new_stash = @_ > 1 ? {@_} : $_[0];
croak('stash takes a hash or hashref')
unless ref $new_stash;
foreach my $key ( keys %$new_stash ) {
$stash->{$key} = $new_stash->{$key};
}
}
$stash;
};
}
sub call {
my ($self, $env) = @_;
$env->{"Catalyst.Stash"}
||= generate_stash_closure($env);
return $self->app->($env);
}
In this version of the middleware we assign a PSGI C<env> key the stash functionality wrapped in a coderef. Basically we just converted the L<Catalyst::Runtime> method 'stash' to be a coderef. This would actually work. To bring in into Catalyst.pm we'd need code something like:
sub stash {
my $c = shift;
$c->request->env->{"Catalyst.Stash"}->(@_);
}
So although we are better because we encapsulated the method behind what a stash is and how its altered we still have onerous structural bindings. We see need to get the raw PSGI env. A simple miss-spelling still breaks the whole thing! Lets try to improve it a bit.
use strict;
use warnings;
package Catalyst::Middleware::Stash;
use base 'Plack::Middleware';
use Carp 'croak';
our $VERSION = "0.001";
sub PSGI_KEY { "Catalyst.Stash.$VERSION" };
sub generate_stash_closure {
my $stash = shift || +{};
return sub {
if(@_) {
my $new_stash = @_ > 1 ? {@_} : $_[0];
croak('stash takes a hash or hashref')
unless ref $new_stash;
foreach my $key ( keys %$new_stash ) {
$stash->{$key} = $new_stash->{$key};
}
}
$stash;
};
}
sub _init_stash {
my ($self, $env) = @_;
return $env->{PSGI_KEY} ||=
generate_stash_closure;
}
sub call {
my ($self, $env) = @_;
$self->_init_stash($env);
return $self->app->($env);
}
So here we encapsulated the PSGI environment key behind a method. This solves the mistyping issue. We also took the opportunity to refactor how the stash get initialize. Here's how it might be used in Catalyst:
use Catalyst::Middleware:Stash;
sub stash {
my $c = shift;
$c->request->env->{Catalyst::Middleware::Stash::PSGI_KEY}->(@_);
}
Small change but that's better since we eliminated the spelling error problem and we are setup so that if we need to change the stash key, we can do so without breaking people's code (since the key name is encapsulated behind a method which comes from the middleware). But I think we can make it even better.
use strict;
use warnings;
package Catalyst::Middleware::Stash;
use base 'Plack::Middleware';
use Exporter 'import';
use Carp 'croak';
our $VERSION = "0.001";
our @EXPORT_OK = qw(stash get_stash);
sub PSGI_KEY { "Catalyst.Stash.$VERSION" };
sub get_stash { return shift->{PSGI_KEY} }
sub stash {
my ($host, @args) = @_;
return get_stash($host->env)->(@args);
}
sub generate_stash_closure {
my $stash = shift || +{};
return sub {
if(@_) {
my $new_stash = @_ > 1 ? {@_} : $_[0];
croak('stash takes a hash or hashref')
unless ref $new_stash;
foreach my $key ( keys %$new_stash ) {
$stash->{$key} = $new_stash->{$key};
}
}
$stash;
};
}
sub _init_stash {
my ($self, $env) = @_;
return $env->{PSGI_KEY} ||=
generate_stash_closure;
}
sub call {
my ($self, $env) = @_;
$self->_init_stash($env);
return $self->app->($env);
}
In this final version we'd complete encapsulated the stash interface and offered two exports to ease use if so desired. This works better than any method that hangs directly off the PSGI env since the client code is not responsible for knowing HOW to access $env. All that is needed is a valid PSGI env, the access and logic is completely on the middleware side. Here's one way this could be used in Catalyst:
use Catalyst::Middleware:Stash 'get_stash';
sub stash {
my $c = shift;
return get_stash($c->request->env)->(@_);
}
Alternatively we offer a 'stash' method that can be invoked on an object that does a method called C<env>. As it happens most common PSGI frameworks do this. Here's an example using a simple very basic PSGI application:
use Plack::Request;
use Catalyst::Middleware::Stash 'stash';
my $app = sub {
my $env = shift;
my $req = Plack::Request->new($env);
my $stashed = $res->stash->{in_the_stash}; # Assume the stash was previously populated.
return [200, ['Content-Type' => 'text/plain'],
["I found $stashed in the stash!"]];
};
So in this last approach we've managed to encapsulate the interface such that the consumer is barely aware that we are using the PSGI env at all. Give how common it is to expose a method 'env' on a request object this approach could achieve both the goal of simplicity as well as strong encapsulation of the behavior.
Ultimately if we are going to migrate more core L<Catalyst::Runtime> functionality into middleware, we need to take care that we are not making a messy and error prone interface. If we do this correctly I think we can end up with code that is more flexible, easy to understand and maintain as well as contribute to the great PSGI middleware ecosystem.
=cut
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment