Catalystic conversion - part 1

autarch on 2007-03-02T01:39:08

So I've already found a couple glitches/design issues with Catalyst.

Issue #1: Inheritance and actions

Say you have some code like this:

package Parent;

sub end : ActionClass('Stultify') { }

package Child;

use base 'Parent';

sub end : Private { my ( $self, $c ) = @_;

return $self->NEXT::end($c) if $c->stash()->{foo};

... }


The problem here is how Catalyst implements actions (and attributes in general, AFAICT). When you call $self->NEXT::end() from Child, you'd expect the Stultify action to get run. But it won't, because of the way Catalyst handles action attributes. The canonical (in my mind) way to implement an attribute (at least that modifies the action of a sub) in Perl is to make a new sub which wrap the original sub and implements the action. Then you take this new sub and replace the old sub in the symbol table. Of course, given a few lexicals and a few attributes, you could create a nice little nested closure leak, but hey, what's some memory between friends?

Catalyst, however, does not do this. I don't really blame the authors, because the whole wrapping thing sucks. Instead, Catalyst just records that sub X has an attribute and makes use of that knowledge somewhere in the system. In the case of actions, it seems like these are called via Catalyst's internal dispatching mechanism.

That's the root of the inheritance problem. That call to NEXT::end() does not go back through the Catalyst dispatcher, it goes through NEXT.pm's dispatcher. This means that the Stultify action never gets called. Grr, annoying.

The solution, of course, is just to stick the Stultify action on the Child::end(), but that kinda defeats the purpose of inheritance!

A better solution might be to replace Catalyst's use of NEXT with something that respects Catalyst's dispatching, maybe "CNEXT" or something like that. At the very least, this is worth documenting somewhere with a big "this will not work like you expect" warning.


Well...

phaylon on 2007-03-02T13:32:20

...I usually just use forward or detach. Catalyst's actions are more than subs in a package, they are full objects knowing their name, private paths, attributes and what else they might need to know. The actions are just built on subs in your controller package.

Not quite what I want

autarch on 2007-03-02T15:50:38

Using forward or detach doesn't quite make sense. Here's my example in a nutshell:

package VegGuide::Controller::Base;
 
use base 'Catalyst::Controller::REST';
 
sub end : ActionClass('Serialize')
{
    my $self = shift;
    my $c = shift;
 
    # This works
    return $self->NEXT::end($c)
        if $c->stash()->{rest};
 
    # These do not
    # $c->detach('Catalyst::Controller::REST');
    # $c->detach('Catalyst::Controller::REST', 'end');
 
    if ( ( ! $c->response()->status()
           || $c->response()->status() == 200 )
         && ! $c->response()->body() )
    {
        return $c->forward( $c->view() );
    }
 
    return;
}