Catalyst Workshop: The Exercise
From TipperWiki
| You missed it!
This workshop was held on Dec. 14 2009. Email the Omaha Perl Mongers mailing list if you'd like us to schedule another workshop. |
These are the /gl?orious/ details of the Catalyst Workshop. You do not need to know or understand what's going on here in advance of the workshop. If you bring a general comfort with ssh'ing into a Linux box and typing commands you should be fine. Unless I turn out to be a worse guide than we all hope I am. :) --Jhannah 00:34, 24 November 2009 (UTC)
Contents |
Registrants
Catalyst ships with a splendid little development server built in. We'll each be running our own dev server, which will be running our own copies of the exercise. Conceptually, the dev server is like running a single-process Apache server, serving YourApp only, and with lots of Catalyst-specific bells and whistles thrown in for free.
So, we need to assign the port that each registrant will use on the server. These ports will only work when the user assigned that port has their dev server running.
Full Name username Dev server location ------------------- ----------- --------------------------------- Jay Hannah jhannah http://clab.ist.unomaha.edu:3000/ Matt Payne mpayne http://clab.ist.unomaha.edu:3001/ Robert Fulkerson rfulkerson http://clab.ist.unomaha.edu:3002/ Todd Hamilton thamilton http://clab.ist.unomaha.edu:3003/ Dave Thacker dthacker http://clab.ist.unomaha.edu:3004/ Zainab Aljubran zaljubran http://clab.ist.unomaha.edu:3005/ Pedro Buenrostro pbuenrostro http://clab.ist.unomaha.edu:3006/ John Dickson jdickson http://clab.ist.unomaha.edu:3007/ Ben Hefner bhefner http://clab.ist.unomaha.edu:3008/ Branden Folino-Haye bfolinohaye http://clab.ist.unomaha.edu:3009/ Paul Birdsong pbirdsong http://clab.ist.unomaha.edu:3010/ Eric Kennedy ekennedy http://clab.ist.unomaha.edu:3011/ Ishwor Thapa ithapa http://clab.ist.unomaha.edu:3012/ Add yourself! Or, register via email. There are 30 PCs in PKI 276. So unless some people are on wireless using their own laptops, only the first 30 registrants can participate.
Catalyst "Hello, world"
- PLEASE feel free to interrupt me with questions at any time, or if you get stuck. This is my first attempt at a Catalyst Workshop, so my attempt to balance covering enough detail vs. too much detail may need adjustment repeatedly as we go. Thanks! --Jhannah 12:20, 8 December 2009 (UTC)
Let's tell Catalyst what port we want the dev server to run on. My port, assigned above, is 3000. Find your port in the list above, and set the environment variable accordingly:
$ export CATALYST_PORT=3000
Now we'll create our Catalyst application. Since everyone loves dirt biking (right? -grin-), we're going to write a little application to log all our off-road adventures. We'll call this application Trails. Create a subdirectory for all your source code, cd into it, and create your application.
$ mkdir src $ cd src ~/src$ catalyst.pl Trails
Now we'll start up the dev server:
~/src$ cd Trails/ ~/src/Trails$ ./script/trails_server.pl -k
Leave your dev server running in PuTTY, and use your web browser to hit it at your assigned URL above.
Congratulations, you're a Catalyst developer! :)
Notice all the debug information that appears in your dev server every time you hit Reload in your browser. We'll talk about all that briefly.
The scaffolding lays out like so:
-
trails.conf- configuration settings -
root/- all our templates (HTML, etc.) -
script/- helpers and the dev server -
t/- unit tests for our application -
lib/Trails.pm- plugins and plugin configuration -
lib/Trails/Controller/- each of our Actions (a subroutine mapped to a URL) lives in a class in this directory. A typical application has lots of controllers. -
lib/Trails/Model/- data persistence layers. We'll have just one for our SQLite database. -
lib/Trails/View/- ways to interface with our application. A typical application has just one, for Template Toolkit.
Creating a View (Template Toolkit)
Being a microcosm of Perl, there's lots of ways to do everything in Catalyst. Template Toolkit is probably the most popular templating system, but there are many others.
Let's leave our development server running in one PuTTY session, and make changes in another. (You can use the -r switch to have the dev server automatically reload every time you change source code, but let's leave that until you're more accustomed to what the dev server looks like in action.)
So in a 2nd PuTTY Window, let's create a view.
~/src/Trails$ ./script/trails_create.pl view TT TT
Let's also create a Controller quick so we can map out some URLs:
~/src/Trails$ ./script/trails_create.pl controller Parks
Whenever we change our source code we'll go to our dev server window, hit Control-C to kill our application, then up-arrow and Enter to restart it. Let's do that now. Once it's restarted, we can now hit our new Controller and see what happens:
http://clab.ist.unomaha.edu:3000/parks ^^^^ change to your port number
You should see this:
Matched Trails::Controller::Parks in Parks.
Which is a pretty boring result. Let's add a subroutine to display a list of parks:
~/src/Trails$ cd lib/Trails/Controller ~/src/Trails/lib/Trails/Controller$ vi Parks.pm
Find the subroutine index(). Below that one, let's add a new subroutine called list(), which will list all the parks.
=head2 index
=cut
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->response->body('Matched Trails::Controller::Parks in Parks.');
}
=head2 list
Display a list of all parks.
=cut
sub list : Local {
my ($self, $c) = @_;
}
Notes:
- "Local" is one Catalyst Action type
- If that colon freaks you out, read this (attributes)
Restart the dev server, hit the URL for this subroutine, and you'll throw a fascinating error:
http://clab.ist.unomaha.edu:3000/parks/list ^^^^ change to your port number Couldn't render template "file error - parks/list.tt: not found"
This tells us exactly which template file Catalyst is now expecting to find, and we haven't provided. Let's create that file:
$ cd ~/src/Trails/root/ ~/src/Trails/root$ mkdir parks ~/src/Trails/root$ cd parks ~/src/Trails/root/parks$ vi list.tt
<ul> <li>Park 1</l1> <li>Park 2</l1> <li>Park 3</l1> <li>...</l1> </ul>
Now hit Reload on that URL and we should see our little list of parks.
(Did you notice that you didn't have to reload the dev server when you changed your templates? You only have to reload the dev server when you change Perl classes, not when you change Template Toolkit files.)
Progress, eh? At this point we're using lots of Catalyst defaults. Let's review what's happening. When we hit the URL /parks/list:
- Catalyst dispatches to the Controller Parks.pm (
lib/Trails/Controller/Parks.pm), the first part of the URL. - It runs the subroutine
list(), the second part of the URL. (Subroutines in Controllers that are mapped to URLs are called "Actions.") There's no code in our subroutine, so not much happens in there. -grin-- (Pro tip: Catalyst flow chart.)
- Catalyst uses Template Toolkit to display the template for this "Action."
- The default template file has the same path as the URL we're hitting, starting in the template root
root/. So this template isroot/parks/list.tt.
WRAPPER
Often a web site will have lots of common HTML, CSS, and JS above and below the content of any given page. Template Toolkit has a WRAPPER directive that does just that. Let's add one.
$ cd ~/src/Trails/lib/Trails/View ~/src/Trails/lib/Trails/View$ vi TT.pm
Change this line:
__PACKAGE__->config(TEMPLATE_EXTENSION => '.tt');
to this:
__PACKAGE__->config( TEMPLATE_EXTENSION => '.tt', WRAPPER => 'wrapper.tt', );
Then create a wrapper.tt template like so:
$ cd ~/src/Trails/root ~/src/Trails/root$ vi wrapper.tt
<h1>Trails</h1> [% content %] <br><br><br><hr> (c) 2009 Neato Stuff, Inc.
Restart your dev server, hit that URL again, and you should see your header and footer pop in. Add all the fancy graphics, stylesheets, and javascript / flash / whatever you desire.
Creating and Using a Model
Most applications use an RDBMS to store data. We'll use SQLite for this application. Once finished, this is easily portable to MySQL or Oracle or Postgres or whatever makes you happy.
There's lots of ways to handle this in Catalyst, and many helpers try to build these things automatically for you. The following is my preferred method. We're going to create a SQLite database manually, and then use DBIx::Class::Schema::Loader to create a static set of DBIx::Class objects that we can use for OO database manipulation.
Often you have many things outside of this one Catalyst application that need to manipulate your database, so let's create a stand-alone ORM, then hook Catalyst to it.
$ cd ~/src/ ~/src$ mkdir Schema ~/src$ cd Schema ~/src/Schema$ vi trailsdb.sql
create table parks ( id INTEGER PRIMARY KEY AUTOINCREMENT, state text, description text ); create table users ( username text PRIMARY KEY, password text ); create table journal_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, username text, park_id integer, entry text, timestamp text );
~/src/Schema$ sqlite trailsdb.sqlite < trailsdb.sql ~/src/Schema$ vi refresh.sh
perl -MDBIx::Class::Schema::Loader=make_schema_at,dump_to_dir:../Schema \
-e 'make_schema_at("TrailsDB", { debug => 1 }, [ "dbi:SQLite:trailsdb.sqlite" ])'
~/src/Schema$ chmod +x refresh.sh ~/src/Schema$ ./refresh.sh
Hey, look at that. DBIx::Class::Schema::Loader auto-discovered our database schema for us and created one class per table. We can now use these classes in any Perl we want to (including our new little Trails application) to manipulate our database via DBIx::Class. That might come in handy. (For the most part, you can hand any DBI connection string for any RDBMS in and this will work. Try it on your databases at $work!)
As a quick tangent, let's see how we might use these classes outside of Catalyst:
~/src/Schema$ vi add_a_row.pl
#!/usr/bin/perl
use TrailsDB;
my $schema = TrailsDB->connect('dbi:SQLite:/home/jhannah/src/Schema/trailsdb.sqlite');
# ^^^ your username here
$new_park = $schema->resultset('Parks')->create({
id => 1,
state => 'IA',
description => 'River Valley OHV Park'
});
~/src/Schema$ export DBIC_TRACE=1 ~/src/Schema$ chmod +x add_a_row.pl ~/src/Schema$ ./add_a_row.pl
Hopefully this demonstrates that it's easy to use DBIx::Class in little stand-alone programs, and easy to add DBIx::Class to any existing Perl program. Feel free to run whatever mixture of traditional DBI and DBIx::Class you want to in your applications. DBIx::Class is great for CRUD, but I use DBI directly whenever I have complex joins or read-only statistics type SQL to run, or whenever the magic of SQL::Abstract overwhelms me. :)
If, at any point, you want to nuke your database and start over, you can do so like this:
~/src/Schema$ rm trailsdb.sqlite ~/src/Schema$ sqlite trailsdb.sqlite < trailsdb.sql
Now let's hook Catalyst into our stand-alone Model.
$ cd ~/src/Trails/lib/Trails/Model ~/src/Trails/lib/Trails/Model$ vi TrailsDB.pm
package Trails::Model::TrailsDB;
use strict;
use base 'Catalyst::Model::DBIC::Schema';
use lib '/home/jhannah/src/Schema';
# ^^^ your username
use TrailsDB;
__PACKAGE__->config(
schema_class => 'TrailsDB',
connect_info => [
'dbi:SQLite:/home/jhannah/src/Schema/trailsdb.sqlite',
# ^^^ your username
],
);
That should do it. Let's run over and start our dev server (or re-start it if you still have in running in a window):
$ cd ~/src/Trails ~/src/Trails$ ./script/trails_server.pl -k
Catalyst seems happy? Excellent. Let's leave the dev server running in this window, and use our other window to go change our list of parks to pull from the database instead of the hard-coded list we added earlier.
$ cd ~/src/Trails/root/parks ~/src/Trails/root/parks$ vi list.tt
List of all parks:
<ul>
[% FOREACH park IN c.model('TrailsDB::Parks').search({}) %]
<li>[% park.description %]</li>
[% END %]
</ul>
Now when we hit our URL again
http://clab.ist.unomaha.edu:3000/parks/list ^^^^ your port
We see that our park list comes from our database. Neato. :)
Forms
- Warning: I use an extremely non-magical approach to handling forms. If you prefer magic, check out FormFu or Reaction. If you get those working, please present them at an upcoming meeting, I'd love to see your take on them! :) --Jhannah 13:05, 8 December 2009 (UTC)
Let's add a way to add a park. We'll add it as the URL /parks/add. Since we're using all the Catalyst defaults that means we need a new subroutine add() inside Parks.pm. Let's add it below list().
~/src/Trails/lib/Trails/Controller$ vi Parks.pm
=head2 add
Add a new park.
=cut
sub add : Local {
my ($self, $c) = @_;
}
(At this point, you might want 3 PuTTYs open. One for the dev server, one in your Controllers directory, and one in your templates directory (root/parks).)
~/src/Trails/root/parks$ vi add.tt
<form> <input name="state"> State<br/> <input name="description"> Description<br/> <input type="submit" value="add park"> </form> [% INCLUDE parks/list.tt %]
Restart your dev server and hit your URL
http://clab.ist.unomaha.edu:3000/parks/add ^^^^ your port
We'll discuss everything that happened here briefly. Check out the dev server output every time you submit the form. :)
But you'll notice that new parks are not actually being added to the database. Let's add code that makes that happen to our Controller.
~/src/Trails/lib/Trails/Controller$ vi Parks.pm
=head2 add
Add a new park.
=cut
sub add : Local {
my ($self, $c) = @_;
if ($c->req->param) {
$c->model('TrailsDB::Parks')->create({
state => $c->req->param('state'),
description => $c->req->param('description'),
});
}
}
Cool. We've now added a zero-security method of adding parks to the database. I can't see what state each park is in though. Bummer. Let's tweak our list.tt template, adding the state field:
~/src/Trails/root/parks$ vi list.tt
List of all parks:
<ul>
[% FOREACH park IN c.model('TrailsDB::Parks').search({}) %]
<li>[% park.description %] ([% park.state %])</li>
[% END %]
</ul>
Ahh... much better. :)
In a real application, or course, you can't just let anybody add any garbage to your database. You'll need authentication, authorization, and form validation.

