From 2c2a72adcc01c1260b410103fa6bab06623f04be Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Thu, 8 Apr 2004 15:09:29 +0000 Subject: [PATCH] Loads more documentation hacking. git-svn-id: http://svn.maypole.perl.org/Maypole/trunk@125 48953598-375a-da11-a14b-00016c27c3ee --- doc/About.pod | 5 +- doc/Beer.pod | 179 +++++++++++++++++++++++++++++++++++++++++++++++- doc/Model.pod | 98 ++++++++++++++++++++++++++ doc/Request.pod | 18 +++++ 4 files changed, 297 insertions(+), 3 deletions(-) diff --git a/doc/About.pod b/doc/About.pod index 87ef195..b5cf3e7 100644 --- a/doc/About.pod +++ b/doc/About.pod @@ -92,8 +92,7 @@ you need to write for a simple database front-end: package BeerDB; use base 'Apache::MVC'; - use Class::DBI::Loader::Relationship; - BeerDB->set_database("dbi:SQLite:t/beerdb.db"); + BeerDB->setup("dbi:SQLite:t/beerdb.db"); BeerDB->config->{uri_base} = "http://localhost/beerdb/"; BeerDB->config->{rows_per_page} = 10; BeerDB->config->{display_tables} = [qw[beer brewery pub style]]; @@ -104,6 +103,8 @@ you need to write for a simple database front-end: integer => [qw/style brewery score/], date => [ qw/date/], ); + + use Class::DBI::Loader::Relationship; BeerDB->config->{loader}->relationship($_) for ( "a brewery produces beers", "a style defines beers", diff --git a/doc/Beer.pod b/doc/Beer.pod index fa8fe84..4c4093f 100644 --- a/doc/Beer.pod +++ b/doc/Beer.pod @@ -1,10 +1,187 @@ =head1 The Beer Database, Twice We briefly introduced the "beer database" example in the L -material; +material, where we presented its driver class as a fait accompli. Where +did all that code come from, and what does it actually mean? =head2 The big beer problem +I have a seriously bad habit. This is not the beer problem; this is a +programming problem. The bad habit is that when I approach a problem I +want to solve, I get sidetracked deeper and deeper trying to solve more +and more generic problems, and then, satisfied with solving the generic +problem, I never get around to solving the specific problem. I always +write libraries for people writing libraries, and never write +applications. + +The thing with really good beer is that it commands you to drink more of +it, and then by the morning you can't remember whether it was any good +or not. After buying several bottles of some random central African +lager on a dim recollection that it was really good and having it turn +out to be abysmal, this really became a problem. If only I could have a +database that I updated every time I buy a new beer, I'd be able to tell +whether or not I should buy that Lithuanian porter again or whether it +would be quicker just to flush my money down the toilet and cut out the +middle-man. + +The only problem with databases on Unix is that there isn't really a +nice way to get data into them. There isn't really a Microsoft Access +equivalent which can put a simple forms-based front-end onto an +arbitrary database, and if there is, I either didn't like it or couldn't +find it, and after a few brews, you really don't want to be trying to type +in your tasting notes in raw SQL. + +So you see a generic problem arising out of a specific problem here. I +didn't want to solve the specific problem of the beer database, because +I'd already had another idea for a database that needed a front-end. So +for two years, I sat on this great idea of having a database of tasting +notes for beer. I even bought that damned African beer again. Enough was +enough. I wrote Maypole. + =head2 The easy way +The first Maypole application was the beer database. We've already met +it; it looks like this. + + package BeerDB; + use base 'Apache::MVC'; + BeerDB->set_database("dbi:SQLite:t/beerdb.db"); + BeerDB->config->{uri_base} = "http://localhost/beerdb/"; + BeerDB->config->{rows_per_page} = 10; + BeerDB->config->{display_tables} = [qw[beer brewery pub style]]; + BeerDB::Brewery->untaint_columns( printable => [qw/name notes url/] ); + BeerDB::Style->untaint_columns( printable => [qw/name notes/] ); + BeerDB::Beer->untaint_columns( + printable => [qw/abv name price notes/], + integer => [qw/style brewery score/], + date => [ qw/date/], + ); + + use Class::DBI::Loader::Relationship; + BeerDB->config->{loader}->relationship($_) for ( + "a brewery produces beers", + "a style defines beers", + "a pub has beers on handpumps"); + 1; + +Now, we can split this into four sections. Let's look at them one +at a time. + +=head3 Driver setup + +Here's the first section: + + package BeerDB; + use base 'Apache::MVC'; + BeerDB->setup("dbi:SQLite:t/beerdb.db"); + +This is actually all you need for a functional database front-end. Everything +else is configuration details. This says three things: we're an application +called C. This package is called the B, because +it's a relatively small class which defines how the whole application is +going to run. + +The second line says that our front-end is going to be C, +which is the Apache C based version of Maypole; there's also +a CGI version, C, and a command-line version for +debugging, C, but C is usually the one you +want. + +Thirdly we're going to need to set up our database with the given DBI +connection string. Now the core of Maypole itself doesn't know about +DBI; as we explained in L, this argument is passed to our +model class wholesale. As we haven't said anything about a model +class, we get the default one, C, which takes a +DBI connect string. So this one line declares that we're using a C +model class and it sets up the database for us. In the same way, we +don't say that we want a particular view class, so we get the default +C. + +At this point, everything is in place; we have our driver class, it uses +a front-end, we have a model class and a view class, and we have a data +source. + +=head3 Application configuration + +The next of our four sections is the configuration for the application itself. + + BeerDB->config->{uri_base} = "http://localhost/beerdb/"; + BeerDB->config->{rows_per_page} = 10; + BeerDB->config->{display_tables} = [qw[beer brewery pub style]]; + +Maypole provides a method called C which returns a hash reference +of the application's whole configuration. We can use this to set some +parameters; the C is used as the canonical URL of the base +of this application, and Maypole uses it to construct links. + +By defining C, we say that any listings we do with the +C and C templates should be arranged in sets of pages, with +a maximum of 10 items on each page. If we didn't declare that, C +would try to put all the objects on one page, which could well be bad. + +Finally, we declare which tables we want our Maypole front-end to +reference. If you remember from the schema, there's a table called +C which acts as a linking table in a many-to-many relationship +between the C and C tables. As it's only a linking table, we +don't want people poking with it directly, so we exclude it from the +list of C. + +=head3 Editability + +The next section is the following set of lines: + + BeerDB::Brewery->untaint_columns( printable => [qw/name notes url/] ); + BeerDB::Style->untaint_columns( printable => [qw/name notes/] ); + BeerDB::Beer->untaint_columns( + printable => [qw/abv name price notes/], + integer => [qw/style brewery score/], + date => [ qw/date/], + ); + +As explained in L, this is an set of instructions to +C regarding how the given columns should be edited. +If we didn't have this section, we'd be able to view and delete records, +but adding and editing them wouldn't work. It took me ages to work that +one out. + +=head3 Relationships + +Finally, we want to explain to Maypole how the various tables relate to +each other. This is done so that, for instance, when displaying a beer, +the brewery does not appear as an integer like "2" but as the name of +the brewery from the C table with an ID of 2. + +The usual C way to do this involves the C and +C methods, but I can never remember how to use them, so I came +up with the C module; this was another +yak that needed shaving on the way to the beer database: + + use Class::DBI::Loader::Relationship; + BeerDB->config->{loader}->relationship($_) for ( + "a brewery produces beers", + "a style defines beers", + "a pub has beers on handpumps"); + 1; + +C acts on a C object and +defines relationships between tables in a fairly free-form style. +The equivalent in ordinary C would be: + + BeerDB::Brewery->has_many(beers => "BeerDB::Beer"); + BeerDB::Beer->has_a(brewery => "BeerDB::Brewery"); + BeerDB::Style->has_many(beers => "BeerDB::Beer"); + BeerDB::Beer->has_a(style => "BeerDB::Style"); + + BeerDB::Handpump->has_a(beer => "BeerDB::Beer"); + BeerDB::Handpump->has_a(pub => "BeerDB::Pub"); + BeerDB::Pub->has_many(beers => [ BeerDB::Handpump => 'beer' ]); + BeerDB::Beer->has_many(pubs => [ BeerDB::Handpump => 'pub' ]); + +Maypole's default templates will use this information to display, for +instance, a list of a brewery's beers on the brewery view page. + +This is the complete beer database application; Maypole's default templates +and the actions in the view class do the rest. But what if we want to a +little more. How would we begin to extend this application? + =head2 The hard way diff --git a/doc/Model.pod b/doc/Model.pod index c5f5b18..bc54ffd 100644 --- a/doc/Model.pod +++ b/doc/Model.pod @@ -4,8 +4,106 @@ =head2 Maypole::Model::CDBI +Maypole model classes are pretty evil. The very first thing a Maypole +model class will do in a Maypole application is to cause a load of +table-based classes to come into being, and then assimilate them. + +What I mean by this is that when you set up a Maypole application, in +your driver class, you'll say something like this: + + BeerDB->setup("dbi:mysql:beerdb"); + +C is a Maypole method, and it hands its parameter to the model +class. In our case, the model class is a DBI connect string, because +that's what C, the C-based model +expects. C has a method called C +that creates all the C table classes after connecting to the +database with that connect string. It does this by using +C, a utility module which asks a database +about its schema and sets up classes such as C to inherit +from C. This is just doing automatically what we did +manually in our examples above. + +Now it gets interesting. The names of these classes are stashed away in +the application's configuration, and then Maypole forcibly has these +classes inherit from the model class. Our C now inherits +both from C and C. + +This is useful for two reasons. The first reason is that +C is stuffed full of C goodies that +make writing Maypole applications a lot easier: + + package Maypole::Model::CDBI; + use base qw(Maypole::Model::Base Class::DBI); + use Class::DBI::AsForm; + use Class::DBI::FromCGI; + use Class::DBI::Loader; + use Class::DBI::AbstractSearch; + use Class::DBI::Plugin::RetrieveAll; + use Class::DBI::Pager; + +We'll meet most of these goodies in L, where we +explain how C works. + +The second reason why we want our table classes to inherit from +C is because C provides a useful set of +default actions. So what's an action, and why are they useful? + =head2 Extending a model class with actions +Maypole operates primarily by turning URLs into method calls on a model +class. All that the model stage of Maypole's operation does, when it +comes down to it, is maintain a mapping of tables to classes, and +despatch a HTTP request to a method call on the relevant table class. This +means that if you request a URL such as + + http://localhost/beerdb/brewery/delete/20 + +Maypole's first stage of operation is to turn that into +Cdelete(20)>. Now, it's slightly more complex than +that. Firstly because it doesn't actually pass the parameter C<20>, but +it passes an object representing row 20 in the database, but we can +gloss over that for the second. No, the real issue is that Maypole does +not allow you to call any method in the table class; that would be +somewhat insecure. + +Instead, Maypole makes a distinction between the kind of methods that +only the class itself and other Perl code can call, and the kind of +methods that anyone can call from a URL. This latter set of methods are +called B methods, and exporting is done with by means of Perl +attributes. You define a method to be exported like so: + + sub drink :Exported { + +This will allow the user to access C over the web. +An exported method accompanied with a template to render its output is +sometimes called an B. + +Maypole model classes like C come with a +relatively handy set of actions which are all you need to set up a CRUD +database front-end: viewing a row in a database, editing it, adding a +new one, deleting, and so on. The most important thing about Maypole, +though, is that it doesn't stop there. You can add your own. + +For instance, in our beer database application, we could create a +C package, and put some additional actions in there. + + package BeerDB::Beer; + sub top_five :Exported { + my ($class, $r) = @_; + $r->{objects} = [ ($r->retrieve_all_sorted_by("score"))[-5..-1] ]; + } + +Our action is called as a class method with the Maypole request object. +It uses the C module's +C mixin to get the top five scoring beers, and +puts these in the C slot of the request of object. Next, the +view class will come along and process the C template with +these five beers. + +We'll look more at how to put together actions in the +L chapter and our case studies. + =head2 What Maypole wants from a model =head2 Building your own model class diff --git a/doc/Request.pod b/doc/Request.pod index 80e4037..96f7c55 100644 --- a/doc/Request.pod +++ b/doc/Request.pod @@ -50,6 +50,24 @@ These hacks deal primarily with the presentation of data to the user, modifying the C template or changing the way that the results of particular actions are displayed. +=head3 Null Action + +You need an "action" which doesn't really do anything, but just formats +up a template. + +B: There are two ways to do this, depending on what precisely +you need. If you just need to display a template, C +style, with no Maypole objects in it, then you don't need to write any +code; just create your template, and it will be available in the usual +way. + +If, on the other hand, you want to display some data, and what you're +essentially doing is a variant of the C action, then you need to +ensure that you have an exported action, as described in +L: + + sub my_view :Exported { } + =head3 Template Switcheroo An action doesn't have any data of its own to display, but needs to display -- 2.39.2