59
Nordaaker Introduction to Catalyst Now we’re cooking with fire

Introduction To Catalyst

Embed Size (px)

DESCRIPTION

A practical introduction to using the Catalyst MVC framework, showing how to make a simple application with Catalyst, DBIx::Class and TT You can also find the video for this presentation at http://www.devtv.eu/wp/2009/04/marcus-ramberg-introduction-to-catalyst/

Citation preview

Page 1: Introduction To Catalyst

Nordaaker

Introduction to Catalyst

Now we’re cooking with fire

Page 2: Introduction To Catalyst

About Marcus

• Oslo Perl Monger

• Catalyst Release Manager

• Nordaaker Ltd

Page 3: Introduction To Catalyst

Today’s dish

• PizzaShop

• Ingredients• DBIx::Class

• Extensible and flexible ORM

• Template Toolkit• Template Processing System

• Catalyst • The Elegant MVC Framework

MVC

Page 4: Introduction To Catalyst

Installing from CPAN

• cpan Task::Catalyst• [DBIx::Class Support]

• [Template::Toolkit Support]

• [Authentication/Authorization Support]

• [HTML::FormFu Support]

Page 5: Introduction To Catalyst

Preparing the skeleton

• Every web-app needs a lot of the same things

• So you keep repeating a lot• Talk to web server• Load config files• Connect to the database• render a template• etc ...

Page 6: Introduction To Catalyst

Yak Shaving

Page 7: Introduction To Catalyst

Setting up the Catalyst app

# Catalyst gets rid of repeat work

~Source $ catalyst.pl PizzaShop

created "PizzaShop"

created "PizzaShop/script"

created "PizzaShop/lib"

created "PizzaShop/root"

....

created "PizzaShop/script/pizzashop_fastcgi.pl"

created "PizzaShop/script/pizzashop_server.pl"

created "PizzaShop/script/pizzashop_test.pl"

created "PizzaShop/script/pizzashop_create.pl"

~/Source $

Page 8: Introduction To Catalyst

Our skeleton

• Is a full CPAN package

• Contains standalone and fastcgi server scripts

• loads config automatically

• testing stubs

• helpers to create more parts of your app

Page 9: Introduction To Catalyst

Generated files

Command-Central:PizzaShop marcus$ ls * Changes Makefile.PL README pizzashop.conf

lib:PizzaShop PizzaShop.pm

root:favicon.ico static

script:pizzashop_cgi.pl pizzashop_fastcgi.pl pizzashop_test.plpizzashop_create.pl pizzashop_server.pl

t:01app.t 02pod.t 03podcoverage.t

Page 10: Introduction To Catalyst

in ~/Source/PizzaShop

$ cat README

Run script/pizzashop_server.pl to test the application.

$ ./script/pizzashop_server.pl

[debug] Debug messages enabled

[debug] Statistics enabled

[debug] Loaded plugins:

.----------------------------------------------------------------------------.

| Catalyst::Plugin::ConfigLoader 0.20 |

| Catalyst::Plugin::Static::Simple 0.20 |

'----------------------------------------------------------------------------'

[debug] Loaded dispatcher "Catalyst::Dispatcher"

[debug] Loaded engine "Catalyst::Engine::HTTP"

[debug] Found home "/Users/marcus/Source/PizzaShop"

[debug] Loaded Config "/Users/marcus/Source/PizzaShop/pizzashop.conf

...

Page 11: Introduction To Catalyst

Continued

[debug] Loaded Private actions:

.----------------------+--------------------------------------+--------------.

| Private | Class | Method |

+----------------------+--------------------------------------+--------------+

| /default | PizzaShop::Controller::Root | default |

| /end | PizzaShop::Controller::Root | end |

| /index | PizzaShop::Controller::Root | index |

'----------------------+--------------------------------------+--------------'

[debug] Loaded Path actions:

.-------------------------------------+--------------------------------------.

| Path | Private |

+-------------------------------------+--------------------------------------+

| / | /default |

| / | /index |

'-------------------------------------+--------------------------------------'

[info] PizzaShop powered by Catalyst 5.7099_03

You can connect to your server at http://command-central:3000

Page 12: Introduction To Catalyst

And in your browser

Page 13: Introduction To Catalyst

Standalone server options

Usage: pizzashop_server.pl [options]

Options: -d -debug force debug mode -f -fork handle each request in a new process (defaults to false) -? -help display this help and exits -host host (defaults to all) -p -port port (defaults to 3000) -k -keepalive enable keep-alive connections -r -restart restart when files get modified (defaults to false) -rd -restartdelay delay between file checks -rr -restartregex regex match files that trigger a restart when modified (defaults to '\.yml$|\.yaml$|\.conf|\.pm$') -restartdirectory the directory to search for modified files, can be set mulitple times (defaults to '[SCRIPT_DIR]/..') -follow_symlinks follow symlinks in search directories (defaults to false. this is a no-op on Win32)

Page 14: Introduction To Catalyst

Let’s check on the action that did this

# In lib/PizzaShop/Controller/Root.pm

sub index :Path :Args(0) {

my ( $self, $c ) = @_;

# Hello World

$c->response->body( $c->welcome_message );

}

Page 15: Introduction To Catalyst

Moving on to TT

• Kind of boring to write all your html in .pm files

• Would like to render some templates kthx

• Will need to create a View

• Catalyst can do that for us tho

Page 16: Introduction To Catalyst

Create script

Command-Central:PizzaShop marcus$ ./script/pizzashop_create.pl

Usage:

pizzashop_create.pl [options] model|view|controller name [helper]

[options]

Options:

-force don't create a .new file where a file to be created exists

-mechanize use Test::WWW::Mechanize::Catalyst for tests if available

-help display this help and exits

Examples:

pizzashop_create.pl controller My::Controller

pizzashop_create.pl controller My::Controller BindLex

pizzashop_create.pl -mechanize controller My::Controller

pizzashop_create.pl view My::View

pizzashop_create.pl view MyView TT

pizzashop_create.pl view TT TT

pizzashop_create.pl model My::Model

pizzashop_create.pl model SomeDB DBIC::Schema MyApp::Schema create=dynamic\

dbi:SQLite:/tmp/my.db

pizzashop_create.pl model AnotherDB DBIC::Schema MyApp::Schema create=static\

dbi:Pg:dbname=foo root 4321

Page 17: Introduction To Catalyst

Create the view

# Let’s call it Default

$ ./script/pizzashop_create.pl view Default TT

exists "/Users/marcus/Source/PizzaShop/script/../lib/PizzaShop/View"

exists "/Users/marcus/Source/PizzaShop/script/../t"

created "/Users/marcus/Source/PizzaShop/script/../lib/PizzaShop/View/Default.pm"

created "/Users/marcus/Source/PizzaShop/script/../t/

$

Page 18: Introduction To Catalyst

lib/PizzaShop/View/Default.pm

package PizzaShop::View::Default;

use strict;

use base 'Catalyst::View::TT';

__PACKAGE__->config(TEMPLATE_EXTENSION => '.tt');

1;

# And a POD template

Page 19: Introduction To Catalyst

Connecting the renderer with the view

• catalyst.pl has already done this for you:

sub end : ActionClass('RenderView') {}

• RenderView will try to render a template unless the body is set or there is a redirect in place

• Uses $c->config->{default_view} and $c->stash->{current_view} to decide if more than one view

Page 20: Introduction To Catalyst

So let’s clear the body

# In lib/PizzaShop/Controller/Root.pm

sub index :Path :Args(0) {

my ( $self, $c ) = @_;

# Hello World

$c->response->body( $c->welcome_message );

}

Page 21: Introduction To Catalyst

In the browser

Page 22: Introduction To Catalyst

Ok, let’s make root/index.tt

<html><head><title>Menu</title></head><body><ul>[%FOREACH pizza IN [ 1 .. 10 ] %]<li>Pizza nr [% pizza %]</li>[% END %]</ul></body></html>

Page 23: Introduction To Catalyst

In the browser

Page 24: Introduction To Catalyst

What comes before VC ?

• It’s time for the Model of course

• Like with any good cooking show, I have been cheating

• Here is a SQL database I prepared the other night:

Page 25: Introduction To Catalyst

product.sql

CREATE TABLE product (

id INT PRIMARY KEY AUTOINCREMENT,

name TEXT,

description TEXT,

price INT,

category_id INT REFERENCES category

);

CREATE TABLE category (

id INT PRIMARY KEY AUTOINCREMENT,

name TEXT

);

Page 26: Introduction To Catalyst

Doing the dirty work

★ So I stuffed this thing into a sqlite database:

$ cat product.sql | sqlite product.db

★ Then let catalyst make a model from it

$ ./script/pizzashop_create.pl model DB DBIC::Schema PizzaShop::Schema \

create=static dbi:SQLite:product.db

exists "/Users/marcus/Source/PizzaShop/script/../lib/Pizza/Shop/Model"

exists "/Users/marcus/Source/PizzaShop/script/../t"

Dumping manual schema for PizzaShop::Schema to directory /Users/marcus/Source/PizzaShop/script/../lib ...

Schema dump completed.

created "/Users/marcus/Source/PizzaShop/script/../lib/PizzaShop/Model/DB.pm"

created "/Users/marcus/Source/PizzaShop/script/../t/model_DB.t"

Page 27: Introduction To Catalyst

The Generated Schema

package Pizza::Shop::Schema::Product;use strict;use warnings;use base 'DBIx::Class';__PACKAGE__->load_components("Core");__PACKAGE__->table("product");__PACKAGE__->add_columns( "id", { data_type => "INT", is_nullable => 0, size => undef }, "name", { data_type => "TEXT", is_nullable => 0, size => undef }, "description", { data_type => "TEXT", is_nullable => 0, size => undef }, "price", { data_type => "INT", is_nullable => 0, size => undef }, "category", { data_type => "INT", is_nullable => 0, size => undef },);__PACKAGE__->set_primary_key("id");__PACKAGE__->belongs_to( "category_id", "Pizza::Shop::Schema::Category", { id => "category_id" } );

Page 28: Introduction To Catalyst

Let’s get cooking

• Our ingredients are pretty much ready now.

• Create a controller to put our Menu in:PizzaShop $ ./script/pizzashop_create.pl controller Menu

created "/Users/marcus/Source/PizzaShop/script/../lib/PizzaShop/Controller/Menu.pm"

created "/Users/marcus/Source/PizzaShop/script/../t/controller_Menu.t"

• Let’s start with something easy

Page 29: Introduction To Catalyst

Peeking at pizzas

# The controller code needed for a view action:

sub detail : Local Args(1) {

my ($self,$c,$pizza_id) = @_;

$c->stash->{pizza} =

$c->model('DB::Product')->find($product_id);

$c->detach('/default') unless $c->stash->{pizza};

}

# root/menu/detail.tt

[%PROCESS header.tt%]

<h1>[% pizza.name %] ([% pizza.category.name %])</h1>

<h2>[% pizza.description %]</h2>

[%PROCESS footer.tt%]

Page 30: Introduction To Catalyst

Header & Footer

# header.tt<html>[% DEFAULT title ='Welcome' %]<head><title> PizzaShop - [% title %]</head><body>

# footer.tt</body></html>

Page 31: Introduction To Catalyst

Alternative: wrapper.tt

• Add WRAPPER=>’...’ to TT view config

• wrapper file calls [% content %]

• Personal preference

• Need to solve templates that does not need wrapper

Page 32: Introduction To Catalyst

And when we restart the server:

| /menu/detail | PizzaShop::Controller::Menu | detail |

Page 33: Introduction To Catalyst

Neat distraction:

DBIC_TRACE=1 ./script/pizzashop_server.pl

SELECT me.id, me.name, me.description, me.price, me.category FROM product me WHERE ( ( me.id = ? ) ): '1'

SELECT me.id, me.name FROM category me WHERE ( ( ( me.id = ? ) ) ): '1'

#which kind of sucks, so let’s do this:$c->model('DB::Product') ->search({id=>$pizza_id}, {prefetch=>[qw/category/]}) ->first;SELECT me.id, me.name, me.description, me.price, me.category, category.id, category.name FROM product me JOIN category category ON ( category.id = me.category ) WHERE ( me.id = ? ): '1'

Page 34: Introduction To Catalyst

Listing items

Find this bit in lib/PizzaShop/Controller/Menu.pm

sub index :Path :Args(0) {

my ( $self, $c ) = @_;

$c->response->body(

'Matched PizzaShop::Controller::Menu in Menu.');

}

Page 35: Introduction To Catalyst

Listing items

Change it slightly like this

sub index :Path :Args(0) {

my ( $self, $c ) = @_;

$c->response->body(

'Matched PizzaShop::Controller::Menu in Menu.');

$c->stash->{menu}=$c->model(‘DB::Product’);

}

Page 36: Introduction To Catalyst

Listing items

And let’s create root/menu/index.tt:

[% PROCESS header.tt title=’Menu’ -%]

<h1>Menu</h1>

<ul>

[% WHILE( product=products.next ) -%]

<li><a href="[%c.uri_for('/product/view',product.id)%]">

[%product.name%]</a></li>

[% END -%]

</ul>

[% PROCESS footer.tt %]

Page 37: Introduction To Catalyst

Meanwhile, in the browser ...

Page 38: Introduction To Catalyst

Paging

In order to digest the full menu we’d like to split it up into pages. Let’s find this stanza in Menu.pm:$c->stash->{menu}= $c->model(DB::Menu’)And we’ll add this before the ;->search({},{ rows=>8, page=>$c->req->params->{page'} || 1 });

Page 39: Introduction To Catalyst

Then in the index.tt,

...

[%product.name%]</a></li>

[%END%]

</ul>

[% PROCESS pager.tt pager=menu.pager -%]

Page 40: Introduction To Catalyst

pager.tt

The resultset object provides access to a Data::Page object, which makes paging easy and reusable:

[% IF ( pager.current_page != pager.first_page) %]

<a href="[%c.req.uri_with({page=>pager.previous_page})%]">

« Previous</a>

[% END %]

[% first = ( pager.current_page > 9 ? (pager.current_page - 9) : 1 )%]

[% last = ( pager.current_page + 9 < pager.last_page ? (pager.current_page + 9) : pager.last_page ) %]

[% FOREACH page IN [first .. last] %]

[%IF (pager.current_page == page)%] [%page%] [% ELSE %]

<a href="[%c.req.uri_with({page=>pager.next_page})%]">[%page%]</a>

[%END%] [%END%]

[% IF ( pager.current_page != pager.last_page ) %]

<a href="[%c.req.uri_with({page=>pager.next_page})%]">Next »</a>

[% END %]

Page 41: Introduction To Catalyst

Et voila:

Page 42: Introduction To Catalyst

One, Two, Many

★ Wouldn’t it be nice to be able to filter by category?

★ Let's add another line to the index action:

$c->stash->{categories} =

$c->model('DB::Category')->search({},

{ join => [qw/ products /],

select => [ 'me.id','me.name',

{ count => 'products.id' } ],

as => [qw/id name product_count/],

group_by => [qw/ me.id /] });

Page 43: Introduction To Catalyst

And in index.tt

[% PROCESS categories.tt %]

# categories.tt:

<div style=”float:right”>

[% WHILE (category = categories.next) %]

<li> <a href="[%c.uri_for(

'/menu/by_category',category.id)%]">

[%category.name%]</a>

([%category.get_column(

'product_count')%])</li>

[%END%]

</div>

Page 44: Introduction To Catalyst

Results in:

Page 45: Introduction To Catalyst

Refactoring:

• The code Menu.pm is a bit messy

• should refactor it into a Resultset class

• $c->model(‘DB::Category’)

->with_counts()

• Left as an excercise for you.

Page 46: Introduction To Catalyst

Desert: Form generation and validation

• Let’s make another controller Restricted::Menu

• And change the base class

use parent 'Catalyst::Controller::HTML::FormFu';

• Also, make it dispatch to /menu

__PACKAGE__->config('path'=>'menu');

Page 47: Introduction To Catalyst

And add an action:

sub add : FormConfig Local Args(0) {

my ($self,$c) = @_;

my $form = $c->stash->{form};

my $product = $c->model('DB::Product')->new({});

$form->default_values($product);

if ($form->submitted && !$form->has_errors) {

$form->model->update( $product );

$c->res->redirect($c->uri_for('/menu'));

}

}

Page 48: Introduction To Catalyst

root/restricted/menu/add.tt

[% PROCESS header.tt title=’Add product’ -%][% form %][% PROCESS footer.tt-%]

Page 49: Introduction To Catalyst

root/forms/restricted/menu/add.yml

---action: indicator: submitelements: - type: Text name: name label: Title constraints: - Required - Word - type: Text name: description label: Description filters: - HTMLScrubber - type: Select name: category label: Category model_config: label_column: name id_column: id model: 'DB::Category’ - type: Submit name: submit add_attributes: value: Add to menu

Page 50: Introduction To Catalyst

Renders like:

Page 51: Introduction To Catalyst

★Ugly★HTML::FormFu includes

some sample css:examples/vertically-aligned/

Page 52: Introduction To Catalyst

A bit better

Page 53: Introduction To Catalyst

Bonus slides: Adding Auth

PizzaShop.pm:use Catalyst qw/-Debug

Authentication

ConfigLoader

Static::Simple

Session

Session::Store::FastMmap

Session::State::Cookie/;

Root.pm:sub login : Global Args(0) {

my ( $self, $c ) = @_;

if ($c->authenticate({

username => $c->req->params->{user},

login => $c->req->params->{pass} }) ) { ... }

}

Page 54: Introduction To Catalyst

Login template

root/login.tt:[%PROCESS header.tt%]<h2>Please log in</h2><form action="[%c.req.uri%]" method="post"> <fieldset> <p>[%alert%]</p> <dl> <dt>Login:</dt> <dd><input name="user" type="text" /></dd> <dt>Password:</dt> <dd><input name="pass" type="password" /></dd> </dd> <input type="submit" class="submit" /> </form>

[% PROCESS footer.tt%]

Page 55: Introduction To Catalyst

And configuration (PizzaShop.conf):

<authentication> default_realm dbic <realms> <dbic> <credential> class Password password_field password password_type clear </credential> <store> class DBIx::Class user_class DB::Users id_field username </store> </dbic></realms>

Page 56: Introduction To Catalyst

DB::Person?

• Just update the SQL:

CREATE TABLE person (

username VARCHAR(25) PRIMARY KEY

password VARCHAR(25)

);

• Then regenerate your schema to get the Person.pm file

Page 57: Introduction To Catalyst

And a simple restricted controller

package PizzaShop::Controller::Restricted;

...

sub auto :Private {

my ($self,$c) = @_;

return 1 if $c->user_exists;

$c->res->redirect($c->uri_for(‘/login’));

return 0;

}

Page 58: Introduction To Catalyst
Page 59: Introduction To Catalyst

Thank you

★ Questions?★ If you come up with anything later

[email protected]