Skip site navigation (1)Skip section navigation (2)

FreeBSD Manual Pages

  
 
  

home | help
Gantry::Docs::TutorialUser Contributed Perl DocumentaGantry::Docs::Tutorial(3)

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 four key modules in this application:

       Apps::Address
	   the base module

       Apps::Address::Model
	   the DBIx::Class schema which	controls the model.  In	a bigger app,
	   it would control all	models.

       Apps::Address::Address
	   the controller for the address table

       Apps::Address::Model::address
	   the object relational mapper	class, which inherits from DBIx::Class

       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::Address
       The job of the base module is to	provide	a home for shared code and a
       common place to hold site navigation links.

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

	   package Apps::Address;

	   use strict;

	   our $VERSION	= '0.01';

       It begins like any other	module...

	   use Gantry qw{ -TemplateEngine=TT };

	   our @ISA = (	'Gantry' );

       ...but, it uses Gantry with a template engine (Template Toolkit).

       Note that somewhere you need to use Gantry with the -Engine option.
       I'll do that in the stand alone server script.  You could also do it in
       a CGI dispatching script	or in httpd.conf for mod_perl deployments.

	   use Apps::Address::Address;

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

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

	       return [
		   { link => $self->app_rootp()	. 'address', label => 'Address'	},
	       ];
	   } # END site_links

       The "site_links"	method provides	a common place for all (or most) app
       pages to	look for site navigation links.	 The only link here takes
       users to	the default page in the	address	table's	controller.

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

	       $self->stash->view->template( 'main.tt' );
	       $self->stash->view->title( 'Main	Listing' );

	       $self->stash->view->data( {
		   pages => [
		       { link => 'address', label => 'Address' },
		   ],
	       } );
	   } # END do_main

	   1;

       "do_main" is one	(of two) default methods Gantry	dispatches to.	If you
       hit the controller on its base URL, Gantry will try to dispatch to
       "do_main".  If you don't	have one of those, it will fall	back to
       "do_default" (which we use sparingly, usually to	accept URL parameters
       without having to use the query string).

       This main method	merely displays	the site links in "main.tt" which
       ships with Gantry.  It shows a bulleted list of all navigation links.

       There is	one other commonly useful method in the	base controller:
       "init".	Gantry.pm handles a set	of standard configuration parameters.
       If you need to handle others, implement an init sub and accessors for
       them.  First, dispatch to SUPER,	so it can handle 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::Address::Model
       The Model might better be called	Apps::Address::Schema, since it
       inherits	from DBIx::Class::Schema.  But we call it the model.  It has
       two purposes.  First, is	to load	the actual model classes.  Second, it
       sets the	DBI options for	the database connections.

	   package Apps::Address::Model;
	   use strict; use warnings;

	   use base 'DBIx::Class::Schema';

	   __PACKAGE__->load_classes( qw/
		   address
	   / );

	   sub get_db_options {
	       return {	AutoCommit => 1	};
	   }

	   1;

       See "DBIx::Class::Schema" for a discussion of "load_classes" and	the
       other things you	can set	up in your schema.

       This schema only	loads "address", since that is our only	table.	Even
       for complex apps, there is rarely any more complexity to	this module.
       But, it will have more classes to load.

   Apps::Address::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::Address::Address;

	   use strict;

	   use base 'Apps::Address';

       It begins like any subclass.  Note that it is a subclass	of
       Apps::Address 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 Gantry::Plugins::DBIxClassConn qw( get_schema );

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

       To ease DBIx::Class use,	Gantry provides	a plugin:
       "Gantry::Plugins::DBIxClassConn".  That plugin exports "get_schema",
       which controllers need to read their database.  We'll see how to	use it
       below.

       In addition to loading the plugin, our controller also needs to use the
       base model (a.k.a., the schema) and which ever models it	actually
       needs.

       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	five methods to	us: "do_add",
       "do_edit", "do_delete", "form_name", and	"write_file".  The "form_name"
       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	that method so it returns the name of
       your template file.

       The "write_file"	method handles file uploads, which we don't need for
       this app.

       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.

       So, we are mixing in "do_add", "do_edit", and "do_delete".  We need to
       implement a few methods to complete our controller.

       We need a small sub so the DBIx::Class plugin can find our schema:

	   sub schema_base_class { return 'Apps::Address::Model'; }

       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:

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

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

       The "do_main" controller	uses the "results.tt" default main listing
       template	which ships with Gantry.  If you change	templates, you'll
       probably	need to	substantially modify the rest of "do_main".

	       my $real_location = $self->location() ||	'';
	       if ( $real_location ) {
		   $real_location =~ s{/+$}{};
		   $real_location .= '/';
	       }

       Some care is required to	avoid missing or doubled slashes when forming
       URLs.  But, that's nothing a little string work can't address.  With a
       clean address in	place we are ready to build the	output.

	       my $retval = {
		   headings	  => [
		       'Name',
		       'Street',
		   ],
		   header_options => [
		       {
			   text	=> 'Add',
			   link	=> $real_location . "add",
		       },
		   ],
	       };

       The template always receives a hash reference.  I frequently call mine
       $retval,	short for return value.	 For "results.tt", the hash describes
       the main	listing	table.	The are	two parts to that: the heading row and
       the body	rows.  There is	one heading row	for the	table.	This one has
       labels: 'Name' and 'Street.'  There is one option the user can invoke
       for the whole table: Add.  Clicking that	will lead to the same URL with
       'add' appended.	That URL will be dispatched to the "do_add" we mixed
       in from the AutoCRUD plugin.

       Now we need the data for	the main listing.  For simplicity, I'll	get
       all the data.  It is not	hard to	get pages of data, but I'll leave that
       for later documents (look for "rows" or "paged_conf" in bigtop or
       tentmaker's docs).

	       my $schema = $self->get_schema();
	       my @rows	  = $ADDRESS->get_listing( { schema => $schema } );

       First, I	asked for the DBIx::Class schema, but calling the "get_schema"
       accessor	mixed in for us	by the "DBIxClassConn" plugin.	Next, I	called
       "get_listing" on	the address model, through its alias.  This sugar
       method returns the rows for our main listing.  Note that	it expects
       named arguments in a hash reference and "schema"	is required.

       Now, it's a fairly simple matter	to loop	over each database row making
       a table row in the template data.

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

       First, I	fished the row id out of the DBIx::Class object, and stored it
       in a scalar.  This allows direct	interpolation into a couple of URL
       link strings.  Then I pushed the	data and the row options into the
       "rows" key of the return	value hash.  The data are just the family's
       name and	street address.	 You could add any other columns from the
       underlying table.  For instance,	to add phone, add:

	   $row->phone,

       to the data list	after "$row-"street>.  The order and contents of the
       data are	up to you.

       Just as the header row has an Add option, each data row has an Edit and
       a Delete	option.	 Note that their URLs include the row id to work on.
       The only	thing I	have left to do	in "do_main" is	to set the return
       value data in place:

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

       The only	other large piece is the form, in which	users enter new
       addresses or edit existing ones.	 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:

	   #-----------------------------------------------------------------
	   # $self->form( $row )
	   #-----------------------------------------------------------------
	   sub form {

	       return {
		   row	  => $row,
		   legend => $self->path_info =~ /edit/i ? 'Edit' : 'Add',
		   fields => [

       The default template is called "form.tt".  Among	other things, it
       expects the return value	hash to	contain	"row" (if editing), "legend"
       (legend of form's fieldset), and	"fields" (what the user	will see and
       enter or	edit).	If the "row" is	supplied, its values are used for
       initial form population.	 The "legend" is set based on the "path_info"
       which contains part of the URL.	If that	URL fragment includes 'edit,'
       the legend is 'Edit.'  Otherwise, it is 'Add.'

       The "fields" are	an array of the	entry elements the user	will see.  The
       order of	the array controls the on screen order.	 Each field is a
       little hash.  While there are other keys, the four most common are used
       over and	over, not just in this example.

		       {
			   name	=> 'name',
			   optional => 0,
			   label => 'Name',
			   type	=> 'text',
		       },

       The "name" must be the name of the column in the	database and will also
       be used as the name of the html form element.

       If "optional" is	true, the field	is optional.  Otherwise, it is
       required.  I could have omitted optional	from the Name hash, since
       required	is the default.

       The "label" is displayed	in the left hand column	of the form input
       table.

       The "type" is the HTML form element type.  See "form.tt"	in Gantry's
       templates for a complete	list of	types is understands.  That will also
       explain how to include other field hash keys to specify things like
       pull down options.

       The other "fields" hashes are all of the	same form.  Only the field
       names and labels	change.	 Here is one example:

		       {
			   name	=> 'city',
			   optional => 1,
			   label => 'City',
			   type	=> 'text',
		       },
		       #...
		   ],
	       };
	   } # END form

       Finally,	there are some small subs which	return strings used by the
       AutoCRUD	plugin at various points.

	   #-----------------------------------------------------------------
	   # 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.

	   #-----------------------------------------------------------------
	   # get_orm_helper( )
	   #-----------------------------------------------------------------
	   sub get_orm_helper {
	       return 'Gantry::Plugins::AutoCRUDHelper::DBIxClass';
	   }

       For historical reasons, the AutoCRUD plugin defaults to using
       Class::DBI.  We no longer use that.  So,	we have	to provide
       "get_orm_helper"	to identify our	CRUD helper.

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

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

	   Delete _____?

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

   Apps::Address::Model::address
       To separate sql from the	controller (and	view) we use Gantry with an
       Object-Relational Mapper	(ORM).	For this example I will	show
       DBIx::Class, since it the one we've settled on.	You could also use
       Class::DBI or Gantry's native models, but I won't show you how.

       Gantry provides its own base class to add to "DBIx::Class" it is
       Gantry::Utils::DBIxClass.  Each model subclasses	it and represents one
       table in	the database.  These classes are standard "DBIx::Class"
       subclasses.  Here is the	own for	my address table:

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

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

	   our $ADDRESS	= 'Apps::Address::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.

	   __PACKAGE__->load_components( qw/ PK::Auto Core / );
	   __PACKAGE__->table( 'address' );
	   __PACKAGE__->add_columns( qw/
	       id
	       name
	       street
	       created
	       modified
	       city
	       state
	       zip
	       phone
	   / );
	   __PACKAGE__->set_primary_key( 'id' );
	   __PACKAGE__->base_model( 'Apps::Address::Model' );

       All of these calls are common when using	"DBIx::Class", except
       "base_model".  Gantry uses it to	hide the connection inside
       "Gantry::Plugins::DBIxClassConn".

       Various parts of	Gantry use other methods I should define here.	They
       are all simple.

	   sub foreign_display {
	       my $self	= shift;

	       my $name	= $self->name()	|| '';

	       return "$name";
	   }

       The "foreign_display" controls the default sort order of	"get_listing"
       which I called in "do_main" of the controller for the address table.
       It also controls	how rows from the table	will be	summarized when	other
       tables refer to this one	via a foreign key.

	   sub get_foreign_display_fields {
	       return [	qw( name ) ];
	   }

       This tells anyone who is	interested the names in	the foreign display
       string in their order of	appearance there.
       "get_foreign_display_fields" is what actually controls "get_listing"
       sort order.

	   sub get_foreign_tables {
	       return qw(
	       );
	   }

       This returns a list of table names for which this table has foreign
       keys.

	   sub table_name {
	       return 'address';
	   }

       Finally,	this returns the name of the table.  This becomes important if
       you are using Postgres schemas which preface the	table name with	its
       schema name and a dot (for instance: my_schema.my_table).  This table
       name will have the dot, even though the dot is converted	to an
       underscore for the package name and in other places where Perl objects
       to dots.

       See the perldoc for "DBIx::Class" and "DBIx::Class::ResultSet" for more
       details.

   Complete Code Listings
       Note that all POD sections have been omitted for	brevity.

       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::Address

	   package Apps::Address;

	   use strict;

	   our $VERSION	= '0.01';

	   use Apps::Address::Address;

	   use Gantry qw{ -TemplateEngine=TT };

	   our @ISA = qw( Gantry );

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

	       $self->stash->view->template( 'main.tt' );
	       $self->stash->view->title( 'Main	Listing' );

	       $self->stash->view->data( {
		   pages => [
		       { link => 'address', label => 'Address' },
		   ],
	       } );
	   } # END do_main

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

	       return [
		   { link => $self->app_rootp()	. 'address', label => 'Address'	},
	       ];
	   } # END site_links

	   1;

       Apps::Address::Model

	   package Apps::Address::Model;
	   use strict; use warnings;

	   use base 'DBIx::Class::Schema';

	   __PACKAGE__->load_classes( qw/
	       address
	   / );

	   sub get_db_options {
	       return {	AutoCommit => 1	};
	   }

	   1;

       Apps::Address::Address

	   package Apps::Address::Address;

	   use strict;

	   use base 'Apps::Address';

	   use Gantry::Plugins::DBIxClassConn qw( get_schema );

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

	   use Gantry::Plugins::AutoCRUD qw(
	       do_add
	       do_edit
	       do_delete
	       form_name
	       write_file
	   );

	   sub schema_base_class { return 'Apps::Address::Model'; }

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

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

	       my $real_location = $self->location() ||	'';
	       if ( $real_location ) {
		   $real_location =~ s{/+$}{};
		   $real_location .= '/';
	       }

	       my $retval = {
		   headings	  => [
		       'Name',
		       'Street',
		   ],
		   header_options => [
		       {
			   text	=> 'Add',
			   link	=> $real_location . "add",
		       },
		   ],
	       };

	       my $schema = $self->get_schema();
	       my @rows	  = $ADDRESS->get_listing( { schema => $schema } );

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

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

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

	       return {
		   row	      => $row,
		   legend => $self->path_info =~ /edit/i ? 'Edit' : 'Add',
		   fields     => [
		       {
			   name	=> 'name',
			   optional => 0,
			   label => 'Name',
			   type	=> 'text',
		       },
		       {
			   name	=> 'street',
			   optional => 1,
			   label => 'Street',
			   type	=> 'text',
		       },
		       {
			   name	=> 'city',
			   optional => 1,
			   label => 'City',
			   type	=> 'text',
		       },
		       {
			   name	=> 'state',
			   optional => 1,
			   label => 'State',
			   type	=> 'text',
		       },
		       {
			   name	=> 'zip',
			   optional => 1,
			   label => 'Zip',
			   type	=> 'text',
		       },
		       {
			   name	=> 'phone',
			   optional => 1,
			   label => 'Phone',
			   type	=> 'text',
		       },
		   ],
	       };
	   } # END form

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

	   #-----------------------------------------------------------------
	   # get_orm_helper( )
	   #-----------------------------------------------------------------
	   sub get_orm_helper {
	       return 'Gantry::Plugins::AutoCRUDHelper::DBIxClass';
	   }

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

	   1;

       Apps::Address::Model::address

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

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

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

	   our @EXPORT_OK = ( '$ADDRESS' );

	   __PACKAGE__->load_components( qw/ PK::Auto Core / );
	   __PACKAGE__->table( 'address' );
	   __PACKAGE__->add_columns( qw/
	       id
	       name
	       street
	       created
	       modified
	       city
	       state
	       zip
	       phone
	   / );
	   __PACKAGE__->set_primary_key( 'id' );
	   __PACKAGE__->base_model( 'Apps::Address::Model' );

	   sub get_foreign_display_fields {
	       return [	qw( name ) ];
	   }

	   sub get_foreign_tables {
	       return qw(
	       );
	   }

	   sub foreign_display {
	       my $self	= shift;

	       my $name	= $self->name()	|| '';

	       return "$name";
	   }

	   sub table_name {
	       return 'address';
	   }

	   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.postgres

       (supplying passwords as requested) where	schema.postgres	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-Address/lib';

	       use Address;
	       use Address::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-Address/html:/home/me/srcgantry/root
	   </Location>

	   <Location /address>
	       SetHandler  perl-script
	       PerlHandler Apps::Address::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-Address/lib';

	   use Apps::Address 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-Address/html:',
			   '/home/me/srcgantry/root',
	       },
	       locations => {
		   '/' => 'Apps::Address',
		   '/address' => 'Apps::Address::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-Address/lib';

	   use Apps::Address 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-Address/html:',
			   '/home/me/srcgantry/root',
	       },
	       locations => {
		   '/' => 'Apps::Address',
		   '/address' => 'Apps::Address::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.

       The easiest way to edit bigtop files is to use tentmaker, a browser
       delivered editor.  It saves a lot of typing.  If	you really want	to see
       what the	bigtop file looks like,	see "Complete Bigtop Code Listings"
       below.  If you want to just build the app from that listing, use
       "address-new.bigtop" from the examples directory	of the Bigtop
       distribution.  Type:

	   bigtop -c address-new.bigtop	all

       If you want to build that bigtop	file, keep reading.

       First, type:

	   tentmaker -n	Apps::Address address

       This will start tentmaker, tell it to make a new	app called
       "Apps::Address" and give	it a single table "address".  Once it starts,
       tentmaker will print a URL on your screen like this:

	...You can connect to your server at http://localhost:8080/

       Go to the URL indicated with a DOM compliant browser like Firefox or
       Safari.	There are five tabs in tentmaker.  We need to change things
       only in the App Body, so	click it.

       Scroll down to edit the tables called 'address' (it should be the only
       table).	After clicking 'edit,' scroll down further until you see the
       'Field Quick Edit' table.  Change the 'Column Name' ident to 'name.'
       Click 'Apply Quick Edit.'  (Actually, you can click anywhere in the
       browser outside fo the input box	to update it.)	Change 'description'
       to 'street'.  Then, under 'Create Field(s),' enter a single string:

	   city	state zip phone	email

       Then press 'Create.'  You should	see the	new fields in the quick	edit
       table.

       Click optional in the quick edit	heading	row to make all	fields
       optional.  Finally, uncheck optional for	the name.

       Now click the 'Bigtop Config' table.  Enter a file name next to 'Save
       As:' After you enter a name, click 'Save	As:'.  tentmaker will print a
       little message under the	the buttons telling you	whether	save your file
       or not.	If it saved successfully, press	'Stop Server' and confirm that
       you want	to stop	the server.

       In the same shell where you launched tentmaker, you should have your
       prompt back.  Type:

	   bigtop -c address.bigtop all

       Change "address.bigtop" to whatever you called the bigtop file.	Bigtop
       will build the application and give you instructions on how to start
       it.  Follow those.  For example,	since I	have an	executable 'sqlite' in
       my path,	bigtop said this:

	   I have generated your 'Apps::Address' application.  I have also
	   taken the liberty
	   of making an	sqlite database	for it to use.	To run the application:

	       cd Apps-Address
	       ./app.server [ port ]

	   The app.server runs on port 8080 by default.

	   Once	the app.server starts, it will print a list of the urls	it can serve.
	   Point your browser to one of	those and enjoy.

	   If you prefer to run	the app	with Postgres or MySQL type one	of these:

	       bigtop --pg_help
	       bigtop --mysql_help

       If you don't have sqlite, it will add a step for	building the database.
       Do type:

	       bigtop --pg_help

       or

	       bigtop --mysql_help

       if you use one of those databases.

   Generating with bigtop
       There are about 100 lines in the	example	bigtop file built above.  Here
       is a complete list of what bigtop built for you from that file (with
       directory levels	shown by indentation):

	Apps-Address/ -	a directory where everything in	the app	lives
	   app.cgi	  CGI script
	   app.db	  sqlite database, if you sqlite in your path
	   app.server	  stand	alone server script
	   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
	      schema.mysql    -	ready for use with mysql
	      schema.postgres -	ready for use with psql
	      schema.sqlite   -	ready for use with sqlite
	   html/
	       templates/
		   genwrapper.tt     - a simple	site look
	   lib/
	      Apps/
		 Address.pm    - base module stub for the app
		 GENAddress.pm - generated base	module for the app
		 Address/
		    Address.pm - controller stub for the address table
		       GEN/
			  Address.pm - generated code for Address.pm above
		       Model.pm	 - DBIx::Class schema to all models
		       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
	      02_pod.t	    - if you have Test::Pod, validates all pod in all modules
	      03_podcover.t - if you have Test::Pod::Coverage, looks for missing pod
	      10_run.t	    - hits the default page of each controller

       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, Address.pm, Address::Model,
       Address::Address, and Address::Model::address are stubs providing a
       place for you to	add your customized code as needed; while
       Address::GEN::Address, Address::Model::GEN::address, etc. 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
       Suppose that you	want some validation of	the input.

       Further,	suppose	my wife	wants us to add	a birth	day table so she can
       send cards.

       We'll see how to	add those things here, by manually editing the bigtop
       file.  You could	do these things	with tentmaker as well.	 But sometimes
       it is easier to work with your favorite text editor.  Do	what makes
       sense.

       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.  tentmaker 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 at a time	with commentary):

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

       This will be 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`;
	       }
	       foreign_display `%name`;
	   }

       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 is AutoCRUD	{
	       controls_table	birth;
	       rel_location	birthday;
	       uses		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	`Birth Day`;

       This page will show up in site navigation with its page_link_label

	       method do_main is main_listing {
		   title	    `Birth Day`;
		   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
		       legend	  => `$self->path_info =~ /edit/i ? 'Edit' : 'Add'`,
		       javascript => `$self->calendar_month_js(	'birthday_form'	)`;
	       }
	   }

       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).

       For the app to work successfully, you will need to alter	the existing
       database	so it has the new columns and birth day	table.	Either throw
       out the old database or alter it	at your	option.	 Bigtop	has data
       statements which	allow you to specify initial data for tables.  This
       makes discarding	a database less	painful.

       Again, I	confess	that I used tentmaker to get me	started	with the
       changes above, then cleaned its output until it became the "Complete
       Bigtop Code Listing" below.

       You can continue	to edit	the bigtop file	with a text editor or
       tentmaker and regenerate	as the app matures.  We	have regenerated
       production apps months after deployment.

   Complete Bigtop Code	Listing
	config {
	    engine CGI;
	    template_engine TT;
	    Init Std {	}
	    SQL	SQLite {  }
	    SQL	Postgres {  }
	    SQL	MySQL {	 }
	    CGI	Gantry { gen_root 1; with_server 1; flex_db 1; }
	    Control Gantry { dbix 1; }
	    Model GantryDBIxClass {  }
	    SiteLook GantryDefault {  }
	}
	app Apps::Address {
	    config {
		dbconn `dbi:SQLite:dbname=app.db` => no_accessor;
		template_wrapper `genwrapper.tt` => no_accessor;
	    }
	    controller is base_controller {
		method do_main is base_links {
		}
		method site_links is links {
		}
	    }
	    table address {
		field id {
		    is int4, primary_key, auto;
		}
		field name {
		    is varchar;
		    label Name;
		    html_form_type text;
		    html_form_optional 0;
		}
		field street {
		    is varchar;
		    label Street;
		    html_form_type text;
		    html_form_optional 1;
		}
		foreign_display	`%name`;
		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;
		    html_form_constraint `qr{^\d{5}$}`;
		}
		field country {
		    is varchar;
		    label Country;
		    html_form_type text;
		    html_form_optional 1;
		}
		field email {
		    is varchar;
		    label Email;
		    html_form_type text;
		    html_form_optional 1;
		}
		field phone {
		    is varchar;
		    label Phone;
		    html_form_type text;
		    html_form_optional 1;
		}
	    }
	    controller Address is AutoCRUD {
		controls_table address;
		rel_location address;
		text_description address;
		page_link_label	Address;
		method do_main is main_listing {
		    cols name, street;
		    header_options Add;
		    row_options	Edit, Delete;
		    title Address;
		}
		method form is AutoCRUD_form {
		    all_fields_but id, created,	modified;
		    extra_keys
			legend => `$self->path_info =~ /edit/i ? 'Edit'	: 'Add'`;
		}
	    }
	    table birth	{
		field id {
		    is int4, primary_key, auto;
		}
		field name {
		    is varchar;
		    label Name;
		    html_form_type text;
		}
		field family {
		    is int4;
		    label Family;
		    refers_to address;
		    html_form_type select;
		}
		field birthday {
		    is date;
		    label Birthday;
		    html_form_type text;
		    date_select_text `Popup Calendar`;
		}
		foreign_display	`%name`;
	    }
	    controller Birth is	AutoCRUD {
		controls_table	 birth;
		rel_location	 birthday;
		uses		 Gantry::Plugins::Calendar;
		text_description birthdays;
		page_link_label	`Birth Days`;
		method do_main is main_listing {
		    title `Birth Day`;
		    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
			legend => `$self->path_info =~ /edit/i ? 'Edit'	: 'Add'`,
			javascript => `$self->calendar_month_js( 'birthday_form' )`;
		}
	    }
	}

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-7, 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.

perl v5.24.1			  2017-07-02	     Gantry::Docs::Tutorial(3)

Name | Introduction | Sample App Description | Hand-writing the Sample App | Deploying the Application | Using Bigtop | Summary | Author | Copyright and License

Want to link to this manual page? Use this URL:
<https://www.freebsd.org/cgi/man.cgi?query=Gantry::Docs::Tutorial&sektion=3&manpath=FreeBSD+12.0-RELEASE+and+Ports>

home | help