Making Catalyst Session Flash Methody

| 7 Comments

I have a handful of modest Catalyst applications and will undoubtedly have more. One of the reasons I enjoy working with Perl 5 so much is that it's reasonably easy to solve a problem experienced in multiple places, extract that solution, and generalize and publish it for other people. The modern Perl world only makes that easier with Moose, CPAN, and even Github.

Sometimes that ease moves the problem around, which reinforces the notion that the only two hard problems in computer science are naming and caching. To abuse that truism, once I've found a workable solution, the hard part is figuring out what to call it and how to make it available to other people.

Take the flash feature of Catalyst's Catalyst::Plugin::Session. It resembles the stash, in that you can store and retrieve information in and from the flash, but that data is only available until the first request which uses it. This is incredibly handy for user notifications, such as "You missed a required entry field!" or "Your interaction succeeded!", especially when combined with the Humane JS library.

My controller actions have a pattern:

sub edit :Chained( 'get_widget' ) :PathPart('edit') :Args(0)
{
    my ($self, $c) = @_;

    return unless lc $c->req->method eq 'post';

    my $params = $c->req->params;
    my $widget   = $c->stash->{widget};

    try
    {
        $widget->$_( $params->{$_} ) for $widget->editable_fields;
        $widget->update;
        push @{ $c->flash->{messages} }, 'Updated widget!';
    }
    catch { push @{ $c->flash->{errors} }, $_ };

    return $c->res->redirect(
        $c->uri_for( $c->controller( 'Widgets' )
          ->action_for( 'index' ) )
    );
}

Yet every time I write that code, it feels slightly wrong, as if I'm violating encapsulation by updating the variable behind the flash directly. It's also prone to typos and has the whiff of violating the Law of Demeter.

I prefer instead to write:

    try
    {
        $widget->$_( $params->{$_} ) for $widget->editable_fields;
        $widget->update;
        $c->add_message( 'Updated widget!' );
    }
    catch { $c->add_error( $_ ) };

... where my actions don't have to know about the internals of how the flash stores its data, where I don't have the possibility of stomping all over the array references accidentally, and where I can replace syntax (the grotty Perl 5 reference syntax!) with a method which describes my intent.

The simplest way to make this work is:

package Catalyst::Plugin::Session::FlashMethods;
# ABSTRACT: add flash-manipulation methods to Catalyst::Plugin::Session

use parent 'Catalyst::Plugin::Session';

use Modern::Perl;

sub add_message
{
    my $self = shift;
    push @{ $self->flash->{messages} }, @_;
}

sub add_error
{
    my $self = shift;
    push @{ $self->flash->{errors} }, @_;
}

1;

... and load it in my applications with:

use Catalyst::Plugin::ConfigLoader;
use Catalyst::Plugin::Static::Simple;
use Catalyst::Plugin::Session::FlashMethods;
use Catalyst::Plugin::Session::State::Cookie;
use Catalyst::Plugin::Session::Store::FastMmap;

use Catalyst qw/
    ConfigLoader
    Static::Simple
    Session::FlashMethods
    Session::State::Cookie
    Session::Store::FastMmap
/;

This approach has (at least) two problems. First, while I'm happy to make this code available to other developers, I don't want to add any difficulties to use other plugins which may themselves extend or modify Catalyst::Plugin::Session.

Second, this code is awfully specific to my own uses. I believe those uses could help other people, but why solve a problem so specific when genericity and wider applicability are possible?

Rephrasing the problem might help.

I have a hash. I want named methods to store application-specific data in that hash. I would like the ability to customize that set of methods methods without hard-coding them, so as to produce a component that multiple people can reuse in multiple applications.

You can see why a big bag of untyped stuff (a hash) is useful.

What I really need is a way to say "My application will use these and only these specific elements of the flash, so please produce methods for them." Fortunately, I have one in MooseX::Role::Parameterized. The consumer of this code looks like:

package MyApp::Catalyst::Plugin::FlashMethods;
# ABSTRACT: role to add flash-manipulation methods to Catalyst app

use Moose;

extends 'Catalyst::Plugin::Session';
with 'Catalyst::Plugin::Role::FlashMethods'
    => { flash_elements => [qw( message error )] };

1;

... and it gets enabled in my Catalyst application with:

use MyApp::Catalyst::Plugin::FlashMethods;
use Catalyst::Plugin::Session::State::Cookie;
use Catalyst::Plugin::Session::Store::FastMmap;

use Catalyst qw/
    -Debug
    ConfigLoader
    Static::Simple
    +MyApp::Catalyst::Plugin::FlashMethods
    Session::State::Cookie
    Session::Store::FastMmap
/;

... which consumes the parametric role:

package Catalyst::Plugin::Role::FlashMethods;

use MooseX::Role::Parameterized;

parameter 'flash_elements', isa => 'ArrayRef[Str]', required => 1;

role
{
    my $p = shift;

    for my $element (@{ $p->flash_elements })
    {
        method "add_${element}" => sub
        {
            my $self = shift;
            push @{ $self->flash->{ $element . 's' } }, @_;
        };
    }
};

1;

This code does have some naïve in it. In particular, it ought to use a smarter pluralization scheme. As well, the interface bothers me in two places. I'm not sure that the module's name is correct. I also don't particularly like that using this role means creating a new class of ~10 lines to make the code available as a Catalyst plugin (though how one might expand Catalyst's import() to understand how to reify a parametric role given the appropriate parameters is beyond my language design skills today).

Then again, my ~10 line plugin which consumes this role does work across my multiple Catalyst applications, at the cost of one more file and nine more lines of code altogether. The ratio of code per method decreases substantially with each new method I add, though.

I had originally intended to improve stash handling in this fashion, but but that's much more work. Neither Catalyst's core nor any other plugin I currently use rely on the presence of flash, so the scope of this work was much smaller. Even if they did, this approach does not prevent anything else from poking into the flash for good or ill.

7 Comments

So, I haven't really thought this all the way through and I certainly haven't tested it, but is there some reason you couldn't just have your main application class consume the role directly? Something like:


package My::Web::App;

use Moose;
use namespace::autoclean;
use Catalyst qw/ all plugins go here /;

extends 'Catalyst';
with 'Catalyst::Role::Flashmethods';

__PACKAGE__->setup()
1;

In other words, since Catalyst is Moose-based now, why not just directly consume the role? Why bother with the plugin wrapper at all?

Hmm. "Methody" is an old word for "Methodist" (the Protestant sect). I wonder why it's never occurred to anyone to call object-oriented programmers "methodists"? Doug Crockford already refers to class-oriented JavaScript as "classical".

I tried that. It had benefits as you can imagine, but it had the drawback that I had to load the Session plugin the normal way and only after calling __PACKAGE__->setup; could I have the application class consume the role.

That's all because I had the role require the presence of the flash() method, which I was loathe to avoid... but I didn't like the fragility of the ordering problem.

If Catalyst's plugin loading were somehow Moosified, the fragility would likely go away.

Not everyone talks like a character from Buffy the Vampire Slayer.

Have you seen my module Catalyst::Plugin::Session::AsObject? It does something sort of similar, in that it helps treat the session as an object, not a data structure.

Patching it to work with flash would be trivial, though I really need to update it to make it a role in the CatalystX namespace.

Loading a Catalyst plugin is nothing more than applying a role. There's also a still working and non-deprecated legacy plugin mechanism that involves injecting base classes, but you don't have to care about that if you already have a role to apply.

use Catalyst
  'Session',
  'FlashMethods' => { flash_elements => [qw( message error )] };

Parametrisation works just like with Moose's 'with' through Data::OptList.

If you're worried about ->flash not being present, just 'require' it from your role. If it's provided by a legacy plugin, it'll work due to those being applied before roles. If it's provided by another role plugin, Moose's role combination will sort it out.

I've also run into the whole, 'this flash thing is awesome' until it lead me to very complicated code as the particular problem grew in complexity. To a certain degree the stash and even session suffer from the same type of problem. They make very simple cases easy but suffer as the complexity grows.

Generally I like your idea of exposing just the bits you need, either with a custom plugin as you gave above or with the Catalyst::Plugin::Session::AsObject mentioned. However I think it would be more ideal of this could be set on a per controller basis rather than global to the app. Or perhaps as part of a workflow object. Unfortunately I can't see a way to do those things that keeps the easy stuff easy. I think one idea I've heard floated around (and possible in connection with the BreadBoard GSoC project) is that of having pluggable scopes, so that you could do something like (totally made up example)

package MyApp::Web::Controller::;

use Moose;
extends 'Catalyst::Controller';

## This would come from the Config
has number_from_conf => (is=>'ro', required=>1);

## This would get reset each request
has something_in_request => (is=>'ro', scope=>'Request', ...);

## rest of the actions...

The you'd have scopes for request, session, etc. This would make it possible to create custom scopes to hide complicated workflows that might span several controllers, for example.

In the meantime, as an alternative to some of your suggestions above, I've toyed with the idea of just creating models for this sort of thing and exposing them via CatalystX::Model::Adaptor. Something like

sub process_form
:Path :Args(0)
{
my ($self, $context) = @_;
my $form = #.. something something

$ctx->model('Messages', type=>'errors')
->add_messages($form->errors);
}

With CX::Model::Adaptor you can make it a 'PerRequest' model that get's built just once for the entire request. Then later in template I might have a helper function that wraps the Messages model and exposes in in a readonly manner to the template. Or something.

Some things like HTML::FormHandler try to offer a toolkit for similar effect, although sometimes all in one solutions that that get a bit inflexible for me. Ideally I'd see each tool in the box more sharply focused.

John

Modern Perl: The Book

cover image for Modern Perl: the book

The best Perl Programmers read Modern Perl: The Book.

affiliated with ModernPerl.net

Categories

Pages

About this Entry

This page contains a single entry by chromatic published on June 9, 2011 3:08 PM.

Perl 5's Unicode Flag Day was the previous entry in this blog.

My Passthrough DarkPAN is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.


Sponsored by Blender Recipe Reviews and the Trendshare how to invest guide

Powered by the Perl programming language

what is programming?