The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Maypole::Manual::Inheritance - structure of a Maypole application

DESCRIPTION

Discusses the inheritance structure of a basic and a more advanced Maypole application.

CONVENTIONS

inheritance
        +
        |
     +-   -+
        |
        +
        
notes
    target *-------- note about the target
association
    source ------> target

Structure of a standard Maypole application

A minimal Maypole application (such as the Beer database example from the Maypole synopsis) consists of a custom driver class (BeerDB.pm), a set of auto-generated model classes, and a view class:

           THE DRIVER
                                          +------- init() is a factory method,
                   1      Maypole         |           it sets up the view
   Maypole::Config <----- config();       |              classes
   model();               init(); *-------+                           THE VIEW
    |                     view_object(); -------+
    |    +--------------* setup();              |      Maypole::View::Base
    |    |                   +                  |              +
    |    |                   |                  |     1        |
    |    |    PLUGINS    Apache::MVC *-----+    +-----> Maypole::View::TT
    |    |       +           +             |             (or another view class)
    |    |       |           |             |
    |    |       +-----+-----+             |
    |    |             |                   |
    |    |           BeerDB                +----- or CGI::Maypole
    |    |                                         or MasonX:::Maypole
    |    |
    |   setup() is a factory method,
    |     it sets up the model
    |         classes
    |
    |                                             THE MODEL
    |
    |  Maypole::Model::Base    Class::DBI
    |             +             +      +
    |             |             |      |
    +-------> Maypole::Model::CDBI   Class::DBI::<db_driver>
                      +                     +
                      |                     |
           +------------+--------+-------+---------+
           |            |        |       |         |
       BeerDB::Pub      |   BeerDB::Beer | BeerDB::Brewery
       beers();         |   pubs();      | beers();
                        |   brewery();   |
                        |   style();     |
          BeerDB::Handpump               |
          pub();                      BeerDB::Style
          beer();                     beers();

What about Maypole::Application - loading plugins

The main job of Maypole::Application is to insert the plugins into the hierarchy. It is also the responsibility of Maypole::Application to decide which frontend to use. It builds the list of plugins, then pushes them onto the driver's @ISA, then pushes the frontend onto the end of the driver's @ISA. So method lookup first searches all the plugins, before searching the frontend and finally Maypole itself.

From Maypole 2.11, Maypole::Application makes no appearance in the inheritance structure of a Maypole application. (In prior versions, Maypole::Application would make itself inherit the plugins, and then insert itself in the hierarchy, but this was unnecessary).

Who builds the model?

First, remember we are talking about the standard, unmodified Maypole here. It is possible, and common, to override some or all of this stage and build a customised model. See below - An advanced Maypole application - for one approach. Also, see Maypole's setup_model() method.

The standard model is built in 3 stages.

First, Maypole::setup_model calls setup_database on the Maypole model class, in this case Maypole::Model::CDBI. setup_database then uses Class::DBI::Loader to autogenerate individual Class::DBI classes for each of the tables in the database (BeerDB::Beer, BeerDB::Pub etc). Class::DBI::Loader identifies the appropriate Class::DBI subclass and inserts it into each of these table classes' @ISA ( Class::DBI::<db_driver> in the diagrams)..

Next, Maypole::setup unshifts Maypole::Model::CDBI onto the @ISA array of each of these classes.

Finally, the relationships among these tables are set up. Either do this manually, using the standard Class::DBI syntax for configuring table relationships, or try Class::DBI::Relationship (which you can use via Maypole::Plugin::Relationship). If you use the plugin, you need to set up the relationships configuration before calling setup(). Be aware that some people like the convenience of Class::DBI::Relationship, others dislike the abstraction. YMMV.

An advanced Maypole application

We'll call it BeerDB2.

Maypole is a framework, and you can replace different bits as you wish. So what follows is one example of good practice, other people may do things differently.

We assume this application is being built from the ground up, but it will often be straightforward to adapt an existing Class::DBI application to this general model.

The main idea is that the autogenerated Maypole model is used as a layer on top of a separate Class::DBI model. I am going to refer to this model as the 'Offline' model, and the Maypole classes as the 'Maypole' model. The idea is that the Offline model can (potentially or in actuality) be used as part of another application, perhaps a command line program or a cron script, whatever. The Offline model does not know about the Maypole model, whereas the Maypole model does know about the Offline model.

Let's call the offline model OfflineBeer. As a traditional Class::DBI application, individual table classes in this model will inherit from a common base (OfflineBeer), which inherits from Class::DBI).

One advantage of this approach is that you can still use Maypole's autogenerated model. Another is that you do not mix online and offline code in the same packages.

Building it

Build a driver in a similar way as for the basic app, calling setup() after setting up all the configuration.

It is a good habit to use a custom Maypole model class for each application, as it's a likely first target for customisation. Start it like this:

    package BeerDB2::Maypole::Model;
    use strict;
    use warnings;
    use base 'Maypole::Model::CDBI';
    1;
    

You can add methods which should be shared by all table classes to this package as and when required.

Configure it like this, before the setup() call in the driver class:

    # in package BeerDB2
    __PACKAGE__->config->model('BeerDB2::Maypole::Model');
    __PACKAGE__->setup;

The setup() call will ensure your custom model is loaded via require.

Note: by default, this will create Maypole/CDBI classes for all the tables in the database. You can control this by passing options for Class::DBI::Loader in the call to setup().

For each class in the model, you need to create a separate file. So for BeerDB2::Beer, you would write:

    package BeerDB2::Beer;
    use strict;
    use warnings;
    use base 'OfflineBeer::Beer';
    1;
    

From Maypole 2.11, this package will be loaded automatically during setup(), and BeerDB2::Maypole::Model is unshifted onto it's @ISA.

Configure relationships either in the individual OfflineBeer::* classes, or else all together in OfflineBeer itself i.e. not in the Maypole model. This way, you only define the relationships in one place.

The resulting model looks like this:

                                       Class::DBI
    MAYPOLE 'MODEL'                       |
                                          |
   Maypole::Model::Base                   |
           +                              |
           |       +-----------------+----+-----------------+
           |       |                 |                      |
           |       |                 |                      |
     Maypole::Model::CDBI            |                      |     OFFLINE
             +                       |                      |        MODEL
             |                       |                      |
     BeerDB2::Maypole::Model  Class::DBI::<db_driver>  OfflineBeer
       +                             +                      +
       |                             |                      |
       +-----------------------------+                      |
       |                                                    |
       +--- BeerDB2::Pub --------+ OfflineBeer::Pub --------+
       |                           beers();                 |
       |                                                    |
       |                           OfflineBeer::Handpump ---+
       |                           beer();                  |
       |                           pub();                   |
       |                                                    |
       +--- BeerDB2::Beer -------+ OfflineBeer::Beer -------+
       |                           pubs();                  |
       |                           brewery();               |
       |                           style();                 |
       |                                                    |
       +--- BeerDB2::Style ------+ OfflineBeer::Style ------+
       |                           beers();                 |
       |                                                    |
       +--- BeerDB2::Brewery ----+ OfflineBeer::Brewery ----+
                                   beers();

Features

1. Non-Maypole applications using the Offline model are completely isolated from the Maypole application, and need not know it exists at all.

2. Methods defined in the Maypole table classes, override methods defined in the Offline table classes, because BeerDB2::Maypole::Model was unshifted onto the beginning of each Maypole table class's @ISA. Perl's depth first, left-to-right method lookup from e.g. BeerDB2::Beer starts in BeerDB2::Beer, then BeerDB2::Maypole::Model, Maypole::Model::CDBI, Maypole::Model::Base, and Class::DBI, before moving on to OfflineBeer::Beer and finally OfflineBeer.

CAVEAT - if your Offline model overrides Class::DBI methods, these methods will not be overridden when called from the Maypole application, because the Maypole model provides an alternative path to Class::DBI which is searched first. The solution is to place such methods in a separate package, e.g. OfflineBeer::CDBI. Place this first in the @ISA of both BeerDB2::Maypole::Model and OfflineBeer. Note that OfflineBeer::CDBI does not itself need to inherit from Class::DBI.

3. Methods defined in the Maypole model base class (BeerDB2::Maypole::Model), override methods in the individual Offline table classes, and in the Offline model base class (Offline).

4. Relationships defined in the Offline classes are inherited by the Maypole model.

5. The Maypole model has full access to the underlying Offline model.

Theory

This layout illustrates more clearly why the Maypole model may be thought of as part of the controller, rather than part of the model of MVC. Its function is to mediate web requests, translating them into method calls on the Offline model, munging the results, and returning them via the Maypole request object.

Another way of thinking about it is that Maypole implements a two-layer controller. The first layer translates a raw request into a single method call on the Maypole model layer, which then translates that call into one or more calls on the underlying model.

Whatever label you prefer to use, this approach provides for clear separation of concerns between the underlying model and the web/user interface, and that's what it's all about.

Advanced applications - building the model by hand ** TODO

- using Maypole::Model::CDBI::Plain or Maypole::FormBuilder::Model::Plain - setup_model() and load_model_subclass() - cutting out all those separate paths to CDBI - they're confusing

Method inheritance ** TODO

More description of Perl's left-to-right, depth-first method lookup, and where it's particularly important in Maypole.

AUTHOR

David Baird, <cpan@riverside-cms.co.uk>

COPYRIGHT & LICENSE

Copyright 2005 David Baird, All Rights Reserved.

This text is free documentation; you can redistribute it and/or modify it under the same terms as the Perl documentation itself.