use Maypole::Config;
use Maypole::Constants;
use Maypole::Headers;
+use Maypole::Components;
use URI();
+use URI::QueryParam;
+use NEXT;
+use File::MMagic::XS qw(:compat);
our $VERSION = '2.11';
+our $mmagic = File::MMagic::XS->new();
# proposed privacy conventions:
# - no leading underscore - public to custom application code and plugins
=cut
-__PACKAGE__->mk_classdata($_) for qw( config init_done view_object );
+__PACKAGE__->mk_classdata($_) for qw( config init_done view_object model_classes_loaded);
__PACKAGE__->mk_accessors(
qw( params query objects model_class template_args output path
args action template error document_encoding content_type table
- headers_in headers_out stash status)
+ headers_in headers_out stash status parent)
);
__PACKAGE__->config( Maypole::Config->new() );
__PACKAGE__->init_done(0);
+__PACKAGE__->model_classes_loaded(0);
+
=head1 HOOKABLE METHODS
As a framework, Maypole provides a number of B<hooks> - methods that are
=cut
-sub debug { 0 }
+sub debug { 0 }
=item config
=item setup
- My::App->setup($data_source, $user, $password, \%attr);
-
-Initialise the Maypole application and plugins and model classes - see
-L<Maypole::Manual::Plugins>.
-
-If your model is based on L<Maypole::Model::CDBI>, the C<\%attr> hashref can
-contain options that are passed directly to L<Class::DBI::Loader>, to control
-how the model hierarchy is constructed.
+ My::App->setup($data_source, $user, $password, \%attr);
+Initialise the Maypole application and plugins and model classes.
Your application should call this B<after> setting up configuration data via
L<"config">.
+It calls the hook C<setup_model> to setup the model. The %attr hash contains
+options and arguments used to set up the model. See the particular model's
+documentation. However here is the most usage of setup where
+Maypole::Model::CDBI is the base class.
+
+ My::App->setup($data_source, $user, $password,
+ { opitons => { # These are DB connection options
+ AutoCommit => 0,
+ RaiseError => 1,
+ ...
+ },
+ # These are Class::DBI::Loader arguments.
+ relationships => 1,
+ ...
+ }
+ );
+
+Also, see L<Maypole::Manual::Plugins>.
+
=cut
+
sub setup
{
my $class = shift;
=cut
-sub setup_model
-{
- my $class = shift;
-
- $class = ref $class if ref $class;
-
- my $config = $class->config;
-
- $config->model || $config->model('Maypole::Model::CDBI');
-
- $config->model->require or die sprintf
- "Couldn't load the model class %s: %s", $config->model, $@;
-
- # among other things, this populates $config->classes
- $config->model->setup_database($config, $class, @_);
-
- foreach my $subclass ( @{ $config->classes } )
- {
- no strict 'refs';
- unshift @{ $subclass . "::ISA" }, $config->model;
-
- # Load custom model code, if it exists - nb this must happen after the
- # unshift, to allow code attributes to work, but before adopt(),
- # in case adopt() calls overridden methods on $subclass
- $class->load_model_subclass($subclass);
-
- $config->model->adopt($subclass) if $config->model->can("adopt");
- }
+sub setup_model {
+ my $class = shift;
+ $class = ref $class if ref $class;
+ my $config = $class->config;
+ $config->model || $config->model('Maypole::Model::CDBI');
+ $config->model->require or die sprintf
+ "Couldn't load the model class %s: %s", $config->model, $@;
+
+ # among other things, this populates $config->classes
+ $config->model->setup_database($config, $class, @_);
+
+ foreach my $subclass ( @{ $config->classes } ) {
+ next if $subclass->isa("Maypole::Model::Base");
+ no strict 'refs';
+ unshift @{ $subclass . "::ISA" }, $config->model;
+ }
+
+ # Load custom model code, if it exists - nb this must happen after the
+ # unshift, to allow code attributes to work, but before adopt(),
+ # in case adopt() calls overridden methods on $subclass
+ foreach my $subclass ( @{ $config->classes } ) {
+ $class->load_model_subclass($subclass) unless ($class->model_classes_loaded());
+ $config->model->adopt($subclass) if $config->model->can("adopt");
+ }
+
}
=item load_model_subclass($subclass)
C<$subclass> package, if one exists. So if you make a customized C<BeerDB::Beer>
package, you don't need to explicitly load it.
-If, perhaps during development, you don't want to load up custom classes, you
+If automatic loading causes problems, Override load_model_subclass in your driver.
+
+sub load_model_subclass {};
+
+Or perhaps during development, if you don't want to load up custom classes, you
can override this method and load them manually.
=cut
-sub load_model_subclass
-{
- my ($class, $subclass) = @_;
-
- my $config = $class->config;
-
- # Load any external files for the model base class or subclasses
- # (e.g. BeerDB/DBI.pm or BeerDB/Beer.pm) based on code borrowed from
- # Maypole::Plugin::Loader and Class::DBI.
- if ( $subclass->require )
- {
- warn "Loaded external module for '$subclass'\n" if $class->debug > 1;
- }
- else
- {
- (my $filename = $subclass) =~ s!::!/!g;
- die "Loading '$subclass' failed: $@\n"
- unless $@ =~ /Can\'t locate \Q$filename\E\.pm/;
- warn "No external module for '$subclass'"
- if $class->debug > 1;
- }
+sub load_model_subclass {
+ my ($class, $subclass) = @_;
+
+ my $config = $class->config;
+
+ # Load any external files for the model base class or subclasses
+ # (e.g. BeerDB/DBI.pm or BeerDB/Beer.pm) based on code borrowed from
+ # Maypole::Plugin::Loader and Class::DBI.
+ if ( $subclass->require ) {
+ warn "Loaded external module for '$subclass'\n" if $class->debug > 1;
+ } else {
+ (my $filename = $subclass) =~ s!::!/!g;
+ die "Loading '$subclass' failed: $@\n"
+ unless $@ =~ /Can\'t locate \Q$filename\E\.pm/;
+ warn "No external module for '$subclass'"
+ if $class->debug > 1;
+ }
}
=item init
# BeerDB::handler() and so this inherited implementation will be
# found. See e.g. "Practical mod_perl" by Bekman & Cholet for
# more information <http://modperlbook.org/html/ch25_01.html>
-sub handler : method
-{
- # See Maypole::Workflow before trying to understand this.
- my ($class, $req) = @_;
+sub handler : method {
+ # See Maypole::Workflow before trying to understand this.
+ my ($class, $req) = @_;
- $class->init unless $class->init_done;
+ $class->init unless $class->init_done;
- my $self = $class->new;
-
- # initialise the request
- $self->headers_out(Maypole::Headers->new);
- $self->get_request($req);
- $self->parse_location;
-
- # hook useful for declining static requests e.g. images, or perhaps for
- # sanitizing request parameters
- $self->status(Maypole::Constants::OK()); # set the default
- $self->__call_hook('start_request_hook');
- return $self->status unless $self->status == Maypole::Constants::OK();
-
- die "status undefined after start_request_hook()" unless defined
- $self->status;
-
- $self->get_session;
- $self->get_user;
+ my $self = $class->new;
- my $status = $self->handler_guts;
+ # initialise the request
+ $self->headers_out(Maypole::Headers->new);
+ $self->get_request($req);
+ $self->parse_location;
- # moving this here causes unit test failures - need to check why
- # before committing the move
- #$status = $self->__call_process_view unless $self->output;
+ # hook useful for declining static requests e.g. images, or perhaps for
+ # sanitizing request parameters
+ $self->status(Maypole::Constants::OK()); # set the default
+ $self->__call_hook('start_request_hook');
+ return $self->status unless $self->status == Maypole::Constants::OK();
- return $status unless $status == OK;
+ die "status undefined after start_request_hook()" unless defined
+ $self->status;
- # TODO: require send_output to return a status code
- $self->send_output;
+ $self->get_session;
+ $self->get_user;
- return $status;
+ my $status = $self->handler_guts;
+ return $status unless $status == OK;
+
+ # TODO: require send_output to return a status code
+ $self->send_output;
+
+ return $status;
+}
+
+=back
+
+=head2 component
+
+ Run Maypole sub-requests as components using L<Maypole::Components>
+
+ [% request.component("/beer/view_as_component/20") %]
+
+ Allows you to integrate the results of a Maypole request into an existing
+request. You'll need to set up actions and templates
+which return fragments of HTML rather than entire pages, but once you've
+done that, you can use the C<component> method of the Maypole request object
+to call those actions. You may pass a query string in the usual URL style.
+You should not fully qualify the Maypole URLs.
+
+=cut
+
+sub component {
+ my ( $r, $path ) = @_;
+ my $self = bless { parent => $r }, ref $r;
+ my $url = URI->new($path);
+ $self->{path} = $url->path;
+ $self->parse_path;
+ $self->params( $url->query_form_hash );
+ $self->query( $r->params );
+ $self->handler_guts;
+ return $self->output;
+}
+
+sub get_template_root {
+ my $self = shift;
+ my $r = shift;
+ return $r->parent->get_template_root if $r->{parent};
+ return $self->NEXT::DISTINCT::get_template_root( $r, @_ );
+}
+
+sub view_object {
+ my $self = shift;
+ my $r = shift;
+ return $r->parent->view_object if $r->{parent};
+ return $self->NEXT::DISTINCT::view_object( $r, @_ );
}
# Instead of making plugin authors use the NEXT::DISTINCT hoopla to ensure other
$self->__load_request_model;
- my $applicable = $self->is_model_applicable;
+ my $applicable = $self->is_model_applicable == OK;
$self->__setup_plain_template unless $applicable;
if ( my $error = $@ )
{
- $status = $self->call_exception($error);
+ $status = $self->call_exception($error, "authentication");
if ( $status != OK )
{
if ( my $error = $@ )
{
- $status = $self->call_exception($error);
+ $status = $self->call_exception($error, "model");
if ( $status != OK )
{
# less frequent path - perhaps output has been set to an error message
return OK if $self->output;
-
+
# normal path - no output has been generated yet
- return $self->__call_process_view;
+ my $processed_view_ok = $self->__call_process_view;
+
+ $self->{content_type} ||= $self->__get_mime_type();
+ $self->{document_encoding} ||= "utf-8";
+
+ return $processed_view_ok;
+}
+
+my %filetypes = (
+ 'js' => 'text/javascript',
+ 'css' => 'text/css',
+ 'htm' => 'text/html',
+ 'html' => 'text/html',
+ );
+
+sub __get_mime_type {
+ my $self = shift;
+ my $type;
+ if ($self->path =~ m/.*\.(\w{3,4})$/) {
+ $type = $filetypes{$1};
+ } else {
+ $type = $mmagic->checktype_contents($self->output);
+ }
+ return $type;
}
sub __load_request_model
if ( my $error = $@ )
{
- $status = $self->call_exception($error);
+ $status = $self->call_exception($error, "view");
if ( $status != OK )
{
Returns a Maypole::Constant to indicate whether the request is valid.
+=cut
+
+sub is_applicable { return shift->is_model_applicable(@_); }
+
=item is_model_applicable
Returns true or false to indicate whether the request is valid.
=cut
-sub is_model_applicable
-{
+sub is_model_applicable {
my ($self) = @_;
-
- # cater for applications that are using obsolete version
- if ($self->can('is_applicable'))
- {
- warn "DEPRECATION WARNING: rewrite is_applicable to the interface ".
- "of Maypole::is_model_applicable\n";
- return $self->is_applicable == OK;
- }
# Establish which tables should be processed by the model
my $config = $self->config;
. join( ",", keys %$ok_tables )
if $self->debug and not $ok_tables->{$table};
- return 0;
+ return DECLINED;
}
# Is the action public?
my $action = $self->action;
- return 1 if $self->model_class->is_public($action);
+ return OK if $self->model_class->is_public($action);
warn "The action '$action' is not applicable to the table '$table'"
- if $self->debug;
+ if $self->debug;
- return 0;
+ return DECLINED;
}
=item get_session
sub call_exception
{
- my ($self, $error) = @_;
+ my ($self, $error, $when) = @_;
# Check if we have a model class with an exception() to delegate to
if ( $self->model_class && $self->model_class->can('exception') )
{
- my $status = $self->model_class->exception( $self, $error );
+ my $status = $self->model_class->exception( $self, $error, $when );
return $status if $status == OK;
}
- return $self->exception($error);
+ return $self->exception($error, $when);
}
=cut
-sub exception { return ERROR }
+sub exception {
+ my ($self, $error, $when) = @_;
+ if ($self->view_object->can("report_error") and $self->debug) {
+ $self->view_object->report_error($self, $error, $when);
+ return OK;
+ }
+ return ERROR;
+}
=item additional_data
=cut
-sub get_template_root {'.'}
-
=back
=head2 Request properties