]> git.decadent.org.uk Git - maypole.git/blob - lib/Maypole.pm
Accessors for everything
[maypole.git] / lib / Maypole.pm
1 package Maypole;
2 use base qw(Class::Accessor::Fast Class::Data::Inheritable);
3 use attributes ();
4 use UNIVERSAL::require;
5 use strict;
6 use warnings;
7 use Maypole::Config;
8 our $VERSION = '2.0';
9 __PACKAGE__->mk_classdata($_) for qw( config init_done view_object );
10 __PACKAGE__->mk_accessors(
11     qw( ar params query objects model_class template_args output path
12       args action template error document_encoding content_type table)
13 );
14 __PACKAGE__->config( Maypole::Config->new() );
15 __PACKAGE__->init_done(0);
16 use Maypole::Constants;
17
18 sub debug { 0 }
19
20 sub setup {
21     my $calling_class = shift;
22     $calling_class = ref $calling_class if ref $calling_class;
23     {
24         no strict 'refs';
25
26         # Naughty.
27         *{ $calling_class . "::handler" } =
28           sub { Maypole::handler( $calling_class, @_ ) };
29     }
30     my $config = $calling_class->config;
31     $config->model || $config->model("Maypole::Model::CDBI");
32     $config->model->require;
33     die "Couldn't load the model class $config->model: $@" if $@;
34     $config->model->setup_database( $config, $calling_class, @_ );
35     for my $subclass ( @{ $config->classes } ) {
36         no strict 'refs';
37         unshift @{ $subclass . "::ISA" }, $config->model;
38         $config->model->adopt($subclass)
39           if $config->model->can("adopt");
40     }
41 }
42
43 sub init {
44     my $class  = shift;
45     my $config = $class->config;
46     $config->view || $config->view("Maypole::View::TT");
47     $config->view->require;
48     die "Couldn't load the view class " . $config->view . ": $@" if $@;
49     $config->display_tables
50       || $config->display_tables( [ $class->config->tables ] );
51     $class->view_object( $class->config->view->new );
52     $class->init_done(1);
53
54 }
55
56 sub handler {
57
58     # See Maypole::Workflow before trying to understand this.
59     my ( $class, $req ) = @_;
60     $class->init unless $class->init_done;
61     my $r = bless { config => $class->config }, $class;
62     $r->get_request($req);
63     $r->parse_location();
64     my $status = $r->handler_guts();
65     return $status unless $status == OK;
66     $r->send_output;
67     return $status;
68 }
69
70 # The root of all evil
71 sub handler_guts {
72     my $r = shift;
73     $r->model_class( $r->config->model->class_of( $r, $r->{table} ) );
74     my $applicable = $r->is_applicable;
75     unless ( $applicable == OK ) {
76
77         # It's just a plain template
78         delete $r->{model_class};
79         $r->{path} =~ s{/$}{};    # De-absolutify
80         $r->template( $r->{path} );
81     }
82
83     # We authenticate every request, needed for proper session management
84     my $status;
85     eval { $status = $r->call_authenticate };
86     if ( my $error = $@ ) {
87         $status = $r->call_exception($error);
88         if ( $status != OK ) {
89             warn "caught authenticate error: $error";
90             return $r->debug ? $r->view_object->error( $r, $error ) : ERROR;
91         }
92     }
93     if ( $r->debug and $status != OK and $status != DECLINED ) {
94         $r->view_object->error( $r,
95             "Got unexpected status $status from calling authentication" );
96     }
97     return $status unless $status == OK;
98
99     # We run additional_data for every request
100     $r->additional_data;
101     if ( $applicable == OK ) {
102         eval { $r->model_class->process($r) };
103         if ( my $error = $@ ) {
104             $status = $r->call_exception($error);
105             if ( $status != OK ) {
106                 warn "caught model error: $error";
107                 return $r->debug ? $r->view_object->error( $r, $error ) : ERROR;
108             }
109         }
110     }
111     if ( !$r->{output} ) {    # You might want to do it yourself
112         eval { $status = $r->view_object->process($r) };
113         if ( my $error = $@ ) {
114             $status = $r->call_exception($error);
115             if ( $status != OK ) {
116                 warn "caught view error: $error" if $r->debug;
117                 return $r->debug ? $r->view_object->error( $r, $error ) : ERROR;
118             }
119         }
120         return $status;
121     }
122     else { return OK; }
123 }
124
125 sub is_applicable {
126     my $self   = shift;
127     my $config = $self->config;
128     $config->ok_tables || $config->ok_tables( $config->display_tables );
129     $config->ok_tables( { map { $_ => 1 } @{ $config->ok_tables } } )
130       if ref $config->ok_tables eq "ARRAY";
131     warn "We don't have that table ($self->{table})"
132       if $self->debug
133       and not $config->ok_tables->{ $self->{table} };
134     return DECLINED() unless exists $config->ok_tables->{ $self->{table} };
135
136     # Does the action method exist?
137     my $cv = $self->model_class->can( $self->{action} );
138     warn "We don't have that action ($self->{action})"
139       if $self->debug and not $cv;
140     return DECLINED() unless $cv;
141
142     # Is it exported?
143     $self->{method_attribs} = join " ", attributes::get($cv);
144     do {
145         warn "$self->{action} not exported" if $self->debug;
146         return DECLINED();
147     } unless $self->{method_attribs} =~ /\bExported\b/i;
148     return OK();
149 }
150
151 sub call_authenticate {
152     my $self = shift;
153
154     # Check if we have a model class
155     if ( $self->{model_class} ) {
156         return $self->model_class->authenticate($self)
157           if $self->model_class->can("authenticate");
158     }
159     return $self->authenticate($self);   # Interface consistency is a Good Thing
160 }
161
162 sub call_exception {
163     my $self = shift;
164     my ($error) = @_;
165
166     # Check if we have a model class
167     if (   $self->{model_class}
168         && $self->model_class->can('exception') )
169     {
170         my $status = $self->model_class->exception( $self, $error );
171         return $status if $status == OK;
172     }
173     return $self->exception($error);
174 }
175
176 sub additional_data { }
177
178 sub authenticate { return OK }
179
180 sub exception { return ERROR }
181
182 sub parse_path {
183     my $self = shift;
184     $self->{path} ||= "frontpage";
185     my @pi = split /\//, $self->{path};
186     shift @pi while @pi and !$pi[0];
187     $self->{table}  = shift @pi;
188     $self->{action} = shift @pi;
189     $self->{args}   = \@pi;
190 }
191
192 =head1 NAME
193
194 Maypole - MVC web application framework
195
196 =head1 SYNOPSIS
197
198 See L<Maypole>.
199
200 =head1 DESCRIPTION
201
202 A large number of web programming tasks follow the same sort of pattern:
203 we have some data in a datasource, typically a relational database. We
204 have a bunch of templates provided by web designers. We have a number of
205 things we want to be able to do with the database - create, add, edit,
206 delete records, view records, run searches, and so on. We have a web
207 server which provides input from the user about what to do. Something in
208 the middle takes the input, grabs the relevant rows from the database,
209 performs the action, constructs a page, and spits it out.
210
211 Maypole aims to be the most generic and extensible "something in the
212 middle" - an MVC-based web application framework.
213
214 An example would help explain this best. You need to add a product
215 catalogue to a company's web site. Users need to list the products in
216 various categories, view a page on each product with its photo and
217 pricing information and so on, and there needs to be a back-end where
218 sales staff can add new lines, change prices, and delete out of date
219 records. So, you set up the database, provide some default templates
220 for the designers to customize, and then write an Apache handler like
221 this:
222
223     package ProductDatabase;
224     use base 'Maypole::Application';
225     __PACKAGE__->set_database("dbi:mysql:products");
226     ProductDatabase->config->uri_base = "http://your.site/catalogue/";
227     ProductDatabase::Product->has_a("category" => ProductDatabase::Category); 
228     # ...
229
230     sub authenticate {
231         my ($self, $request) = @_;
232         return OK if $request->{ar}->get_remote_host() eq "sales.yourcorp.com";
233         return OK if $request->{action} =~ /^(view|list)$/;
234         return DECLINED;
235     }
236     1;
237
238 You then put the following in your Apache config:
239
240     <Location /catalogue>
241         SetHandler perl-script
242         PerlHandler ProductDatabase
243     </Location>
244
245 And copy the templates found in F<templates/factory> into the
246 F<catalogue/factory> directory off the web root. When the designers get
247 back to you with custom templates, they are to go in
248 F<catalogue/custom>. If you need to do override templates on a
249 database-table-by-table basis, put the new template in
250 F<catalogue/I<table>>. 
251
252 This will automatically give you C<add>, C<edit>, C<list>, C<view> and
253 C<delete> commands; for instance, a product list, go to 
254
255     http://your.site/catalogue/product/list
256
257 For a full example, see the included "beer database" application.
258
259 =head1 HOW IT WORKS
260
261 There's some documentation for the workflow in L<Maypole::Workflow>,
262 but the basic idea is that a URL part like C<product/list> gets
263 translated into a call to C<ProductDatabase::Product-E<gt>list>. This
264 propagates the request with a set of objects from the database, and then 
265 calls the C<list> template; first, a C<product/list> template if it
266 exists, then the C<custom/list> and finally C<factory/list>. 
267
268 If there's another action you want the system to do, you need to either
269 subclass the model class, and configure your class slightly differently:
270
271     package ProductDatabase::Model;
272     use base 'Maypole::Model::CDBI';
273
274     sub supersearch :Exported {
275         my ($self, $request) = @_;
276         # Do stuff, get a bunch of objects back
277         $r->objects(\@objects);
278         $r->template("template_name");
279     }
280
281 Then your top-level application package should change the model class:
282 (Before calling C<setup>)
283
284     ProductDatabase->config->model("ProductDatabase::Model");
285
286 (The C<:Exported> attribute means that the method can be called via the
287 URL C</I<table>/supersearch/...>.)
288
289 Alternatively, you can put the method directly into the specific model
290 class for the table:
291
292     sub ProductDatabase::Product::supersearch :Exported { ... }
293
294 By default, the view class uses Template Toolkit as the template
295 processor, and the model class uses C<Class::DBI>; it may help you to be
296 familiar with these modules before going much further with this,
297 although I expect there to be other subclasses for other templating
298 systems and database abstraction layers as time goes on. The article at
299 C<http://www.perl.com/pub/a/2003/07/15/nocode.html> is a great
300 introduction to the process we're trying to automate.
301
302 =head1 USING MAYPOLE
303
304 You should probably not use Maypole directly. Maypole is an abstract
305 class which does not specify how to communicate with the outside world.
306 The most popular subclass of Maypole is L<Apache::MVC>, which interfaces
307 the Maypole framework to Apache mod_perl; another important one is
308 L<CGI::Maypole>. However, if you just don't care, use Maypole::Application,
309 and it will choose the right one for you.
310
311 If you are implementing Maypole subclasses, you need to provide at least
312 the C<parse_location> and C<send_output> methods. You may also want to
313 provide C<get_request> and C<get_template_root>. See the
314 L<Maypole::Workflow> documentation for what these are expected to do.
315
316 =cut
317
318 sub get_template_root { "." }
319 sub get_request       { }
320
321 sub parse_location {
322     die "Do not use Maypole directly; use Apache::MVC or similar";
323 }
324
325 sub send_output {
326     die "Do not use Maypole directly; use Apache::MVC or similar";
327 }
328
329 =head1 SEE ALSO
330
331 There's more documentation, examples, and a wiki at the Maypole web site:
332
333 http://maypole.simon-cozens.org/
334
335 L<Maypole::Application>,L<Apache::MVC>, L<CGI::Maypole>.
336
337 =head1 MAINTAINER
338
339 Sebastian Riedel, c<sri@oook.de>
340
341 =head1 AUTHOR
342
343 Simon Cozens, C<simon@cpan.org>
344
345 =head1 THANK YOU
346
347 Danijel Milicevic, Jesse Scheidlower, Jody Belka, Marcus Ramberg,
348 Mickael Joanne, Simon Flack, Veljko Vidovic and all the others who've helped.
349
350 =head1 LICENSE
351
352 You may distribute this code under the same terms as Perl itself.
353
354 =cut
355
356 1;