[cgiapp] handling data and control flow better (longish)

Ron Savage ron at savage.net.au
Mon Jun 8 03:11:21 EDT 2009


Hi David

Thanx for your contribution.

But, where to start!

Let me try to address a few points...

1) RESTful interfaces.

http://www.onlamp.com/pub/a/onlamp/2008/02/19/developing-restful-web-services-in-perl.html

I mention this because I'm about to quote my own code. If I had followed
the RESTful approach, what I call add_note would probably have been
called note_add.

Other RESTful URLs:

http://tomayko.com/writings/rest-to-my-wife (patronizing, clearly)

http://duncan-cragg.org/blog/post/getting-data-rest-dialogues/

http://www.prescod.net/rest/mistakes/

There will of course be a huge number of such on-line docs.

2)
These code fragments are from an on-line address book app. It uses
Moose, but that won't be evident (or necessary).

In package Local::Contacts::App (notes follow):

# -----------------------------------------------

sub add_note
{
	my($self, $session) = @_;
	my($target_id)      = $self -> param('target_id');
	my($type, $id)      = $self -> decode_target_id($session, $target_id);
	my(%entity_type)    =
	(
	 o => 'organization',
	 p => 'person',
	);
	my($method_name_1)  = $entity_type{$type};
	my($method_name_2)  = "get_${method_name_1}_via_id";
	my($entity)         = $self -> db() -> $method_name_1() ->
$method_name_2($id);
	my($entity_name)    = $$entity{'name'};
	my($input)          = Local::Contacts::App::Validate -> new(app =>
$self) -> add_note();
	my($report)         = $self -> view() -> note() -> report_add($self ->
param('user_id'), $input, $method_name_1, $id, $entity_name);

	$self -> log(__PACKAGE__ . '. Leaving add_note');

	$self -> notes($session, $report);

} # End of add_note.

# -----------------------------------------------

So, sub add_note() is a run-mode.

Then we have:

o All code refers to either a person or an organization.

Hence we can add notes (donations, etc) to either type of entity.

This adds complexity I know, but it's a realistic app so bear with me.

o From my($target_id) down to my($entity) is an experiment of mine. I
wanted to output not the entity's real id, but a disguised version
thereof. This extra complexity you probably don't need.

In fact, I've thought about putting this id-hiding code in a stand-alone
module, and releasing it to CPAN.

o $entity_name is retrieved (in this round-a-bout way) in order to
output it at the top of the page, since it's vital for the user to know
exactly what they're looking at.

o Note how the call to the constructor of Local::Contacts::App::Validate
passes the app, to give access to anything needed.

If you are worried this is an abuse of the 'encapsulation' concept, I
disagree.

o In chains of calls, e.g. $self -> view() -> note() -> report_add(...)
the very last name is always a sub (of course), and all others are
objects in HasA relationships to their callers.

o Besides the id of the entity being updated, the id of the user logged
on is carried around (or can be stuffed into the session, at
code-clean-up time).

3)
In package Local::Contacts::App::Validate:

# --------------------------------------------------

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

	return Data::FormValidator -> check($self -> app(), $self ->
note_profile() );

} # End of add_note.

# --------------------------------------------------

o I call Data::FV directly, and return the object, so it can be passed
into other methods. In the next sub, it's called $result.

4)
In package Local::Contacts::App::View::Note:

# -----------------------------------------------

sub report_add
{
	my($self, $user_id, $result, $entity_type, $id, $name) = @_;
	my($msgs)     = $result -> msgs();
	my(%prompt)   = map{my($s) = $_; $s =~ s/^field_//; $s =~ tr/_/ /; ($_
=> ucfirst $s)} keys %$msgs;
	my($template) = $self -> load_tmpl('update.report.tmpl');
	my($error)    = $result -> has_invalid() || $result -> has_missing();

	$template -> param(error => $$msgs{'error'});

	if ($error)
	{
		my($msg);
		my(@msg);

		for $msg (sort keys %$msgs)
		{
			if ($msg =~ /^field_/)
			{
				push @msg, qq|$prompt{$msg}: $$msgs{$msg}|;
			}
		}

		$template -> param(tr_loop => [map{ {td => $_} } @msg]);
	}
	else
	{
		# Use scalar context to retrieve a hash ref.

		my $note = $result -> valid();

		# Force the user_id into the person's record, so it is available
elsewhere.
		# Note: This is the user_id of the person logged on.

		$$note{'creator_id'} = $user_id;

		# Convert id to table_name_id and table_id.

		$$note{'table_id'} = $id;
		my(%table_name)    =
		(
		 organization => 'organizations',
		 person       => 'people',
		);
		my($table_name)         = $table_name{$entity_type};
		$$note{'table_name_id'} = ${$self -> app() -> db() ->
table_names()}{$table_name};

		delete $$note{'target_id'};
		delete $$note{'submit_note_add'};

		$self -> log('-'x 50);
		$self -> log("Adding note ..."); # Skip note because of
Log::Dispatch::DBI's limit.
		$self -> log("$_ => $$note{$_}") for sort grep{! /^note$/} keys %
$note;
		$self -> log('-'x 50);

		$template -> param(message => $self -> app() -> db() -> note() ->
add($note, $name) );
	}

	$self -> log(__PACKAGE__ . '. Leaving report_add');

	return $template -> output();

} # End of report_add.

# -----------------------------------------------

o The comment in the last sub,
# Convert id to table_name_id and table_id
shows I can log transactions per row per table, if I want.

o The deletes just below that simply zap stuff in the hash but which are
not to be saved.

o This line
$template -> param(error => $$msgs{'error'});
may of course output nothing. I hope so!

5) The last line of the first sub above calls notes(), to display the
new note. This latter sub can be called directly (i.e. as a run mode).
It displays all notes for the given entity, and hence the new note.

It looks like:

# -----------------------------------------------

sub notes
{
	my($self, $session, $report) = @_;
	my($target_id)   = $self -> param('target_id');
	my($type, $id)   = $self -> decode_target_id($session, $target_id);
	my(%entity_type) =
	(
	 o => 'organization',
	 p => 'person',
	);
	my($method) = "notes_for_$entity_type{$type}";

	$self -> log(__PACKAGE__ . '. Leaving notes');

	print $self -> $method($session, $target_id, $id, $report);

} # End of notes.

# -----------------------------------------------

which in turn calls:

# -----------------------------------------------

sub notes_for_organization
{
	my($self, $session, $target_id, $id, $report) = @_;
	my($organization)                  = $self -> db() -> organization() ->
get_organization_via_id($id);
	my($note)                          = $self -> db() -> note() ->
get_notes('organizations', $id);
	$$session{'organization_note_map'} = $self ->
initialize_note_map($note);
	my($result)                        = $self -> view() -> note() ->
display($self -> view() -> util(), $target_id, $organization, $note,
'organization', $report);

	$self -> log(__PACKAGE__ . '. Leaving notes_for_organization');

	return $result;

} # End of notes_for_organization.

# -----------------------------------------------

o Similarly for sub notes_for_person().

6)

o And now for the pay-off. See how I (think I) handle your problem of
"sub-run modes". I'd say my equivalent is 'if ($error)' in the sub
report_add().

2 cases, sure, but not all that complex.

o This is a big app, with currency conversion for the donations, names,
addresses, notes, and multiples of (most of) these per entity. But the
above pattern of code structure is repeated many times. And hence it is
not as bad as it might seem.

o My next task is to remove the Apache-specific code and switch to
CGI::Application::Dispatch.

HTH. Keep the discussion rolling!

-- 
Ron Savage
ron at savage.net.au
http://savage.net.au/index.html




More information about the cgiapp mailing list