]> git.decadent.org.uk Git - maypole.git/blobdiff - lib/Maypole.pm
Use Class::Accessor::Fast;
[maypole.git] / lib / Maypole.pm
index 55aa509c1f84def086eccc44ebabc10136a994e7..e677828c96916ad6c4dddcd8de07eba08f5d5e77 100644 (file)
 package Maypole;
-use base qw(Class::Accessor Class::Data::Inheritable);
+use base qw(Class::Accessor::FAST Class::Data::Inheritable);
 use attributes ();
-use Class::DBI::Loader;
 use UNIVERSAL::require;
-use Apache::Constants ":common";
 use strict;
 use warnings;
-our $VERSION = "0.2";
+use Maypole::Config;
+our $VERSION = "1.8";
 __PACKAGE__->mk_classdata($_) for qw( config init_done view_object );
-__PACKAGE__->mk_accessors ( qw( ar params query objects model_class
-args action template ));
-__PACKAGE__->config({});
+__PACKAGE__->mk_accessors(
+    qw( ar params query objects model_class
+      args action template )
+);
+__PACKAGE__->config( Maypole::Config->new() );
 __PACKAGE__->init_done(0);
+use Maypole::Constants;
 
+sub debug { 0 }
 
-sub import {
-    my $real = shift;
-    if ($real ne "Apache::MVC") {
+sub setup {
+    my $calling_class = shift;
+    $calling_class = ref $calling_class if ref $calling_class;
+    {
         no strict 'refs';
-        *{$real."::handler"} = sub { Apache::MVC::handler($real, @_) };
-    }
-}
 
-sub set_database {
-    my ($calling_class, $dsn) = @_;
-    $calling_class = ref $calling_class if ref $calling_class;
+        # Naughty.
+        *{ $calling_class . "::handler" } =
+          sub { Maypole::handler( $calling_class, @_ ) };
+    }
     my $config = $calling_class->config;
-    $config->{model} ||= "Apache::MVC::Model::CDBI";
-    $config->{model}->require;
-    $config->{dsn} = $dsn;
-    $config->{loader} = Class::DBI::Loader->new(
-        namespace => $calling_class,
-        dsn => $dsn
-    ); 
-    $config->{classes} = [ $config->{loader}->classes ];
-    for my $subclass (@{$config->{classes}}) {
+    $config->model || $config->model("Maypole::Model::CDBI");
+    $config->model->require;
+    die "Couldn't load the model class $config->model: $@" if $@;
+    $config->model->setup_database( $config, $calling_class, @_ );
+    for my $subclass ( @{ $config->classes } ) {
         no strict 'refs';
-        unshift @{$subclass."::ISA"}, $config->{model};
-        $config->{model}->adopt($subclass)
-           if $config->{model}->can("adopt");
+        unshift @{ $subclass . "::ISA" }, $config->model;
+        $config->model->adopt($subclass)
+          if $config->model->can("adopt");
     }
 }
 
 sub init {
-    my $class = shift;
+    my $class  = shift;
     my $config = $class->config;
-    $config->{view}  ||= "Apache::MVC::View::TT";
-    $config->{view}->require;
-    $config->{display_tables} ||= [ $class->config->{loader}->tables ];
-    $class->view_object($class->config->{view}->new);
+    $config->view || $config->view("Maypole::View::TT");
+    $config->view->require;
+    die "Couldn't load the view class ".$config->view.": $@" if $@;
+    $config->display_tables || $config->display_tables([ $class->config->tables ]);
+    $class->view_object( $class->config->view->new );
     $class->init_done(1);
 
 }
 
-sub class_of {
-    my ($self, $table) = @_;
-    return $self->config->{loader}->_table2class($table);
-}
-
 sub handler {
-    # See Apache::MVC::Workflow before trying to understand this.
-    my $class = shift;
+
+    # See Maypole::Workflow before trying to understand this.
+    my ( $class, $req ) = @_;
     $class->init unless $class->init_done;
     my $r = bless { config => $class->config }, $class;
-    $r->get_request();
+    $r->get_request($req);
     $r->parse_location();
-
-    $r->model_class($r->class_of($r->{table}));
-    my $status = $r->is_applicable;
-    if ($status == OK) { 
-        $status = $r->call_authenticate;
-        return $status unless $status == OK;
-        $r->additional_data();
-    
-        $r->model_class->process($r);
-    } else { 
-        # Otherwise, it's just a plain template.
-        delete $r->{model_class};
-        $r->{path} =~ s{/}{}; # De-absolutify
-        $r->template($r->{path});
-    }
-    return $r->view_object->process($r);
+    my $status = $r->handler_guts();
+    return $status unless $status == OK;
+    $r->send_output;
+    return $status;
 }
 
-sub get_request {
-    my $self = shift;
-    require Apache; require Apache::Request; 
-    $self->{ar} = Apache::Request->new(Apache->request);
-}
+sub handler_guts {
+    my $r = shift;
+    $r->model_class( $r->config->model->class_of( $r, $r->{table} ) );
+    my $applicable = $r->is_applicable;
+    unless ( $applicable == OK ) {
 
-sub parse_location {
-    my $self = shift;
-    $self->{path} = $self->{ar}->uri;
-    my $loc = $self->{ar}->location;
-    $self->{path} =~ s/^$loc//; # I shouldn't need to do this?
-    $self->{path} ||= "frontpage";
-    my @pi = split /\//, $self->{path};
-    shift @pi while @pi and !$pi[0];
-    $self->{table} = shift @pi;
-    $self->{action} = shift @pi;
-    $self->{args} = \@pi;
+        # It's just a plain template
+        delete $r->{model_class};
+        $r->{path} =~ s{/$}{};    # De-absolutify
+        $r->template( $r->{path} );
+    }
 
-    $self->{params} = { $self->{ar}->content };
-    $self->{query}  = { $self->{ar}->args };
+    # We authenticate every request, needed for proper session management
+    my $status;
+    eval { $status = $r->call_authenticate };
+    if ( my $error = $@ ) {
+        $status = $r->call_exception($error);
+        if ( $status != OK ) {
+            warn "caught authenticate error: $error";
+            return $r->debug ? $r->view_object->error( $r, $error ) : ERROR;
+        }
+    }
+    if ( $r->debug and $status != OK and $status != DECLINED ) {
+        $r->view_object->error( $r,
+            "Got unexpected status $status from calling authentication" );
+    }
+    return $status unless $status == OK;
+
+    # We run additional_data for every request
+    $r->additional_data;
+    if ( $applicable == OK ) {
+        eval { $r->model_class->process($r) };
+        if ( my $error = $@ ) {
+            $status = $r->call_exception($error);
+            if ( $status != OK ) {
+                warn "caught model error: $error";
+                return $r->debug ? $r->view_object->error( $r, $error ) : ERROR;
+            }
+        }
+    }
+    if ( !$r->{output} ) {    # You might want to do it yourself
+        eval { $status = $r->view_object->process($r) };
+        if ( my $error = $@ ) {
+            $status = $r->call_exception($error);
+            if ( $status != OK ) {
+                warn "caught view error: $error";
+                return $r->debug ? $r->view_object->error( $r, $error ) : ERROR;
+            }
+        }
+        return $status;
+    }
+    else { return OK; }
 }
 
 sub is_applicable {
-    my $self = shift;
+    my $self   = shift;
     my $config = $self->config;
-    $config->{ok_tables} = {map {$_ => 1} @{$config->{display_tables}}};
+    $config->ok_tables || $config->ok_tables($config->display_tables);
+    $config->ok_tables ({ map { $_ => 1 } @{ $config->ok_tables } })
+      if ref $config->ok_tables eq "ARRAY";
     warn "We don't have that table ($self->{table})"
-        unless $config->{ok_tables}{$self->{table}};
-    return DECLINED() unless exists $config->{ok_tables}{$self->{table}};
+      if $self->debug
+      and not $config->ok_tables->{ $self->{table} };
+    return DECLINED() unless exists $config->ok_tables->{ $self->{table} };
 
     # Does the action method exist?
-    my $cv = $self->model_class->can($self->{action});
-    warn "We don't have that action ($self->{action})" unless $cv;
+    my $cv = $self->model_class->can( $self->{action} );
+    warn "We don't have that action ($self->{action})"
+      if $self->debug and not $cv;
     return DECLINED() unless $cv;
 
     # Is it exported?
     $self->{method_attribs} = join " ", attributes::get($cv);
-    do { warn "$self->{action} not exported";
-    return DECLINED() 
-     } unless $self->{method_attribs} =~ /\bExported\b/i;
+    do {
+        warn "$self->{action} not exported" if $self->debug;
+        return DECLINED();
+    } unless $self->{method_attribs} =~ /\bExported\b/i;
     return OK();
 }
 
 sub call_authenticate {
     my $self = shift;
-    return $self->model_class->authenticate($self) if 
-        $self->model_class->can("authenticate");
-    return $self->authenticate();
+
+    # Check if we have a model class
+    if ( $self->{model_class} ) {
+        return $self->model_class->authenticate($self)
+          if $self->model_class->can("authenticate");
+    }
+    return $self->authenticate($self);   # Interface consistency is a Good Thing
+}
+
+sub call_exception {
+    my $self = shift;
+    my ($error) = @_;
+
+    # Check if we have a model class
+    if (   $self->{model_class}
+        && $self->model_class->can('exception') )
+    {
+        my $status = $self->model_class->exception( $self, $error );
+        return $status if $status == OK;
+    }
+    return $self->exception($error);
 }
 
-sub additional_data {}
+sub additional_data { }
 
 sub authenticate { return OK }
 
-1;
+sub exception { return ERROR }
+
+sub parse_path {
+    my $self = shift;
+    $self->{path} ||= "frontpage";
+    my @pi = split /\//, $self->{path};
+    shift @pi while @pi and !$pi[0];
+    $self->{table}  = shift @pi;
+    $self->{action} = shift @pi;
+    $self->{args}   = \@pi;
+}
 
 =head1 NAME
 
@@ -145,7 +193,7 @@ Maypole - MVC web application framework
 
 =head1 SYNOPSIS
 
-See L<Apache::MVC>.
+See L<Maypole>.
 
 =head1 DESCRIPTION
 
@@ -173,7 +221,7 @@ this:
     package ProductDatabase;
     use base 'Apache::MVC';
     __PACKAGE__->set_database("dbi:mysql:products");
-    BeerDB->config->{uri_base} = "http://your.site/catalogue/";
+    ProductDatabase->config->uri_base = "http://your.site/catalogue/";
     ProductDatabase::Product->has_a("category" => ProductDatabase::Category); 
     # ...
 
@@ -208,7 +256,7 @@ For a full example, see the included "beer database" application.
 
 =head1 HOW IT WORKS
 
-There's some documentation for the workflow in L<Apache::MVC::Workflow>,
+There's some documentation for the workflow in L<Maypole::Workflow>,
 but the basic idea is that a URL part like C<product/list> gets
 translated into a call to C<ProductDatabase::Product-E<gt>list>. This
 propagates the request with a set of objects from the database, and then 
@@ -219,7 +267,7 @@ If there's another action you want the system to do, you need to either
 subclass the model class, and configure your class slightly differently:
 
     package ProductDatabase::Model;
-    use base 'Apache::MVC::Model::CDBI';
+    use base 'Maypole::Model::CDBI';
 
     sub supersearch :Exported {
         my ($self, $request) = @_;
@@ -228,7 +276,10 @@ subclass the model class, and configure your class slightly differently:
         $r->template("template_name");
     }
 
-    ProductDatabase->config->{model_class} = "ProductDatabase::Model";
+Then your top-level application package should change the model class:
+(Before calling C<setup>)
+
+    ProductDatabase->config->model("ProductDatabase::Model");
 
 (The C<:Exported> attribute means that the method can be called via the
 URL C</I<table>/supersearch/...>.)
@@ -246,10 +297,57 @@ systems and database abstraction layers as time goes on. The article at
 C<http://www.perl.com/pub/a/2003/07/15/nocode.html> is a great
 introduction to the process we're trying to automate.
 
+=head1 USING MAYPOLE
+
+You should probably not use Maypole directly. Maypole is an abstract
+class which does not specify how to communicate with the outside world.
+The most popular subclass of Maypole is L<Apache::MVC>, which interfaces
+the Maypole framework to Apache mod_perl; another important one is
+L<CGI::Maypole>.
+
+If you are implementing Maypole subclasses, you need to provide at least
+the C<parse_location> and C<send_output> methods. You may also want to
+provide C<get_request> and C<get_template_root>. See the
+L<Maypole::Workflow> documentation for what these are expected to do.
+
+=cut
+
+sub get_template_root { "." }
+sub get_request       { }
+
+sub parse_location {
+    die "Do not use Maypole directly; use Apache::MVC or similar";
+}
+
+sub send_output {
+    die "Do not use Maypole directly; use Apache::MVC or similar";
+}
+
+=head1 SEE ALSO
+
+There's more documentation, examples, and a wiki at the Maypole web site:
+
+http://maypole.simon-cozens.org/
+
+L<Apache::MVC>, L<CGI::Maypole>.
+
+=head1 MAINTAINER
+
+Sebastian Riedel, c<sri@oook.de>
+
 =head1 AUTHOR
 
 Simon Cozens, C<simon@cpan.org>
 
+=head1 THANK YOU
+
+Jesse Scheidlower, Jody Belka, Marcus Ramberg, Mickael Joanne, Simon Flack,
+Veljko Vidovic and all the others who've helped.
+
 =head1 LICENSE
 
 You may distribute this code under the same terms as Perl itself.
+
+=cut
+
+1;