X-Git-Url: https://git.decadent.org.uk/gitweb/?a=blobdiff_plain;f=lib%2FMaypole.pm;h=e677828c96916ad6c4dddcd8de07eba08f5d5e77;hb=8fe02231d92e4564f0dc52a1aa00e9d0cb974819;hp=55aa509c1f84def086eccc44ebabc10136a994e7;hpb=dc05783cf83b60a298f12e52ac6d67c489f35868;p=maypole.git diff --git a/lib/Maypole.pm b/lib/Maypole.pm index 55aa509..e677828 100644 --- a/lib/Maypole.pm +++ b/lib/Maypole.pm @@ -1,143 +1,191 @@ 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. +See L. =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, +There's some documentation for the workflow in L, but the basic idea is that a URL part like C gets translated into a call to Clist>. 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) + + ProductDatabase->config->model("ProductDatabase::Model"); (The C<:Exported> attribute means that the method can be called via the URL C/supersearch/...>.) @@ -246,10 +297,57 @@ systems and database abstraction layers as time goes on. The article at C 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, which interfaces +the Maypole framework to Apache mod_perl; another important one is +L. + +If you are implementing Maypole subclasses, you need to provide at least +the C and C methods. You may also want to +provide C and C. See the +L 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, L. + +=head1 MAINTAINER + +Sebastian Riedel, c + =head1 AUTHOR Simon Cozens, C +=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;