Name

Gantry::Docs::Tutorial - The Gantry Tutorial

Introduction

Gantry is a mature web framework, released in late 2005 onto an unsuspecting world. For more information on the framework, its features and history, see Gantry::Docs::About.

Here we will explore the basic workings of Gantry by constructing a very simple application. Don't let the simplicity of this example fool you -- this framework has extreme flexibility in delivering applications with web and scripted components. The example in this document is only to get you started.

This document begins by describing a simple one-table management application. It walks through the process of building the application. Then, it shows a tool -- called Bigtop -- which can be used to build the application from a relatively small configuration file. Finally, it shows how to add another table and regenerate the app via Bigtop.

Sample App Description

I'm worried about my wife's address book. There is only one copy and without it, we would lose track of many of our friends and some of our relatives. I want to put my wife's address book into a database, but allow her to use it through a web interface.

Here are the things that Lisa tracks:

name

the name of a person or nuclear family

address

postal address, so we can send toys to the kids etc.

phone

one or more numbers (email addresses are in the margin, but that will have to wait for version 1.1)

This leads to one table:

CREATE SEQUENCE address_seq;
CREATE TABLE address (
    id int4 PRIMARY KEY DEFAULT NEXTVAL( 'address_seq' ),
    name   varchar,
    street varchar,
    city   varchar,
    state  varchar,
    zip    varchar,
    phone  varchar
);

The application needs to show all the addresses in a single table, allow for adding new ones and editing or deleting existing ones. To make it easier to accomodate Lisa's international family and friends, we won't do any validation of the data -- except to make sure she enters some. For example, this will allow her to wedge several numbers (home, cell, etc.) into the phone field.

Hand-writing the Sample App

After creating a directory called Apps-Address, I made a lib subdirectory for the code. (You could use h2xs to help with the initial steps. Or, you could use Bigtop, as I did, see "Using Bigtop" below.)

There are three modules in this application:

Apps::AddressBook

the base module

Apps::AddressBook::Address

the controller for the address table

Apps::AddressBook::Model::address

the object relational mapper

We'll walk through each of these in a subsection, showing the code with commentary interspersed. After our tour I'll show the modules again without the commentary, so you can see how they look when whole, in "Complete Code Listings".

Apps::AddressBook

The job of the base module is to load Gantry. If there was any app specific configuration info, this would be the place to handle it (see below for an example). The base module is also a nice home for code the other modules need to share.

Here is our module (without its documentation but with commentary interspersed):

package Apps::AddressBook;

use strict;

our $VERSION = '0.01';

It begins like any other module...

use Gantry qw{ -Engine=MP13 -TemplateEngine=TT };

our @ISA = ( 'Gantry' );

...but, it uses Gantry with an engine (mod_perl 1.3 in this case) and a template engine (Template Toolkit).

Note that somewhere you need to use Gantry with the -Engine and -TemplateEngine options. You could do that in your httpd.conf or CGI dispatching script. If you do that, reduce the previous two lines to:

use base 'Gantry';

Doing that would add the flexibilty of redeploying the application from one engine to another with absolutely no code changes. Note that wherever you decide to use Gantry with the -Engine and -TemplateEngine flags, you cannot use base, since that pragma has no way to pass import requests to the base module.

use Apps::AddressBook::Address;

1;

For the convenience of future readers, the base module has an explicit use for the single controller Apps::AddressBook::Address (which we will see below). This is purely for documentation.

Gantry.pm handles a set of standard configuration parameters. If you need to handle others, implement an init sub and accessors for them. It's usually easier to dispatch to SUPER for the standard parameters, then handle your app specific ones. For example, an init to catch an smtp host name might look like this:

sub init {
    my ( $self ) = @_;

    # process SUPER's init code
    $self->SUPER::init( );

    $self->smtp_host( $self->fish_conf( 'smtp_host' ) || '' );
} # END init

Using fish_conf has two advantages over a more direct approach like this:

$self->smtp_host( $self->r->dir_config( 'smtp_host' ) || '' );

First, using dir_config ties you to mod_perl. Second, directly fishing from the request object prevents a more general solution, like Gantry::Conf (see Gantry::Conf::Tutorial for how to use that).

Apps::AddressBook::Address

This is the workhorse for this application. It manages the CRUD (create, retrieve, update, and delete) for address book rows. Again, I'll include it a piece at a time with running commentary.

package Apps::AddressBook::Address;

use strict;

use base 'Apps::AddressBook';

It begins like any subclass. Note that it is a subclass of Apps::AddressBook which is itself a subclass of Gantry. The only handler sub is in Gantry.pm (unless you count user authentication, but that's way ahead of our little story about the vulnerable address book with the flowers on the cover).

use Apps::AddressBook::Model::address qw( $ADDRESS );

Each table has a model in the Model namespace with the same name as the table (note the case -- this exactly matches the sql shown in the previous section). The model exports an alias to its full name as $ADDRESS to save us some typing when we use it. It uses uc on the table's name to make the alias more visible.

use Gantry::Plugins::AutoCRUD;

This is the real key to avoiding work. AutoCRUD handles create, update and delete (we'll see retrieval in a minute). This module is more of a mixin than a plugin. It exports four methods to us: do_add, do_edit, do_delete, and form_name. The last is just the name of the template to use for add/edit input. If you don't want the standard form.tt, that comes with Gantry, don't import that method. Instead, implement a method which returns the name of your template file.

In gantry, the handler calls methods named do_* where the star is replaced with a string from the url. So the url for adding an entry to the address book would be something like:

http://somehost.example.com/address/add

where somehost.example.com is our host (or virtual host) and /address/add is the requested page. address is a Location in our apache conf and add becomes do_add, the name of the method to execute. Using the do_ prefix has two advantages. First, since url pieces are used directly, it keeps people from running non-handlers by clever url spoofing. Second, and for our company more importantly, it makes it clear which methods are accessible, and which are not. This aids us when we are modifying a controller. If it starts with do_ it can be reached via url.

#-----------------------------------------------------------------
# get_model_name( )
#-----------------------------------------------------------------
sub get_model_name {
    return $ADDRESS;
}

Gantry::Plugins::AutoCRUD uses get_model_name to find out which model class to use for create, update, delete, and lookups.

#-----------------------------------------------------------------
# text_descr( )
#-----------------------------------------------------------------
sub text_descr     {
    return 'address';
}

Gantry::Plugins::AutoCRUD uses text_descr to fill in the blank in things like:

Delete _____?

Now we are coming to the real code. The default action for a Location in Gantry is do_main. We usually use it to display a table with one summary row for each database row like this. It looks like this:

Main Listing Screen Shot

The code begins:

#-----------------------------------------------------------------
# $self->do_main(  )
#-----------------------------------------------------------------
sub do_main {
    my ( $self ) = @_;

    $self->stash->view->template( 'results.tt' );

Gantry objects store data for one page hit. Much of the data you should directly access is in the stash, which is a Gantry::Stash object. It provides accessors for all its data. If you need additional accessors feel free to add them. Gantry objects are hashes whose keys are usually formed from the attribute name like this:

name      becomes    __NAME__

but sometimes things aren't perfect, so stick with the accessors.

One of the key things in the stash is the view. It is a Gantry::Stach::View object which holds data destined for the template as well as the name of the template (which is set to 'results.tt' above).

$self->stash->view->title( 'Address' );

The stash view title appears in the browser window border.

Now we need to put some data into the standard results.tt table. We'll start with the headings for the columns.

my $data = {
    headings       => [
        'Name',
        'Phone Number',
    ],
    header_options => [
        {
            text => 'Add',
            link => $self->location() . "/add",
        },
    ],
};

The template is expecting a hash reference as its data. Two of the keys are headings and header_options. Headings are the labels for the normal columns in table. I could have included address here, but that would only have added lines of code without really showing you anything.

The header_options appear at the right side of the table heading. These are things you can do without referencing a current row. In this case, only add is possible. The link uses the location of the current page with /add appended. Clicking the link will cause Gantry to hit again, when it will dispatch to do_add in AutoCRUD.

my @rows = $ADDRESS->retrieve_all_for_main_listing();

The model will return an array of objects sorted in order by name through the badly named retrieve_all_for_main_listing. We can then walk that array as shown here:

foreach my $row ( @rows ) {
    my $id = $row->id;
    push(
        @{ $data->{rows} }, {
            data => [
                $row->name,
                $row->phone,
            ],
            options => [
                {
                    text => 'Edit',
                    link => $self->location() . "/edit/$id",
                },
                {
                    text => 'Delete',
                    link => $self->location() . "/delete/$id",
                },
            ],
        }
    );
}

For each row, we need to hold onto the id (so we can use it in edit/delete links), then push into the rows array of the data hash we are building for results.tt.

Each row we push is a hash with two keys: data and options. The data is just the values from the model object for the column in the output table (name and phone number here). The options are the things that can be done to the row. Here, the user will be able to edit and delete the row. Those links go directly to do_delete and do_edit which were exported by Gantry::Plugins::AutoCRUD.

Finally, we need to put the data into the stash:

    $self->stash->view->data( $retval );
} # END do_main

The only other piece of the controller is the form to use for add and edit. AutoCRUD calls this method for you when the users visits do_add and do_edit pages. Call this method form. If an edit triggered the call, it will pass in the row as it stands in the database.

The following code produces this on the screen:

Form Screen Shot
#-----------------------------------------------------------------
# $self->form( $row )
#-----------------------------------------------------------------
sub form {
    my ( $self, $row ) = @_;

    return {
        name       => 'address',
        row        => $row,
        legend     => $self->path_info =~ /edit/i ? 'Edit' : 'Add',
        fields     => [
            {
                name  => 'name',
                label => 'Name',
                type  => 'text',
            },
            # other similar hashes ommitted
            # see Complete Code Lsting
        ],
    }
} # END of form

1;

The form method must return a hash reference whose keys are understood by the form template (form.tt in our case). Here's what the keys mean to the standard form.tt:

name

the name of the form

row

data from the database to use as a default (meaningful for edit)

legend

form.tt uses a fieldset, this is its legend

fields

an array reference of the input fields (more info below)

Only one field is shown, because in our example they all look alike. The real code would need hashes just like the one above for street, city, state, zip, and phone number.

Each member of the fields array is a hash. There are lots of keys; here are the ones I'm using:

name

the name of the column in the database and the html form element

label

what the user sees next to the entry component

type

html form input type (choose from text, select, and textarea)

Other keys control things like how wide the field is, etc. See the docs in form.tt for details.

That's the whole controller (save the #... where the other fields go -- see below for "Complete Code Listing").

Apps::AddressBook::Model::address

To separate sql from the controller (and view) Gantry uses Class::DBI::Sweet (or any other object relational modeller with the same API) through its model base class Gantry::Utils::CDBI. Each model subclasses that class and represents one table in the database. These classes are standard Class::DBI subclasses. Here is ours:

package Apps::AddressBook::Model::address;
use strict; use warnings;

use base 'Gantry::Utils::CDBI', 'Exporter';

our $ADDRESS = 'Apps::AddressBook::Model::address';

our @EXPORT_OK = ( '$ADDRESS' );

Note that we export the alias for controllers to use when referring to the model class. This mitigates the length of the name. Gantry does not require you to do this. If you prefer to type the name, feel free.

Apps::AddressBook::Model::address->table   ( 'address'     );
Apps::AddressBook::Model::address->sequence( 'address_seq' );
Apps::AddressBook::Model::address->columns ( Primary   => qw/ id / );

Apps::AddressBook::Model::address->columns (
    All       => qw/
        id
        name
        street
        city
        state
        zip
        phone
    /
);

1;

See the perldoc for Class::DBI and Class::DBI::Sweet for more details.

Complete Code Listings

SQL for database creation

CREATE SEQUENCE address_seq;
CREATE TABLE address (
    id int4 PRIMARY KEY DEFAULT NEXTVAL( 'address_seq' ),
    name   varchar,
    street varchar,
    city   varchar,
    state  varchar,
    zip    varchar,
    phone  varchar
);

Apps::AddressBook

package Apps::AddressBook;

use strict;

our $VERSION = '0.01';

use Gantry qw{ -Engine=MP13 -TemplateEngine=TT };

our @ISA = ( 'Gantry' );

use Apps::AddressBook::Address;

1;

Apps::AddressBook::Address

package Apps::AddressBook::Address;

use strict;

use base 'Apps::AddressBook';

use Apps::AddressBook::Model::address qw( $ADDRESS );

use Gantry::Plugins::AutoCRUD;

#-----------------------------------------------------------------
# get_model_name( )
#-----------------------------------------------------------------
sub get_model_name {
    return $ADDRESS;
}

#-----------------------------------------------------------------
# text_descr( )
#-----------------------------------------------------------------
sub text_descr     {
    return 'address';
}

#-----------------------------------------------------------------
# $self->do_main(  )
#-----------------------------------------------------------------
sub do_main {
    my ( $self ) = @_;

    $self->stash->view->template( 'results.tt' );
    $self->stash->view->title( 'Address' );

    my $data = {
        headings       => [
            'Name',
            'Phone Number',
        ],
        header_options => [
            {
                text => 'Add',
                link => $self->location() . "/add",
            },
        ],
    };

    my @rows = $ADDRESS->retrieve_all_for_main_listing();

    foreach my $row ( @rows ) {
        my $id = $row->id;
        push(
            @{ $data->{rows} }, {
                data => [
                    $row->name,
                    $row->phone,
                ],
                options => [
                    {
                        text => 'Edit',
                        link => $self->location() . "/edit/$id",
                    },
                    {
                        text => 'Delete',
                        link => $self->location() . "/delete/$id",
                    },
                ],
            }
        );
    }

    $self->stash->view->data( $retval );
} # END do_main

#-----------------------------------------------------------------
# $self->form( $row )
#-----------------------------------------------------------------
sub form {
    my ( $self, $row ) = @_;

    return {
        name       => 'address',
        row        => $row,
        legend     => $self->path_info =~ /edit/i ? 'Edit' : 'Add',
        fields     => [
            {
                name  => 'name',
                label => 'Name',
                type  => 'text',
            },
            #...
        ],
    }
} # END of form

1;

Apps::AddressBook::Model::address

package Apps::AddressBook::Model::address;
use strict; use warnings;

use base 'Gantry::Utils::CDBI', 'Exporter';

our $ADDRESS = 'Apps::AddressBook::Model::address';

our @EXPORT_OK = ( '$ADDRESS' );

Apps::AddressBook::Model::address->table   ( 'address'     );
Apps::AddressBook::Model::address->sequence( 'address_seq' );
Apps::AddressBook::Model::address->columns ( Primary   => qw/ id / );

Apps::AddressBook::Model::address->columns (
    All       => qw/
        id
        name
        street
        city
        state
        zip
        phone
    /
);

1;

Deploying the Application

After coding the above modules we only need to do two more things: create the database and add our application to httpd.conf.

In postgres, you can merely say something like

createdb address
psql address -U apache < schema.sql

(supplying passwords as requested) where schema.sql is the one shown above in "Sample App Description".

Assuming you are using mod_perl 1.3, you can add the following to your httpd.conf:

<Perl>
    #!/usr/bin/perl

    use lib '/home/me/Apps-AddressBook/lib';

    use AddressBook;
    use AddressBook::Address;
</Perl>

<Location />
    PerlSetVar dbconn dbi:Pg:dbname=address
    PerlSetVar dbuser apache
    PerlSetVar dbpass secret
    PerlSetVar template_wrapper wrapper.tt
    PerlSetVar root /home/me/Apps-AddressBook/html:/home/me/srcgantry/root
</Location>

<Location /address>
    SetHandler  perl-script
    PerlHandler Apps::AddressBook::Address
</Location>

Adjust the dbconn, dbuser, and dbpass PerlSetVars for your database. The root needs to include the directory where wrapper.tt lives. You can copy one from the sample_wrapper.tt that ships with gantry (look in the directory named root).

Now all that remains is to restart the server.

If you are using Gantry::Conf (which we prefer, but didn't discuss above), you need to set one var:

PerlSetVar GantryConfInstance addressbook

Then create a config file for the set vars shown above. See Gantry::Conf::Tutorial for details.

If you are using CGI you need to make a script instead of adjusting apache locations. Here is ours:

#!/usr/bin/perl

use CGI::Carp qw( fatalsToBrowser );

use lib '/home/me/Apps-AddressBook/lib';

use Apps::AddressBook qw{ -Engine=CGI -TemplateEngine=TT };

use Gantry::Engine::CGI;

my $cgi = Gantry::Engine::CGI->new( {
    config => {
        dbconn => 'dbi:Pg:dbname=address',
        dbuser => 'apache',
        template_wrapper => 'wrapper.tt',
        root => '/home/me/Apps-AddressBook/html:',
                '/home/me/srcgantry/root',
    },
    locations => {
        '/' => 'Apps::AddressBook',
        '/address' => 'Apps::AddressBook::Address',
    },
} );

$cgi->dispatch();

If you are using Gantry::Conf with CGI, use the single config hash key:

my $cgi = Gantry::Engine::CGI->new( {
    config => {
        GantryConfInstance => 'address',
    }
    # locations as above
} );

If you want to deploy the app as a stand alone server (most useful during testing), change the above cgi script to this:

#!/usr/bin/perl

use Gantry::Server;

use lib '/home/me/Apps-AddressBook/lib';

use Apps::AddressBook qw{ -Engine=CGI -TemplateEngine=TT };
use Gantry::Engine::CGI;

my $cgi = Gantry::Engine::CGI->new( {
    config => {
        dbconn => 'dbi:Pg:dbname=address',
        dbuser => 'apache',
        template_wrapper => 'wrapper.tt',
        root => '/home/me/Apps-AddressBook/html:',
                '/home/me/srcgantry/root',
    },
    locations => {
        '/' => 'Apps::AddressBook',
        '/address' => 'Apps::AddressBook::Address',
    },
} );

my $port = shift || 8080;
my $server = Gantry::Server->new( $port );

$server->set_engine_object( $cgi );
$server->run();

That is, trade use CGI::Carp for use Gantry::Server and <$cgi-dispatch>> for the last four lines shown above. Running the script will start a server on port 8080 (or whatever port was supplied on the command line).

Using Bigtop

Now I have a confession. I never coded the example in the previous section. I let Bigtop do it.

Bigtop is a code generator which can safely regenerate as thing change (like the data model). The bigtop script reads a Bigtop file to produce apps like the one shown above. There is a more detailed example in the tutorial for Bigtop.

Bigtop uses its own little language to describe web applications. The language is designed for simplicity of structure. There are basically only two constructs: semi-colon terminated statements and brace delimited blocks.

[ Since this tutorial was writtern Bigtop has acquired tentmaker: a browser delivered editor. Using it saves typing. See Bigtop::Docs::TentMaker for details. ]

To show how to use Bigtop, I'll walk through the above example again, this time using bigtop.

First, type:

bigtop --new Apps::AddressBook

This will create a subdirectory under the current directory called Apps-AddressBook and fill it with the basic structure of an application.

Now, change to the Apps-AddressBook directory and edit docs/apps-addressbook.bigtop (feel free to rename the bigtop file). Alternatively, you could prepare the entire Bigtop file, then use

bigtop --create file.bigtop all

(The file for this example is in the examples directory of the Bigtop distribution as address.bigtop.)

Immediately after initial generation is a good time to put the application under your favorite revision control system.

There are two required blocks in a bigtop file: config and app. The config block always comes first. Let's see what goes in it. As in the the "Hand-writing the Sample App" section above, I'll show the Bigtop code a bit at a time with commentary interspersed. Then, after some user suggested revisions, I'll show the whole file all in one piece in "Complete Bigtop Code Listing".

config

The config block allows you to specify the engine (like mod_perl 1.3) and the template engine (like Template Toolkit) of the application. Do this with statements:

config {
    engine          MP13;
    template_engine TT;

There are other engines, notably: CGI and MP20 for mod_perl 2.0. If you don't want Template Toolkit, you can choose 'Default' as the template engine. Then you are on your own.

The rest of the config section has a list of the things you want to generate and who should do the generating.

Init            Std           {}

Init is only really useful when you create a new application, so the --new option flags it with no_gen, like this:

Init            Std           { no_gen 1; }

All of the other backends also respect the no_gen statement.

Once you build the first time, you probably want to set no_gen on Init, since it is responsible for making things like Build.PL and the Changes file, which we don't want to overwrite.

SQL             Postgres      {}

SQL is responsible for making a file of sql statements which can build the database (like schema.sql shown in "Sample App Description"). Here we will ensure Postgres syntax.

The other backends are similar.

HttpdConf       Gantry        {}

This generates the necessary bits for use in an Include statement in httpd.conf.

Control         Gantry        {}

This generates the controllers for the app, like Apps::AddressBook::Address.

Model           GantryCDBI    {}

Generates the model class for the address table: Apps::AddressBook::Model::address.

    SiteLook        GantryDefault {
        gantry_wrapper `/path/to/sample_wrapper.tt`;
    }
}

Generates wrapper.tt. Note that Bigtop::SiteLook::GantryDefault expects a gantry_wrapper statement with a path to the sample wrapper that ships with gantry. Some backends understand other statements, see their docs.

That's all there is to the config block.

app

app Apps::AddressBook {
    #...
}

Most of the description is in the app block. The block has a name which becomes the name of the base controller module. It usually corresponds to the directory where the app is built (Apps::AddressBook usually lives in Apps-AddressBook).

For our simple app, there are two statements in the app block and three sub-blocks. Folded in vim it looks like this:

app Apps::AddressBook {
    authors `Phil Crow`;
    email   `philcrow2000@yahoo.com`;
+--- 8 lines: config {------------------------------------------------
    sequence address_seq        {}
+--- 34 lines: table    address {----------------------------------------
+--- 18 lines: controller Address {--------------------------------------
}

The two statements are the author and the email contact for him (me). The email statement is optional, but if you want to use Module::Build's Build.PL, you should include authors (otherwise you can't ./Build dist).

The four blocks are:

config

listing configuration variables and their values

sequence

defining an SQL sequence named address_seq

table

defining an SQL table named address, its Model, and how its columns look on-screen

controller

defining the controller named Apps::AddressBook::Address

Details follow.

config

There are several config parameters needed to make the app work, as we saw in the hand written section above. Here we specify these in a block

app Apps::AddressBook {
    #...
    config {
        dbconn    `dbi:Pg:dbname=address`          => no_accessor;
        dbuser    apache                           => no_accessor;
        dbpass    not_telling                      => no_accessor;
        template_wrapper `wrapper.tt`              => no_accessor;
        root      `/home/me/Apps-Address/html:/home/me/srcgantry/root`
                                                   => no_accessor;
    }

Using the no_accessor option prevents bigtop from making an accessor and a statement in the init method for the variable. For the variables shown here, those accessors would be the same as the ones provided by Gantry.pm, so we don't need them.

sequence

Normally, we use a sequence for each table. This allows for automatic primary key generation. This block defines the sequence for the address table:

sequence address_seq {}

Note that the block is empty. In the future you might be able to specify min and max values, etc. For now, leave it empty.

table

Now we come to the interesting parts. First, we define the table. Inside its block we specify the name of its sequence and include a block for each of its columns.

table address {
    sequence address_seq;
    field id { is int4, primary_key, assign_by_sequence; }

The sequence must be previously defined. The id field will be the automatically generated primary key of the table. It has only one statement: is. Normally the is statement for a field simply gives the SQL type of the field (like int4 or varchar). Here other attributes are added. Namely, it is marked as a primary key (this affects both the SQL and Model) which is assigned from the sequence. You can abbreviate assign_by_sequence as auto.

The other fields have simpler is statements, but use additional statements.

field name {
    is             varchar;
    label          Name;
    html_form_type text;
}

The label is what the user sees when the field is on-screen. This is a table column label and the label next the input box on the add/edit form. The html_form_type text yields an input element on the form of type text. Not all types are supported by the Gantry templates, but bigtop doesn't care. Give it whatever you like. Gantry understands text, textarea, and select (which it interprets as 'pick one item from a drop down menu').

All of the other fields are extremely similar -- see "Complete Bigtop Code Listing".

They are called fields instead of columns to remind you that they appear in many places, not just in the database table.

controller

Usually, each controller works with one table. Once generation is complete, you can safely add additional tables' models to the controller. But, many times the standard one-table/one-controller paradigm is sufficient, as in the case of the address book:

controller Address {
    controls_table       address;
    rel_location         address;
    uses                 Gantry::Plugins::AutoCRUD;
    text_description     `address`;
    #... method blocks
}

There are many statements for use in controller blocks, four are shown here.

controls_table

must be a table defined in the bigtop file

rel_location

the tail of the controller's Location url

uses

a comma separated list of modules used by the controller. You could also declare the controller as being type AutoCRUD and leave Gantry::Plugins::AutoCRUD out of the uses list:

controller Address is AutoCRUD {
    # as before with no uses statement
}
text_description

used by AutoCRUD as discussed in the "Hand-writing the Sample App" section above

Recall that for AutoCRUD to work we need to define two methods: do_main and form. These are represented by method subblocks in the controller block.

Each method is declared like this:

method name is type { ... }

In our case the names will be do_main and form. Their types will be main_listing for do_main and AutoCRUD_form for form.

method do_main is main_listing {
    title            `Address`;
    cols             name, phone;
    header_options   Add;
    row_options      Edit, Delete;
}

The main listing has a browser title 'Address'. Note that bigtop uses backticks for quoting. This leaves regular single and double quotes for the normal Perl meanings. I presume that web apps are exceedingly unlikely to use backticks for shelling out. If they ever did, it would be in hand written code, not generated code.

The cols are the columns that will appear in the main listing table. header_options appear at the right side of the heading strip while row_options appear at the right side of each row.

method form is AutoCRUD_form {
    form_name        address;
    all_fields_but   id;
    extra_keys
        legend     => `$self->path_info =~ /edit/i ? 'Edit' : 'Add'`;
}

The form name becomes the name attribute of the form. We use it to support our legacy calendar popups.

You can either list fields you want to include on your form, or list the fields you don't want with all_fields_but.

Extra keys go directly into the hash reference returned by the form method. Ours puts Edit or Add into the fieldset legend.

Generating with bigtop

There are 80 lines in the example shown above. Once you have those typed in, you can generate like this:

bigtop --create address.bigtop all

This will create the Apps-AddressBook subdirectory of the current directory and all of the pieces described above. You can then use the application after installing it as described in the Deploying the Application section above.

Here is a complete list of what you get (with directory levels shown by indentation):

Apps-AddressBook/ - a directory where everything in the app lives
   Build.PL
   Changes        ready for use
   MANIFEST       complete as of the initial generation
   MANIFEST.SKIP
   README         in need of heavy editing
   docs/
      address.bigtop  - the original bigtop file
      httpd.conf      - an excerpt ready for inclusion in httpd.conf
                        for your mod_perl enabled apache
      schema.postgres - ready for use with psql
   html/
      wrapper.tt     - a simple site look
   lib/
      Apps/
         AddressBook.pm - base module for the app
         AddressBook/
            Address.pm - controller stub for the address table
               GEN/
                  Address.pm - generated code for Address.pm above
               Model/
                  address.pm - model stub for the address table
                  GEN/
                     address.pm - generated code for address.pm above
   t/
      01_use.t - tests whether each controller compiles

Note that there are more modules than in the hand written version. This allows you to change the data model and regenerate without fear of losing hand coded changes. So, AddressBook.pm, AddressBook::Address, and AddressBook::Model::address are stubs providing a place for you to add your customized code as needed; while AddressBook::GEN::Address and AddressBook::Model::GEN::address are generated each time you run bigtop. If you need to do something other than what the generated code does, simply redefine the behavior in the non-generated code stubs and that will be used. Do not edit the GEN modules, instead only add code to the stubs as needed.

Revisions

As soon as I told my wife that I had the above app running, she offered helpful suggestions in case she decided to use it. First, the address fields should be optional, since she doesn't always know where people live. Second, she would like to add email addresses, since for some people that is all she has. Finally, and for our purposes more interestingly, she once tracked birthdays in the back of the book. While that requires a bit too much effort, and we too stingy to send gifts, it would be a nice addition, especially as nieces and nephews keep appearing with some frequency.

The joy of bigtop is that revisions like these are fairly easily made. In each case, we will: (1) modify the bigtop file and (2) regenerate. Those two steps are often enough by themselves. But, as before, there may be some code we actually have to write.

The final version of the bigtop file is shown below. It is also in the Bigtop distribution's example directory as adress2.bigtop.

Making things optional

To mark the street, city, state, and zip as optional, we just need to add a statement to their blocks:

field street {
    is                 varchar;
    label              Street;
    html_form_type     text;
    html_form_optional 1;
}

Setting html_form_optional to any true value (like 1 shown here), tells the AutoCRUD scheme in Gantry to instruct Data::FormValidator that the field is optional. Users will not be required to supply it. The other address fields also receive this treatment.

Constraining things

No data in the sample address book is validated (because Lisa has too many friends and relatives living in too many places for meaningful validation).

But, if you want validation, you can include it like so:

field zip {
    is    varchar;
    label Zip;
    html_form_type text;
    html_form_optional 1;
    html_form_constraint `qr{^\d{5}$}`;
}

The constraint could be a valid Perl regex. You could also call a sub which returns a regex. If you include a uses statement in your controller like this:

uses Data::FormValidator::Constraints => `qw(:closures)`;

You can set the constraint like so:

html_form_constraint `zip_or_postcode()`;

See perldoc Data::FormValidator::Constraints for details of the closures available. All of them return a regex suitable for use as shown.

Email address field

It is particularly easy to add a new field to the address table:

field email {
    is                 varchar;
    label              `Email Address`;
    html_form_type     text;
    html_form_optional 1;
}

Note that I put the label for this field in backquotes, since its name contains a space.

We don't have to change the Address controller block, because the only thing affected is the form. We already specified that the form should have all_fields_but id. So, email will show up upon regeneration.

Birthday table

The most interesting change is adding birthdays. In my mind, this leads to a new table with this schema:

CREATE SEQUENCE birth_seq;
CREATE TABLE birth (
    id int4 PRIMARY KEY DEFAULT NEXTVAL( 'birth_seq' ),
    name varchar,
    family int4,
    birthday date
);

To generate this sql, its model and controller we can add this to our bigtop file (again, I'll show it a bit a time with commentary):

sequence birth_seq {}
table birth {
    sequence birth_seq;
    field id { is int4, primary_key, assign_by_sequence; }
    field name {
        is             varchar;
        label          Name;
        html_form_type text;
    }

This will the name of one person in a nuclear family.

field family {
    is                int4;
    label             Family;
    html_form_type    select;
    refers_to         address;
}

This field becomes a foreign key pointing to the address table, since it uses the refers_to statement. When the user enters a value for this field, they must choose one family defined in the address table.

    field birthday {
        is                date;
        label             Birthday;
        html_form_type    date;
        date_select_text `Popup Calendar`;
    }
}

I've chosen to store the actual date of birth (which leads to recording women's ages, shame on me). This is to show how date selection works smoothly for your users. There are three steps to this process. The first one is shown here: use the date_select_text statement. Its value becomes the link text the user clicks to popup the calendar selection mini-window. See, the controller below for the other two steps.

controller Birth {
    controls_table   birth;
    rel_location     birthday;
    uses             Gantry::Plugins::AutoCRUD,
                     Gantry::Plugins::Calendar;

Step two in easy dates is to use Gantry::Plugins::Calendar which provides javascript code generation routines.

text_description `birthday`;
page_link_label  Birthdays;

This page will show up in site navigation with its page_link_label

method do_main is main_listing {
    title            `Birthday`;
    cols             name, family, birthday;
    header_options   Add;
    row_options      Edit, Delete;
}

The main listing is just like the one for the address table, except for the names of the displayed fields.

    method form is AutoCRUD_form {
        form_name        birthday_form;
        all_fields_but   id;
        extra_keys
            javascript => `$self->calendar_month_js( 'birthday_form' )`,
            legend     => `$self->path_info =~ /edit/i ? 'Edit' : 'Add'`;
    }
}

Now the name of the form becomes important. The calendar_month_js method (mixed in by Gantry::Plugins::Calendar) generates the javascript for the popup and its callback, which populates the date fields. Note that we don't tell it which fields to handle. It will work on all fields that have date_select_text statements.

Once these changes are made, we can regenerate the application:

bigtop docs/address.bigtop all

Execute this command while in the build directory (the one with the Changes file in it).

Complete Bigtop Code Listing

config {
   engine          MP13;
   template_engine TT;
   Init            Std           {}
   SQL             Postgres      {}
   HttpdConf       Gantry        {}
   Control         Gantry        {}
   Model           GantryCDBI    {}
   SiteLook        GantryDefault {
       gantry_wrapper `/home/pcrow/srcgantry/root/sample_wrapper.tt`;
   }
}
app Apps::AddressBook {
   authors `Phil Crow`;
   email   `philcrow2000@yahoo.com`;
   config {
       dbconn    `dbi:Pg:dbname=address`          => no_accessor;
       dbuser    apache                           => no_accessor;
       template_wrapper `wrapper.tt`              => no_accessor;
       root      `/home/pcrow/Bigtop/examples/Apps-AddressBook/html:/home/pcrow/srcgantry/root`
                                                  => no_accessor;
   }
   sequence address_seq        {}
   table    address {
       sequence address_seq;
       foreign_display `%name`;
       field id { is int4, primary_key, assign_by_sequence; }
       field name {
           is             varchar;
           label          Name;
           html_form_type text;
       }
       field street {
           is             varchar;
           label          Street;
           html_form_type text;
           html_form_optional 1;
       }
       field city {
           is             varchar;
           label          City;
           html_form_type text;
           html_form_optional 1;
       }
       field state {
           is             varchar;
           label          State;
           html_form_type text;
           html_form_optional 1;
       }
       field zip {
           is             varchar;
           label          Zip;
           html_form_type text;
           html_form_optional 1;
       }
       field phone {
           is             varchar;
           label          Number;
           html_form_type text;
       }
       field email {
           is                 varchar;
           label              `Email Address`;
           html_form_type     text;
           html_form_optional 1;
       }
   }
   sequence birth_seq {}
   table birth {
       sequence birth_seq;
       field id { is int4, primary_key, assign_by_sequence; }
       field name {
           is             varchar;
           label          Name;
           html_form_type text;
       }
       field family {
           is                int4;
           label             Family;
           html_form_type    select;
           refers_to         address;
       }
       field birthday {
           is                date;
           label             Birthday;
           html_form_type    date;
           date_select_text `Popup Calendar`;
       }
   }
   controller Address {
       controls_table   address;
       rel_location     address;
       uses             Gantry::Plugins::AutoCRUD;
       text_description `address`;
       method do_main is main_listing {
           title            `Address`;
           cols             name, phone;
           header_options   Add;
           row_options      Edit, Delete;
       }
       method form is AutoCRUD_form {
           form_name        address;
           all_fields_but   id;
           extra_keys
               legend     => `$self->path_info =~ /edit/i ? 'Edit' : 'Add'`;
       }
   }
   controller Birth {
       controls_table   birth;
       rel_location     birthday;
       uses             Gantry::Plugins::AutoCRUD,
                        Gantry::Plugins::Calendar;
       text_description `birthday`;
       page_link_label  Birthdays;
       method do_main is main_listing {
           title            `Birthday`;
           cols             name, family, birthday;
           header_options   Add;
           row_options      Edit, Delete;
       }
       method form is AutoCRUD_form {
           form_name        birthday_form;
           all_fields_but   id;
           extra_keys
               javascript => `$self->calendar_month_js( 'birthday_form' )`,
               legend     => `$self->path_info =~ /edit/i ? 'Edit' : 'Add'`;
       }
   }
}

Summary

In this document we have seen how a simple Gantry app can be written and deployed. While building a simple app with bigtop can take just a few minutes, interesting parts can be fleshed out as needed. Our goal is to provide a framework that automates the 50-80% of most apps which is repetitive, allowing us to focus our time on the more interesting bits that vary from app to app.

If you want to see a more realistic app, see Bigtop::Docs::Tutorial which builds a basic freelancer's billing app.

There are other documents you might also want to read.

Gantry::Docs::FAQ

categorized questions and answers explaining how to do common tasks

Gantry::Docs::About

marketing document listing the features of Gantry and telling its history

The modules have their own docs which is where would be gantry developers should look for more information.

Author

Phil Crow <philcrow2000@yahoo.com>

Copyright and License

Copyright (c) 2006, Phil Crow.

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.6 or, at your option, any later version of Perl 5 you may have available.