]> git.decadent.org.uk Git - maypole.git/blob - lib/Maypole.pm
9a86489b8127d7cbb0914409d88ca0a415948415
[maypole.git] / lib / Maypole.pm
1 package Maypole;
2 use base qw(Class::Accessor Class::Data::Inheritable);
3 use attributes ();
4 use UNIVERSAL::require;
5 use Apache::Constants ":common";
6 use strict;
7 use warnings;
8 our $VERSION = "1.1";
9 __PACKAGE__->mk_classdata($_) for qw( config init_done view_object );
10 __PACKAGE__->mk_accessors ( qw( ar params query objects model_class
11 args action template ));
12 __PACKAGE__->config({});
13 __PACKAGE__->init_done(0);
14
15
16 sub setup {
17     my $calling_class = shift;
18     $calling_class = ref $calling_class if ref $calling_class;
19     {
20       no strict 'refs';
21       # Naughty.
22       *{$calling_class."::handler"} = sub { Maypole::handler($calling_class, @_) };
23     }
24     my $config = $calling_class->config;
25     $config->{model} ||= "Maypole::Model::CDBI";
26     $config->{model}->require;
27     $config->{model}->setup_database($config, $calling_class, @_);
28     for my $subclass (@{$config->{classes}}) {
29         no strict 'refs';
30         unshift @{$subclass."::ISA"}, $config->{model};
31         $config->{model}->adopt($subclass)
32            if $config->{model}->can("adopt");
33     }
34 }
35
36 sub init {
37     my $class = shift;
38     my $config = $class->config;
39     $config->{view}  ||= "Maypole::View::TT";
40     $config->{view}->require;
41     $config->{display_tables} ||= [ @{$class->config->{tables}} ];
42     $class->view_object($class->config->{view}->new);
43     $class->init_done(1);
44
45 }
46
47 sub handler {
48     # See Maypole::Workflow before trying to understand this.
49     my $class = shift;
50     $class->init unless $class->init_done;
51     my $r = bless { config => $class->config }, $class;
52     $r->get_request();
53     $r->parse_location();
54
55     $r->model_class($r->config->{model}->class_of($r, $r->{table}));
56     my $status = $r->is_applicable;
57     if ($status == OK) { 
58         $status = $r->call_authenticate;
59         return $status unless $status == OK;
60         $r->additional_data();
61     
62         $r->model_class->process($r);
63     } else { 
64         # Otherwise, it's just a plain template.
65         $r->call_authenticate; # No harm in it
66         delete $r->{model_class};
67         $r->{path} =~ s{/}{}; # De-absolutify
68         $r->template($r->{path});
69     }
70     return $r->view_object->process($r);
71 }
72
73 sub is_applicable {
74     my $self = shift;
75     my $config = $self->config;
76     $config->{ok_tables} = {map {$_ => 1} @{$config->{display_tables}}};
77     #warn "We don't have that table ($self->{table})"
78     #    unless $config->{ok_tables}{$self->{table}};
79     return DECLINED() unless exists $config->{ok_tables}{$self->{table}};
80
81     # Does the action method exist?
82     my $cv = $self->model_class->can($self->{action});
83     #warn "We don't have that action ($self->{action})" unless $cv;
84     return DECLINED() unless $cv;
85
86     # Is it exported?
87     $self->{method_attribs} = join " ", attributes::get($cv);
88     do { # warn "$self->{action} not exported";
89     return DECLINED() 
90      } unless $self->{method_attribs} =~ /\bExported\b/i;
91     return OK();
92 }
93
94 sub call_authenticate {
95     my $self = shift;
96     return $self->model_class->authenticate($self) if 
97         $self->model_class->can("authenticate"); 
98     return $self->authenticate($self); # Interface consistency is a Good Thing
99 }
100
101 sub additional_data {}
102
103 sub authenticate { return OK }
104
105 =head1 NAME
106
107 Maypole - MVC web application framework
108
109 =head1 SYNOPSIS
110
111 See L<Maypole>.
112
113 =head1 DESCRIPTION
114
115 A large number of web programming tasks follow the same sort of pattern:
116 we have some data in a datasource, typically a relational database. We
117 have a bunch of templates provided by web designers. We have a number of
118 things we want to be able to do with the database - create, add, edit,
119 delete records, view records, run searches, and so on. We have a web
120 server which provides input from the user about what to do. Something in
121 the middle takes the input, grabs the relevant rows from the database,
122 performs the action, constructs a page, and spits it out.
123
124 Maypole aims to be the most generic and extensible "something in the
125 middle" - an MVC-based web application framework.
126
127 An example would help explain this best. You need to add a product
128 catalogue to a company's web site. Users need to list the products in
129 various categories, view a page on each product with its photo and
130 pricing information and so on, and there needs to be a back-end where
131 sales staff can add new lines, change prices, and delete out of date
132 records. So, you set up the database, provide some default templates
133 for the designers to customize, and then write an Apache handler like
134 this:
135
136     package ProductDatabase;
137     use base 'Apache::MVC';
138     __PACKAGE__->set_database("dbi:mysql:products");
139     ProductDatabase->config->{uri_base} = "http://your.site/catalogue/";
140     ProductDatabase::Product->has_a("category" => ProductDatabase::Category); 
141     # ...
142
143     sub authenticate {
144         my ($self, $request) = @_;
145         return OK if $request->{ar}->get_remote_host() eq "sales.yourcorp.com";
146         return OK if $request->{action} =~ /^(view|list)$/;
147         return DECLINED;
148     }
149     1;
150
151 You then put the following in your Apache config:
152
153     <Location /catalogue>
154         SetHandler perl-script
155         PerlHandler ProductDatabase
156     </Location>
157
158 And copy the templates found in F<templates/factory> into the
159 F<catalogue/factory> directory off the web root. When the designers get
160 back to you with custom templates, they are to go in
161 F<catalogue/custom>. If you need to do override templates on a
162 database-table-by-table basis, put the new template in
163 F<catalogue/I<table>>. 
164
165 This will automatically give you C<add>, C<edit>, C<list>, C<view> and
166 C<delete> commands; for instance, a product list, go to 
167
168     http://your.site/catalogue/product/list
169
170 For a full example, see the included "beer database" application.
171
172 =head1 HOW IT WORKS
173
174 There's some documentation for the workflow in L<Maypole::Workflow>,
175 but the basic idea is that a URL part like C<product/list> gets
176 translated into a call to C<ProductDatabase::Product-E<gt>list>. This
177 propagates the request with a set of objects from the database, and then 
178 calls the C<list> template; first, a C<product/list> template if it
179 exists, then the C<custom/list> and finally C<factory/list>. 
180
181 If there's another action you want the system to do, you need to either
182 subclass the model class, and configure your class slightly differently:
183
184     package ProductDatabase::Model;
185     use base 'Maypole::Model::CDBI';
186
187     sub supersearch :Exported {
188         my ($self, $request) = @_;
189         # Do stuff, get a bunch of objects back
190         $r->objects(\@objects);
191         $r->template("template_name");
192     }
193
194 Then your top-level application package should change the model class:
195 (Before calling C<setup>)
196
197     ProductDatabase->config->{model_class} = "ProductDatabase::Model";
198
199 (The C<:Exported> attribute means that the method can be called via the
200 URL C</I<table>/supersearch/...>.)
201
202 Alternatively, you can put the method directly into the specific model
203 class for the table:
204
205     sub ProductDatabase::Product::supersearch :Exported { ... }
206
207 By default, the view class uses Template Toolkit as the template
208 processor, and the model class uses C<Class::DBI>; it may help you to be
209 familiar with these modules before going much further with this,
210 although I expect there to be other subclasses for other templating
211 systems and database abstraction layers as time goes on. The article at
212 C<http://www.perl.com/pub/a/2003/07/15/nocode.html> is a great
213 introduction to the process we're trying to automate.
214
215 =head1 USING MAYPOLE
216
217 You should probably not use Maypole directly. Maypole is an abstract
218 class which does not specify how to communicate with the outside world.
219 The most popular subclass of Maypole is L<Apache::MVC>, which interfaces
220 the Maypole framework to Apache mod_perl.
221
222 If you are implementing Maypole subclasses, you need to provide at least 
223 the C<get_request> and C<parse_location> methods. See the
224 L<Maypole::Workflow> documentation for what these are expected to do.
225
226 =cut
227
228 sub get_request { die "Do not use Maypole directly; use Apache::MVC or similar" }
229 sub parse_location { die "Do not use Maypole directly; use Apache::MVC or similar" }
230
231 =head1 AUTHOR
232
233 Simon Cozens, C<simon@cpan.org>
234
235 =head1 LICENSE
236
237 You may distribute this code under the same terms as Perl itself.
238
239 =cut
240
241 1;
242