http://qs1969.pair.com?node_id=411968

I have only recently started learning to use mod_perl, but, so far, I like what I see. There is one thing I couldn't find, though. I wanted a simple application controller, which would handle some of the grunt work for me. I wanted to separate the logic and display into modules and templates. This didn't seem like a lot to ask, but I couldn't find what I was looking for on CPAN.

CGI::Application came close, and merlyn's newly released CGI::Prototype looks interesting (apparently this is a recently popular subject), but both are geared towards CGI. They can doubtlessly be made to work with mod_perl via Apache::Registry, but I would rather run my controller directly as a PerlHandler. I got [a bit too] excited when I saw Apache::MVC, but, alas, that is specifically for Maypole. Maypole looks very nice indeed, but it's not quite what I was looking for. I also tried searching CPAN for 'apache controller', and other similar permutations. I found many things that seemed specific to one framework or another, but nothing generic that had the attributes I wanted. Perhaps this was a failing -- on my part -- to search for the relevant terms.

So I thought, "what the heck," and rolled my own. I made it about as simple as I could possibly imagine, while still being useful, and began to use it for my personal website. It seems to work fairly well, but I was interested in some feedback. The main things I was wonder are:

Here is the documentation I have written:


SYNOPSIS

Apache::Controller is a generic controller for mod_perl web applications. It will map each incoming request to a certain action module, fill out a template, and send the resulting data to the client.

    package MyApplication::Controller;
    use lib "/my/application/root/modules";
    use Apache::Controller;
    my $controller = Apache::Controller->new(
        ROOT => "/my/application/root",
        action_modules => {
            _default => 'MyApplication::Default',
            manage   => 'MyApplication::Manage',
            view     => 'MyApplication::View',
        },
    );

    sub handler { $controller->handler(shift) }
    1;


METHODS

new
Creates a new Apache::Controller object. Required named parameters are ROOT and action_modules.

ROOT defines the root directory where this application resides.

action_modules is a hash that maps action names to modules. The keys are names of the actions, and the values are corresponding modules. The modules should each contain a subroutine called handler.

handler
Handles the request. Requires one argument, an Apache::Request object.

The first stage of request handling is URI parsing. The URI is split into an action name and arguments portion. The action name looks like a top-level directory in the URI, and the arguments consist of anything following the action name. For example, a request to http://www.example.com/view/page1/print would call the view action with arguments page1/print.

If there is no action name found in the URI, _default is used. This is analogous to index.html. Handling of arguments is currently left entirely to the action module, but may be standardized in the future.

Once the action name is parsed, the action_modules hash is consulted to find an action module. If one is not found, NOT_IMPLEMENTED is returned. If one is found, it is checked for a handler subroutine. If the handler is not found, NOT_IMPLEMENTED is returned.

The handler subroutine can return one of two things: a redirect URL, or a list of parameters. This determination is done by counting the number of elements returned. If there's only a single element returned, it is assumed to be a redirect URL. If there is an even number of elements returned, it is assumed to be a list of parameters. If there is an odd number of elements, or no elements at all, a SERVER_ERROR is returned.

The final stage in request handling is output. First, a template is looked for in the ROOT/templates directory. The template is assumed to have the same name as the action, with the .tmpl file extension. For example, an action called view would have a template file named ROOT/templates/view.tmpl.

If found, the template is loaded and passed the parameter list, then output to the client. If no template is found, a default page is populated, which lists each element in the parameter list. This is a debug feature which may be disabled in the future.


I have decided not to include the code in this post, because I'd rather not (yet) concentrate the Power of the Monks in that direction. Right now I'm more concerned with getting the documentation, name, and interface right, than with code cleanliness and correctness. I will nail down those bits if there's any further interest in this thing.

Replies are listed 'Best First'.
Re: RFC: Apache::Controller
by Joost (Canon) on Dec 02, 2004 at 23:26 UTC
    First things first: I don't think the "perfect" framework for web applications has been invented yet, so ++ for giving it a shot.

    One thing I don't like is that the template is too tightly coupled to the action - it's entirely possible that several related actions want to use the same template, or for an action to have more than one resulting template.

    For instance:
    form -> (submit action) -> form again, with missing data highlighted -> (submit action) -> confirmation screen.

    By deriving the template name from the classname of the action you need to create extra templates that will probably only include the "real" template.

    Also, I'm sceptical about using classes to describe actions. See my comment in the CGI::Prototype thread.

    Lastly, I would like more hooks in the code - some place to do general setup or session management would be nice for instance (maybe by inheriting from the Apache::Controller). See CGI::Application and CGI::Prototype for examples :-)

      Thanks for your comments! I will definitely give them some more thought, but here are some of my initial reactions.

      First things first: I don't think the "perfect" framework for web applications has been invented yet

      I agree, but before we start off with the wrong idea, I'm not really attempting to build an entire framework. I'm only trying to make one part of it -- the controller. And a very limited controller at that.

      One thing I don't like is that the template is too tightly coupled to the action - it's entirely possible that several related actions want to use the same template

      This is true, and probably something I'll put in eventually. I could easily extend the contructor to take in template names for the actions, along with the module names. I'd keep the default behavior of template name being the same as action name, though, because it's quite convenient.

      or for an action to have more than one resulting template.

      I'm not so sure about this. I would either make the template flexible enough to handle everything the action needs, or to create two different actions.

      Also, I'm sceptical about using classes to describe actions.

      Well, I don't know if this has any bearing on your argument or not, but I'm not really using classes. Just plain old Perl packages. The main purpose they serve is to contain the handler subroutines, but they also make for a nice separation at the filesystem level, which I like.

      Lastly, I would like more hooks in the code - some place to do general setup or session management would be nice for instance

      This is a perfect example of why I don't like the name I've chosen. I'm not attempting to write an end-all, be-all solution, just something that lets me get some stuff done. These features would definitely be useful in many applications, but for what I'm doing, they'd just get in the way. Maybe there's a better way to name my module that conveys that message?

        or for an action to have more than one resulting template.
        I'm not so sure about this. I would either make the template flexible enough to handle everything the action needs, or to create two different actions
        But how do you know what action is going to get run, then?

        See my example about form validation above. Basically, you recieve data from "form 1" ( to an action) , and you want to either a) go back to a form 1. b) proceed to form 2, and try to submit that (repeat for form 2, 3 etc.).

        In your system you will need to include form 1 and form 2 into one template just because the action can't choose the template for you. It's certainly possible to do that, but I think it will get ugly very quickly.

Re: RFC: Apache::Controller
by hardburn (Abbot) on Dec 03, 2004 at 05:03 UTC

    If you're aiming for using such a beast directly as an Apache handler, you could say:

    package My::App; use base 'CGI::Application'; sub handler { my $r = shift; __PACKAGE__->new->run; } # Rest of CGI::Application code

    Nothing to it, really. No need for wheel-reinvention when this wheel will do what you want.

    "There is no shame in being self-taught, only in not trying to learn in the first place." -- Atrus, Myst: The Book of D'ni.

      Hmm, of course it looks simple and obvious when someone else does it. Maybe they should put an example like this in the CGI::Application docs. The only mention I saw of mod_perl said that it could be used with Apache::Registry, which caused me to immediately look elsewhere.

        Re: C::A and mod_perl. I guess the best place to go after the docs is the Wiki entry on using a mod_perl handler. Alas, the wiki doesn't seem to be as well known as it should be....
Re: RFC: Apache::Controller
by perrin (Chancellor) on Dec 02, 2004 at 22:28 UTC
    If your problem with CGI::Application was that you don't want to waste your time with Apache::Registry and little .cgi files, check out CGI::Application::Plugin::Apache, written by one of my co-workers. It makes CGI::Application fit into mod_perl much more naturally.
      If your problem with CGI::Application was that you don't want to waste your time with Apache::Registry and little .cgi files

      Yes, that was my main problem with it.

      check out CGI::Application::Plugin::Apache, written by one of my co-workers. It makes CGI::Application fit into mod_perl much more naturally.

      Ah, if only I'd known about that before I started, it would have saved me a few hours of work. But at this point, I probably won't make the effort to switch over.

      I'm still curious if my module would be of interest to anyone else. So far it doesn't seem like it, so I'm leaning towards not releasing it...

Re: RFC: Apache::Controller
by domm (Chaplain) on Dec 03, 2004 at 09:27 UTC
    I'm in a hurry right now, so I haven't read your node completly, but what I read sounds a bit like Apache::Dispatch
    -- #!/usr/bin/perl for(ref bless{},just'another'perl'hacker){s-:+-$"-g&&print$_.$/}

      Ah, another potential solution that I missed. But like I said before, I still wonder if there's any value for others in my module. More and more it seems there isn't.

Re: RFC: Apache::Controller
by sri (Vicar) on Dec 04, 2004 at 03:26 UTC
    Funny, this looks a bit similar to a project i'll release very soon called Catalyst.

    I started the project while i maintained Maypole, it was supposed to fix some design flaws and become Maypole 3.0.

    But it has fast grown into something very different from Maypole, so it became a subproject which now completely aims at enterprise class applications.

    There are still lots of features from Maypole present but i also added many things from Struts, Struts-Chain, Struts-Delegate(Action mapping, Chain of Responsibility, Forms, Decorators, Filters), Tomcat(Contexts, Sessions, elegant api for headers, cookies and uploads...), SpringMVC(AOP features...), Ruby on Rails (Multiple controller support) and WebWork2.

    Stuff like exception handling, roles based security, i18n, mod_perl1/mod_perl2/CGI support with unified api, simple debugging and profiling is naturally built in.

    It also has many benefits for development in big teams and refactoring. (separation of flow and code, splitting code into small pieces...)

    Here is an example for a Controller class.
    package PetStore; use strict; use Catalyst '-Debug'; # Application name PetStore->config->name('Catalyst PetStore'); # Root directory for additional files PetStore->config->root('/home/sri/PetStore/web'); # Base uri for our application PetStore->config->base('http://localhost/petstore'); # Session settings PetStore->config->session( class => 'Apache::Session::Postgres', options => { DataSource => 'dbi:Pg:dbname=petstore', TableName => 'session', UserName => 'postgres', Password => 0, Commit => 1 } ); # Actions PetStore->action( # Built in action that'll get called at the end of a request, # we use it for the view here, # which in this case is YAML REST or TT2 _end => sub { # form() returns a Data::FormValidator::Results object that # will be automatically initialised when you use it if ( form->valid('rest') ) { # trunk is our universal data container to exchange # informations between actions and methods trunk( content_type => 'text/plain' ); call('PetStore::View::REST'); } else { # Set a default template trunk( template => 'index.tt' ); call('PetStore::View::TT'); } }, # Built in action that'll get called once at startup time to # initialze your environment '_setup' => sub { # setup initializes a Catalyst component for you setup('PetStore::Model::Cart'); setup( 'PetStore::Model::CDBI', base => 'Catalyst::Model::CDBI', dsn => 'dbi:Pg:dbname=petstore', user => 'postgres', password => '', options => { AutoCommit => 1 } ); setup( 'PetStore::View::REST', base => 'Catalyst::View::REST::YAML' ); setup( 'PetStore::View::TT', base => 'Catalyst::View::TT' ); }, # Private action to publish the cart _cart_to_objects => sub { call(qw(PetStore::Model::Cart items_to_objects)); call(qw(PetStore::Model::CDBI::Item describe_cart_items)); call(qw(PetStore::Model::CDBI::Category describe_cart_items)); }, # Private action to to publish categories _categories_to_objects => sub { call(qw(PetStore::Model::CDBI::Category list)); }, # Start page 'index.html' => sub { trunk( template => 'index.tt' ); # Merge with the existing Data::FormValidator::Results object # or create a new one form( required => ['yada'] ); # Include a private action at this point include('_categories_to_objects'); }, # Show and edit the cart 'cart.html' => sub { trunk( template => 'cart.tt' ); call(qw(PetStore::Model::Cart add)) if form->valid('add'); call(qw(PetStore::Model::Cart update)) if form->valid('update'); include('_cart_to_objects'); }, # Show products in category 'category.html' => sub { roles(qw(foo bar)); trunk( template => 'category.tt' ); call(qw(PetStore::Controller::CDBI::Product list)); } ); 1;


    The code is almost done and i'm currently writing documentation and examples for it, so if you're interested just hang out at #maypole on irc.perl.org (i'm searching testers;) or wait for the release, which will happen soon.
      Funny, this looks a bit similar to a project i'll release very soon called Catalyst.

      Only if you would say a jetliner is "a bit similar" to a hang glider. :-) I must say, your project is much more ambitious than mine. I was attempting to be one humble piece of a very small pie, but what you have seems to be an entire pie with ice cream and chocolate sauce on the side. [Hmm, what other strained metaphors can I come up with?]

      What you've shown looks very nice, and it would probably save a lot of time and effort in a large project, but for some projects it would be major overkill. I was trying to build the module that's useful for the smaller projects. It works well with the small, extremely simple application that I'm building, and attempts to follow the "do one thing and do it well" philosophy.

      The problem is, I don't know if the small, simple solution would really be useful for anyone else. Based on the replies to this Meditation, the answer is "nah, we're happy with what we've already got." So I'll avoid releasing it unless someone asks.

Re: RFC: Apache::Controller
by Mutant (Priest) on May 03, 2005 at 10:44 UTC
    Just to add another module to the list (since this node came up in a search for this sort of stuff), there's also CGI::Application::Dispatch.