-# Manual.pm
+# Manual.pm
# Copyright (c) 2006 Jonathan Rockway <jrockway@cpan.org>
package Catalyst::Manual;
Install L<Task::Catalyst::Tutorial|Task::Catalyst::Tutorial> to
install all the dependencies you need to follow along with the
-Tutorial. You can also refer to
+Tutorial. You can also refer to
L<Catalyst::Manual::Tutorial::Intro|Catalyst::Manual::Tutorial::01_Intro>
for more information on installation options.
=item *
-The Definitive Guide to Catalyst: Writing Extendable, Scalable and
+The Definitive Guide to Catalyst: Writing Extendable, Scalable and
Maintainable Perl-Based Web Applications
-By: Kieren Diment, Matt Trout
+By: Kieren Diment, Matt Trout
Available July 12, 2009
ISBN 10: 1-4302-2365-0
ISBN 13: 978-1-4302-2365-8
=head1 NAME
-Catalyst::Manual::Actions - Catalyst Reusable Actions
+Catalyst::Manual::Actions - Catalyst Reusable Actions
=head1 DESCRIPTION
This is pretty simple. Actions work just like the normal dispatch
attributes you are used to, like Local or Private:
- sub Hello :Local :ActionClass('SayBefore') {
+ sub Hello :Local :ActionClass('SayBefore') {
$c->res->output( 'Hello '.$c->stash->{what} );
}
package Catalyst::Action::MyAction;
use Moose;
use namespace::autoclean;
-
+
extends 'Catalyst::Action';
before 'execute' => sub {
my ( $self, $controller, $c, $test ) = @_;
$c->stash->{foo} = 'bar';
};
-
+
1;
and this would be used in a controller like this:
=head2 Catalyst::Action::RenderView
-This is meant to decorate end actions. It's similar in operation to
+This is meant to decorate end actions. It's similar in operation to
L<Catalyst::Plugin::DefaultEnd>, but allows you to decide on an action
level rather than on an application level where it should be run.
of L<Moose> attributes.
Most of the accessors to information gathered during compile time (such
-as configuration) are managed by C<Catalyst::ClassData>, which is a
+as configuration) are managed by C<Catalyst::ClassData>, which is a
L<Moose>-aware version of L<Class::Data::Inheritable> but not compatible
with L<MooseX::ClassAttribute>.
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
-
+
=head2 Controller Roles
It is possible to use roles to apply method modifiers on controller actions
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller' };
-
- sub foo : Local {
+
+ sub foo : Local {
my ($self, $c) = @_;
$c->res->body('Hello ');
}
my ($self, $c) = @_;
$c->res->body($c->res->body . 'World');
};
-
+
It is possible to have action methods with attributes inside Moose roles, using
L<MooseX::MethodAttributes>, example:
package MyApp::ControllerRole;
use MooseX::MethodAttributes::Role;
use namespace::autoclean;
-
+
sub foo : Local {
my ($self, $c) = @_;
...
}
-
+
package MyApp::Controller::Foo;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller' };
-
+
with 'MyApp::ControllerRole';
=head1 AUTHORS
=head4 L<Catalyst::Authentication::Store::DBI>
-Allows you to use a plain L<DBI> database connection to identify users.
+Allows you to use a plain L<DBI> database connection to identify users.
=head4 L<Catalyst::Authentication::Store::Htpasswd>
use lib '/var/www/MyApp/lib';
use MyApp;
</Perl>
-
+
<Location />
SetHandler modperl
PerlResponseHandler MyApp
=head2 Other web servers
The proxy configuration above can also be replicated with a different
-frontend server or proxy, such as varnish, nginx, or lighttpd.
+frontend server or proxy, such as varnish, nginx, or lighttpd.
=head1 AUTHORS
RewriteRule ^(.*)$ script/myapp_fastcgi.pl/$1 [PT,L]
Now C<http://mydomain.com/> should now Just Work. Congratulations, now
-you can tell your friends about your new website.
+you can tell your friends about your new website.
=head1 AUTHORS
methods code. You can surround this by overriding the method in a
subclass:
- package Catalyst::Action::MyFoo;
+ package Catalyst::Action::MyFoo;
use Moose;
use namespace::autoclean;
- use MRO::Compat;
+ use MRO::Compat;
extends 'Catalyst::Action';
sub execute {
1;
We are using L<MRO::Compat> to ensure that you have the next::method
-call, from L<Class::C3> (in older perls), or natively (if you are using
-perl 5.10) to re-dispatch to the original C<execute> method in the
+call, from L<Class::C3> (in older perls), or natively (if you are using
+perl 5.10) to re-dispatch to the original C<execute> method in the
L<Catalyst::Action> class.
The Catalyst dispatcher handles an incoming request and, depending
-upon the dispatch type, will call the appropriate target or chain.
+upon the dispatch type, will call the appropriate target or chain.
From time to time it asks the actions themselves, or through the
controller, if they would match the current request. That's what the
C<match> method does. So by overriding this, you can change on what
For example, the action class below will make the action only match on
Mondays:
- package Catalyst::Action::OnlyMondays;
+ package Catalyst::Action::OnlyMondays;
use Moose;
use namespace::autoclean;
use MRO::Compat;
package MyApp::Controller::Foo;
use Moose;
use namespace::autoclean;
-
+
BEGIN { extends 'MyApp::Base::Controller::ModelBase'; }
__PACKAGE__->config( model_name => 'DB::Foo',
package Catalyst::View::MyView;
use Moose;
use namespace::autoclean;
-
+
extends 'Catalyst::View';
sub process {
if (!blessed($_[0]) || !$_[0]->isa('Catalyst::Action'));
return $uri;
};
-
+
Note that Catalyst will load any Moose Roles in the plugin list,
and apply them to your application class.
package CatalystX::Component::Foo;
use Moose;
use namespace::autoclean;
-
+
extends 'Catalyst::Component';
sub COMPONENT {
my $class = shift;
# Note: $app is like $c, but since the application isn't fully
- # initialized, we don't want to call it $c yet. $config
+ # initialized, we don't want to call it $c yet. $config
# is a hashref of config options possibly set on this component.
my ($app, $config) = @_;
$c->stash->{message} = 'Hello World!';
$self->check_message( $c, 'test1' );
}
-
+
sub check_message {
my ( $self, $c, $first_argument ) = @_;
# do something...
=back
-Final code tarballs for each chapter of the tutorial are available at
+Final code tarballs for each chapter of the tutorial are available at
L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/examples/Tutorial/>.
=head2 L<Chapter 1: Intro|Catalyst::Manual::Tutorial::01_Intro>
-Note: Click on the heading in the previous line to jump to the actual
+Note: Click on the heading in the previous line to jump to the actual
chapter. Below is a "table of contents" for this chapter.
=over 4
=head2 L<Chapter 2: Catalyst Basics|Catalyst::Manual::Tutorial::02_CatalystBasics>
-Note: Click on the heading in the previous line to jump to the actual
+Note: Click on the heading in the previous line to jump to the actual
chapter. Below is a "table of contents" for this chapter.
=over 4
=head2 L<Chapter 3: More Catalyst Basics|Catalyst::Manual::Tutorial::03_MoreCatalystBasics>
-Note: Click on the heading in the previous line to jump to the actual
+Note: Click on the heading in the previous line to jump to the actual
chapter. Below is a "table of contents" for this chapter.
=over 4
=head2 L<Chapter 4: Basic CRUD|Catalyst::Manual::Tutorial::04_BasicCRUD>
-Note: Click on the heading in the previous line to jump to the actual
+Note: Click on the heading in the previous line to jump to the actual
chapter. Below is a "table of contents" for this chapter.
=over 4
=head2 L<Chapter 5: Authentication|Catalyst::Manual::Tutorial::05_Authentication>
-Note: Click on the heading in the previous line to jump to the actual
+Note: Click on the heading in the previous line to jump to the actual
chapter. Below is a "table of contents" for this chapter.
=over 4
=head2 L<Chapter 6: Authorization|Catalyst::Manual::Tutorial::06_Authorization>
-Note: Click on the heading in the previous line to jump to the actual
+Note: Click on the heading in the previous line to jump to the actual
chapter. Below is a "table of contents" for this chapter.
=over 4
=head2 L<Chapter 7: Debugging|Catalyst::Manual::Tutorial::07_Debugging>
-Note: Click on the heading in the previous line to jump to the actual
+Note: Click on the heading in the previous line to jump to the actual
chapter. Below is a "table of contents" for this chapter.
=over 4
=head2 L<Chapter 8: Testing|Catalyst::Manual::Tutorial::08_Testing>
-Note: Click on the heading in the previous line to jump to the actual
+Note: Click on the heading in the previous line to jump to the actual
chapter. Below is a "table of contents" for this chapter.
=over 4
=head2 L<Chapter 9: Advanced CRUD|Catalyst::Manual::Tutorial::09_AdvancedCRUD>
-Note: Click on the heading in the previous line to jump to the actual
+Note: Click on the heading in the previous line to jump to the actual
chapter. Below is a "table of contents" for this chapter.
=over 4
=head2 L<Chapter 10: Appendices|Catalyst::Manual::Tutorial::10_Appendices>
-Note: Click on the heading in the previous line to jump to the actual
+Note: Click on the heading in the previous line to jump to the actual
chapter. Below is a "table of contents" for this chapter.
=over 4
=head1 THANKS
-This tutorial would not have been possible without the input of many
-different people in the Catalyst community. In particular, the
+This tutorial would not have been possible without the input of many
+different people in the Catalyst community. In particular, the
primary author would like to thank:
=over 4
Other Catalyst documentation folks like Kieren Diment, Gavin Henry,
and Jess Robinson (including their work on the original Catalyst
-tutorial).
+tutorial).
=item *
=item *
-People who have emailed me with corrections and suggestions on the
-tutorial. As of the most recent release, this include: Florian Ragwitz,
-Mauro Andreolini, Jim Howard, Giovanni Gigante, William Moreno, Bryan
-Roach, Ashley Berlin, David Kamholz, Kevin Old, Henning Sprang, Jeremy
-Jones, David Kurtz, Ingo Wichmann, Shlomi Fish, Murray Walker, Adam
-Witney and xenoterracide (Caleb Cushing). Thanks to Devin Austin for
-coming up with an initial version of a non-TTSite wrapper page. Also, a
-huge thank you to Kiffin Gish for all the hard work on the "database
-depluralization" effort and Rafael Kitover for the work on updating the
-tutorial to include foreign key support for SQLite. I'm sure I am
-missing some names here... apologies for that (please let me know if you
-name should be here).
+People who have emailed me with corrections and suggestions on the
+tutorial. As of the most recent release, this include: Florian Ragwitz,
+Mauro Andreolini, Jim Howard, Giovanni Gigante, William Moreno, Bryan
+Roach, Ashley Berlin, David Kamholz, Kevin Old, Henning Sprang, Jeremy
+Jones, David Kurtz, Ingo Wichmann, Shlomi Fish, Murray Walker, Adam
+Witney and xenoterracide (Caleb Cushing). Thanks to Devin Austin for
+coming up with an initial version of a non-TTSite wrapper page. Also, a
+huge thank you to Kiffin Gish for all the hard work on the "database
+depluralization" effort and Rafael Kitover for the work on updating the
+tutorial to include foreign key support for SQLite. I'm sure I am
+missing some names here... apologies for that (please let me know if you
+name should be here).
=back
=over 4
-=item *
+=item *
A simple application that lists and adds books.
some of the more advanced techniques you will probably want to use in
your applications).
-=item *
+=item *
How to write CRUD (Create, Read, Update, and Delete) operations in
Catalyst.
Authentication ("auth").
-=item *
+=item *
Role-based authorization ("authz").
-=item *
+=item *
Attempts to provide an example showing current (5.9) Catalyst
practices.
-=item *
+=item *
The use of Template Toolkit (TT).
-=item *
+=item *
Useful techniques for troubleshooting and debugging Catalyst
applications.
-=item *
+=item *
The use of SQLite as a database (with code also provided for MySQL and
PostgreSQL). (Note: Because we make use of the DBIx::Class Object
agnostic and can easily be used by any of the databases supported by
DBIx::Class.)
-=item *
+=item *
The use of L<HTML::FormFu> or L<HTML::FormHandler>
for automated form processing and validation.
=over 4
-=item 1
+=item 1
Download a Tutorial Virtual Machine image from
L<http://cattut.shadowcat.co.uk/>
=over 4
-=item *
+=item *
Debian 6 (Squeeze)
-=item *
+=item *
Catalyst v5.90002
Catalyst::Devel v1.34
-=item *
+=item *
DBIx::Class v0.08195
HTML::FormFu -- v0.09004
-=item *
+=item *
B<NOTE:> You can check the versions you have installed with the
following command (note the slash before the space):
perl -MCatalyst::Devel -e 'print "$Catalyst::Devel::VERSION\n";'
-=item *
+=item *
This tutorial will show URLs in the format of C<http://localhost:3000>,
but if you are running your web browser from outside the Tutorial
Changes # Record of application changes
lib # Lib directory for your app's Perl modules
Hello # Application main code directory
- Controller # Directory for Controller modules
+ Controller # Directory for Controller modules
Model # Directory for Models
View # Directory for Views
Hello.pm # Base application module
hello_server.pl # The normal development server
hello_test.pl # Test your app from the command line
t # Directory for tests
- 01app.t # Test scaffold
- 02pod.t
- 03podcoverage.t
+ 01app.t # Test scaffold
+ 02pod.t
+ 03podcoverage.t
Catalyst will "auto-discover" modules in the Controller, Model, and View
.----------------------------------------------------------------------------.
| Catalyst::Plugin::ConfigLoader 0.30 |
'----------------------------------------------------------------------------'
-
+
[debug] Loaded dispatcher "Catalyst::Dispatcher"
[debug] Loaded engine "Catalyst::Engine"
[debug] Found home "/home/catalyst/Hello"
+-----------------------------------------------------------------+----------+
| Hello::Controller::Root | instance |
'-----------------------------------------------------------------+----------'
-
+
[debug] Loaded Private actions:
.----------------------+--------------------------------------+--------------.
| Private | Class | Method |
| /end | Hello::Controller::Root | end |
| /index | Hello::Controller::Root | index |
'----------------------+--------------------------------------+--------------'
-
+
[debug] Loaded Path actions:
.-------------------------------------+--------------------------------------.
| Path | Private |
| / | /index |
| / | /default |
'-------------------------------------+--------------------------------------'
-
+
[info] Hello powered by Catalyst 5.90002
HTTP::Server::PSGI: Accepting connections at http://0:3000/
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
-
+
# Hello World
$c->response->body( $c->welcome_message );
}
sub hello :Global {
my ( $self, $c ) = @_;
-
+
$c->response->body("Hello, World!");
}
Saw changes to the following files:
- /home/catalyst/Hello/lib/Hello/Controller/Root.pm (modify)
-
+
Attempting to restart the server
...
[debug] Loaded Private actions:
sub hello :Global {
my ( $self, $c ) = @_;
-
+
$c->stash(template => 'hello.tt');
}
used previous is becoming more common because it allows you to
set multiple stash variables in one line. For example:
- $c->stash(template => 'hello.tt', foo => 'bar',
+ $c->stash(template => 'hello.tt', foo => 'bar',
another_thing => 1);
You can also set multiple stash values with a hashref:
- $c->stash({template => 'hello.tt', foo => 'bar',
+ $c->stash({template => 'hello.tt', foo => 'bar',
another_thing => 1});
Any of these formats work, but the C<$c-E<gt>stash(name =E<gt> value);>
sub test :Local {
my ( $self, $c ) = @_;
-
+
$c->stash(username => 'John',
template => 'site/test.tt');
}
-Debug
ConfigLoader
Static::Simple
-
+
StackTrace
/;
and add the following method to the controller:
=head2 list
-
+
Fetch all book objects and pass to books/list.tt2 in stash to be displayed
-
+
=cut
-
+
sub list :Local {
# Retrieve the usual Perl OO '$self' for this object. $c is the Catalyst
# 'Context' that's used to 'glue together' the various components
# that make up the application
my ($self, $c) = @_;
-
+
# Retrieve all of the book records as book model objects and store in the
# stash where they can be accessed by the TT template
# $c->stash(books => [$c->model('DB::Book')->all]);
# But, for now, use this code until we create the model later
$c->stash(books => '');
-
+
# Set the TT template to use. You will almost always want to do this
# in your action methods (action methods respond to user input in
# your controllers).
Then create C<root/src/books/list.tt2> in your editor and enter:
[% # This is a TT comment. -%]
-
+
[%- # Provide a title -%]
[% META title = 'Book List' -%]
-
+
[% # Note That the '-' at the beginning or end of TT code -%]
[% # "chomps" the whitespace/newline at that end of the -%]
[% # output (use View Source in browser to see the effect) -%]
-
+
[% # Some basic HTML with a loop to display books -%]
<table>
<tr><th>Title</th><th>Rating</th><th>Author(s)</th></tr>
and delete the next 2 lines):
=head2 list
-
+
Fetch all book objects and pass to books/list.tt2 in stash to be displayed
-
+
=cut
-
+
sub list :Local {
# Retrieve the usual Perl OO '$self' for this object. $c is the Catalyst
# 'Context' that's used to 'glue together' the various components
# that make up the application
my ($self, $c) = @_;
-
+
# Retrieve all of the book records as book model objects and store
# in the stash where they can be accessed by the TT template
$c->stash(books => [$c->model('DB::Book')->all]);
-
+
# Set the TT template to use. You will almost always want to do this
# in your action methods (action methods respond to user input in
# your controllers).
| Catalyst::Plugin::ConfigLoader 0.30 |
| Catalyst::Plugin::StackTrace 0.11 |
'----------------------------------------------------------------------------'
-
+
[debug] Loaded dispatcher "Catalyst::Dispatcher"
[debug] Loaded engine "Catalyst::Engine"
[debug] Found home "/home/catalyst/MyApp"
| MyApp::Model::DB::BookAuthor | class |
| MyApp::View::HTML | instance |
'-----------------------------------------------------------------+----------'
-
+
[debug] Loaded Private actions:
.----------------------+--------------------------------------+--------------.
| Private | Class | Method |
| /books/index | MyApp::Controller::Books | index |
| /books/list | MyApp::Controller::Books | list |
'----------------------+--------------------------------------+--------------'
-
+
[debug] Loaded Path actions:
.-------------------------------------+--------------------------------------.
| Path | Private |
| /books | /books/index |
| /books/list | /books/list |
'-------------------------------------+--------------------------------------'
-
+
[info] MyApp powered by Catalyst 5.80020
HTTP::Server::PSGI: Accepting connections at http://0:3000
<title>[% template.title or "My Catalyst App!" %]</title>
<link rel="stylesheet" href="[% c.uri_for('/static/css/main.css') %]" />
</head>
-
+
<body>
<div id="outer">
<div id="header">
[%# Insert the page title -%]
<h1>[% template.title or site.title %]</h1>
</div>
-
+
<div id="bodyblock">
<div id="menu">
Navigation:
%]" title="Catalyst Welcome Page">Welcome</a></li>
</ul>
</div><!-- end menu -->
-
+
<div id="content">
[%# Status and error messages %]
<span class="message">[% status_msg %]</span>
[% content %]
</div><!-- end content -->
</div><!-- end bodyblock -->
-
+
<div id="footer">Copyright (c) your name goes here</div>
</div><!-- end outer -->
-
+
</body>
</html>
notice the following code:
=head1 RELATIONS
-
+
=head2 book_authors
-
+
Type: has_many
-
+
Related object: L<MyApp::Schema::Result::BookAuthor>
-
+
=cut
-
+
__PACKAGE__->has_many(
"book_authors",
"MyApp::Schema::Result::BookAuthor",
image" to the C<has_many> relationship we just looked at above:
=head1 RELATIONS
-
+
=head2 book
-
+
Type: belongs_to
-
+
Related object: L<MyApp::Schema::Result::Book>
-
+
=cut
-
+
__PACKAGE__->belongs_to(
"book",
"MyApp::Schema::Result::Book",
DBIx::Class):
SELECT me.id, me.title, me.rating FROM book me:
- SELECT author.id, author.first_name, author.last_name FROM book_author me
+ SELECT author.id, author.first_name, author.last_name FROM book_author me
JOIN author author ON author.id = me.author_id WHERE ( me.book_id = ? ): '1'
- SELECT author.id, author.first_name, author.last_name FROM book_author me
+ SELECT author.id, author.first_name, author.last_name FROM book_author me
JOIN author author ON author.id = me.author_id WHERE ( me.book_id = ? ): '2'
- SELECT author.id, author.first_name, author.last_name FROM book_author me
+ SELECT author.id, author.first_name, author.last_name FROM book_author me
JOIN author author ON author.id = me.author_id WHERE ( me.book_id = ? ): '3'
- SELECT author.id, author.first_name, author.last_name FROM book_author me
+ SELECT author.id, author.first_name, author.last_name FROM book_author me
JOIN author author ON author.id = me.author_id WHERE ( me.book_id = ? ): '4'
- SELECT author.id, author.first_name, author.last_name FROM book_author me
+ SELECT author.id, author.first_name, author.last_name FROM book_author me
JOIN author author ON author.id = me.author_id WHERE ( me.book_id = ? ): '5'
Also note in C<root/src/books/list.tt2> that we are using "| html", a
You should get a page with the following message at the top:
- Caught exception in MyApp::Controller::Root->end "Forced debug -
+ Caught exception in MyApp::Controller::Root->end "Forced debug -
Scrubbed output at /usr/share/perl5/Catalyst/Action/RenderView.pm line 46."
Along with a summary of your application's state at the end of the
C<$c-E<gt>stash-E<gt>{template}> line has changed):
=head2 list
-
+
Fetch all book objects and pass to books/list.tt2 in stash to be displayed
-
+
=cut
-
+
sub list :Local {
# Retrieve the usual Perl OO '$self' for this object. $c is the Catalyst
# 'Context' that's used to 'glue together' the various components
# that make up the application
my ($self, $c) = @_;
-
+
# Retrieve all of the book records as book model objects and store in the
# stash where they can be accessed by the TT template
$c->stash(books => [$c->model('DB::Book')->all]);
-
+
# Set the TT template to use. You will almost always want to do this
# in your action methods (actions methods respond to user input in
# your controllers).
Edit C<lib/MyApp/Controller/Books.pm> and enter the following method:
=head2 url_create
-
+
Create a book with the supplied title, rating, and author
-
+
=cut
-
+
sub url_create :Local {
# In addition to self & context, get the title, rating, &
# author_id args from the URL. Note that Catalyst automatically
# puts extra information after the "/<controller_name>/<action_name/"
# into @_. The args are separated by the '/' char on the URL.
my ($self, $c, $title, $rating, $author_id) = @_;
-
+
# Call create() on the book model object. Pass the table
# columns/field values we want to set as hash values
my $book = $c->model('DB::Book')->create({
title => $title,
rating => $rating
});
-
+
# Add a record to the join table for this book, mapping to
# appropriate author
$book->add_to_book_authors({author_id => $author_id});
# Note: Above is a shortcut for this:
# $book->create_related('book_authors', {author_id => $author_id});
-
+
# Assign the Book object to the stash for display and set template
$c->stash(book => $book,
template => 'books/create_done.tt2');
-
+
# Disable caching for this page
$c->response->header('Cache-Control' => 'no-cache');
}
[% # Not a good idea for production use, though. :-) 'Indent=1' is -%]
[% # optional, but prevents "massive indenting" of deeply nested objects -%]
[% USE Dumper(Indent=1) -%]
-
+
[% # Set the page title. META can 'go back' and set values in templates -%]
[% # that have been processed 'before' this template (here it's updating -%]
[% # the title in the root/src/wrapper.tt2 wrapper template). Note that -%]
[% # interpolation -- if you need dynamic/interpolated content in your -%]
[% # title, set "$c->stash(title => $something)" in the controller). -%]
[% META title = 'Book Created' %]
-
+
[% # Output information about the record that was added. First title. -%]
<p>Added book '[% book.title %]'
-
+
[% # Then, output the last name of the first author -%]
by '[% book.authors.first.last_name %]'
-
+
[% # Then, output the rating for the book that was added -%]
with a rating of [% book.rating %].</p>
-
+
[% # Provide a link back to the list page. 'c.uri_for' builds -%]
[% # a full URI; e.g., 'http://localhost:3000/books/list' -%]
<p><a href="[% c.uri_for('/books/list') %]">Return to list</a></p>
-
+
[% # Try out the TT Dumper (for development only!) -%]
<pre>
Dump of the 'book' variable:
browser at the C</books/list> page). You should now see the six DBIC
debug messages similar to the following (where N=1-6):
- SELECT author.id, author.first_name, author.last_name
- FROM book_author me JOIN author author
+ SELECT author.id, author.first_name, author.last_name
+ FROM book_author me JOIN author author
ON author.id = me.author_id WHERE ( me.book_id = ? ): 'N'
sub url_create :Chained('/') :PathPart('books/url_create') :Args(3) {
# In addition to self & context, get the title, rating, &
# author_id args from the URL. Note that Catalyst automatically
- # puts the first 3 arguments worth of extra information after the
+ # puts the first 3 arguments worth of extra information after the
# "/<controller_name>/<action_name/" into @_ because we specified
# "Args(3)". The args are separated by the '/' char on the URL.
my ($self, $c, $title, $rating, $author_id) = @_;
-
+
...
This converts the method to take advantage of the Chained
| /books | /books/index |
| /books/list | /books/list |
'-------------------------------------+--------------------------------------'
-
+
[debug] Loaded Chained actions:
.-------------------------------------+--------------------------------------.
| Path Spec | Private |
method:
=head2 base
-
+
Can place common logic to start chained dispatch here
-
+
=cut
-
+
sub base :Chained('/') :PathPart('books') :CaptureArgs(0) {
my ($self, $c) = @_;
-
+
# Store the ResultSet in stash so it's available for other methods
$c->stash(resultset => $c->model('DB::Book'));
-
+
# Print a message to the debug log
$c->log->debug('*** INSIDE BASE METHOD ***');
}
Edit C<lib/MyApp/Controller/Books.pm> and add the following method:
=head2 form_create
-
+
Display form to collect information for book to create
-
+
=cut
-
+
sub form_create :Chained('base') :PathPart('form_create') :Args(0) {
my ($self, $c) = @_;
-
+
# Set the TT template to use
$c->stash(template => 'books/form_create.tt2');
}
Open C<root/src/books/form_create.tt2> in your editor and enter:
[% META title = 'Manual Form Book Create' -%]
-
+
<form method="post" action="[% c.uri_for('form_create_do') %]">
<table>
<tr><td>Title:</td><td><input type="text" name="title"></td></tr>
save the form information to the database:
=head2 form_create_do
-
+
Take information from form and add to database
-
+
=cut
-
+
sub form_create_do :Chained('base') :PathPart('form_create_do') :Args(0) {
my ($self, $c) = @_;
-
+
# Retrieve the values from the form
my $title = $c->request->params->{title} || 'N/A';
my $rating = $c->request->params->{rating} || 'N/A';
my $author_id = $c->request->params->{author_id} || '1';
-
+
# Create the book
my $book = $c->model('DB::Book')->create({
title => $title,
$book->add_to_book_authors({author_id => $author_id});
# Note: Above is a shortcut for this:
# $book->create_related('book_authors', {author_id => $author_id});
-
+
# Store new model object in stash and set template
$c->stash(book => $book,
template => 'books/create_done.tt2');
header, and 2) the five lines for the Delete link near the bottom):
[% # This is a TT comment. -%]
-
+
[%- # Provide a title -%]
[% META title = 'Book List' -%]
-
+
[% # Note That the '-' at the beginning or end of TT code -%]
[% # "chomps" the whitespace/newline at that end of the -%]
[% # output (use View Source in browser to see the effect) -%]
-
+
[% # Some basic HTML with a loop to display books -%]
<table>
<tr><th>Title</th><th>Rating</th><th>Author(s)</th><th>Links</th></tr>
add the following code:
=head2 object
-
+
Fetch the specified book object based on the book ID and store
it in the stash
-
+
=cut
-
+
sub object :Chained('base') :PathPart('id') :CaptureArgs(1) {
# $id = primary key of book to delete
my ($self, $c, $id) = @_;
-
+
# Find the book object and store it in the stash
$c->stash(object => $c->stash->{resultset}->find($id));
-
+
# Make sure the lookup was successful. You would probably
# want to do something like this in a real app:
# $c->detach('/error_404') if !$c->stash->{object};
die "Book $id not found!" if !$c->stash->{object};
-
+
# Print a message to the debug log
$c->log->debug("*** INSIDE OBJECT METHOD for obj id=$id ***");
}
following method:
=head2 delete
-
+
Delete a book
-
+
=cut
-
+
sub delete :Chained('object') :PathPart('delete') :Args(0) {
my ($self, $c) = @_;
-
+
# Use the book object saved by 'object' and delete it along
# with related 'book_author' entries
$c->stash->{object}->delete;
-
+
# Set a status message to be displayed at the top of the view
$c->stash->{status_msg} = "Book deleted.";
-
+
# Forward to the list action/method in this controller
$c->forward('list');
}
method to match:
=head2 delete
-
+
Delete a book
-
+
=cut
-
+
sub delete :Chained('object') :PathPart('delete') :Args(0) {
my ($self, $c) = @_;
-
+
# Use the book object saved by 'object' and delete it along
# with related 'book_author' entries
$c->stash->{object}->delete;
-
+
# Set a status message to be displayed at the top of the view
$c->stash->{status_msg} = "Book deleted.";
-
+
# Redirect the user back to the list page. Note the use
# of $self->action_for as earlier in this section (BasicCRUD)
$c->response->redirect($c->uri_for($self->action_for('list')));
method to match the following:
=head2 delete
-
+
Delete a book
-
+
=cut
-
+
sub delete :Chained('object') :PathPart('delete') :Args(0) {
my ($self, $c) = @_;
-
+
# Use the book object saved by 'object' and delete it along
# with related 'book_author' entries
$c->stash->{object}->delete;
-
+
# Redirect the user back to the list page with status msg as an arg
$c->response->redirect($c->uri_for($self->action_for('list'),
{status_msg => "Book deleted."}));
Notice in the debug log that the SQL DBIC generated has changed to
incorporate the datetime logic:
- INSERT INTO book ( created, rating, title, updated ) VALUES ( ?, ?, ?, ? ):
+ INSERT INTO book ( created, rating, title, updated ) VALUES ( ?, ?, ?, ? ):
'2010-02-16 04:18:42', '5', 'TCPIP_Illustrated_Vol-2', '2010-02-16 04:18:42'
INSERT INTO book_author ( author_id, book_id ) VALUES ( ?, ? ): '4', '10'
Then open C<lib/MyApp/Schema/ResultSet/Book.pm> and enter the following:
package MyApp::Schema::ResultSet::Book;
-
+
use strict;
use warnings;
use base 'DBIx::Class::ResultSet';
-
+
=head2 created_after
-
+
A predefined search for recently added books
-
+
=cut
-
+
sub created_after {
my ($self, $datetime) = @_;
-
+
my $date_str = $self->result_source->schema->storage
->datetime_parser->format_datetime($datetime);
-
+
return $self->search({
created => { '>' => $date_str }
});
}
-
+
1;
Then add the following method to the C<lib/MyApp/Controller/Books.pm>:
=head2 list_recent
-
+
List recently created books
-
+
=cut
-
+
sub list_recent :Chained('base') :PathPart('list_recent') :Args(1) {
my ($self, $c, $mins) = @_;
-
+
# Retrieve all of the book records as book model objects and store in the
# stash where they can be accessed by the TT template, but only
# retrieve books created within the last $min number of minutes
$c->stash(books => [$c->model('DB::Book')
->created_after(DateTime->now->subtract(minutes => $mins))]);
-
+
# Set the TT template to use. You will almost always want to do this
# in your action methods (action methods respond to user input in
# your controllers).
the following method:
=head2 list_recent_tcp
-
+
List recently created books
-
+
=cut
-
+
sub list_recent_tcp :Chained('base') :PathPart('list_recent_tcp') :Args(1) {
my ($self, $c, $mins) = @_;
-
+
# Retrieve all of the book records as book model objects and store in the
# stash where they can be accessed by the TT template, but only
# retrieve books created within the last $min number of minutes
->created_after(DateTime->now->subtract(minutes => $mins))
->search({title => {'like', '%TCP%'}})
]);
-
+
# Set the TT template to use. You will almost always want to do this
# in your action methods (action methods respond to user input in
# your controllers).
Take a look at the DBIC_TRACE output in the development server log for
the first URL and you should see something similar to the following:
- SELECT me.id, me.title, me.rating, me.created, me.updated FROM book me
+ SELECT me.id, me.title, me.rating, me.created, me.updated FROM book me
WHERE ( ( title LIKE ? AND created > ? ) ): '%TCP%', '2010-02-16 02:49:32'
However, let's not pollute our controller code with this raw "TCP" query
and add the following method:
=head2 title_like
-
+
A predefined search for books with a 'LIKE' search in the string
-
+
=cut
-
+
sub title_like {
my ($self, $title_str) = @_;
-
+
return $self->search({
title => { 'like' => "%$title_str%" }
});
shown here -- the rest of the method should be the same):
=head2 list_recent_tcp
-
+
List recently created books
-
+
=cut
-
+
sub list_recent_tcp :Chained('base') :PathPart('list_recent_tcp') :Args(1) {
my ($self, $c, $mins) = @_;
-
+
# Retrieve all of the book records as book model objects and store in the
# stash where they can be accessed by the TT template, but only
# retrieve books created within the last $min number of minutes
->created_after(DateTime->now->subtract(minutes => $mins))
->title_like('TCP')
]);
-
+
# Set the TT template to use. You will almost always want to do this
# in your action methods (action methods respond to user input in
# your controllers).
#
sub full_name {
my ($self) = @_;
-
+
return $self->first_name . ' ' . $self->last_name;
}
C<lib/MyApp/Schema/Result/Book.pm> and add the following method:
=head2 author_count
-
+
Return the number of authors for the current book
-
+
=cut
-
+
sub author_count {
my ($self) = @_;
-
+
# Use the 'many_to_many' relationship to fetch all of the authors for the current
# and the 'count' method in DBIx::Class::ResultSet to get a SQL COUNT
return $self->authors->count;
same C<lib/MyApp/Schema/Result/Book.pm> file:
=head2 author_list
-
+
Return a comma-separated list of authors for the current book
-
+
=cut
-
+
sub author_list {
my ($self) = @_;
-
- # Loop through all authors for the current book, calling all the 'full_name'
+
+ # Loop through all authors for the current book, calling all the 'full_name'
# Result Class method for each
my @names;
foreach my $author ($self->authors) {
push(@names, $author->full_name);
}
-
+
return join(', ', @names);
}
-Debug
ConfigLoader
Static::Simple
-
+
StackTrace
-
+
Authentication
-
+
Session
Session::Store::File
Session::State::Cookie
C<sub index> to match:
=head2 index
-
+
Login logic
-
+
=cut
-
+
sub index :Path :Args(0) {
my ($self, $c) = @_;
-
+
# Get the username and password from form
my $username = $c->request->params->{username};
my $password = $c->request->params->{password};
-
+
# If the username and password values were found in form
if ($username && $password) {
# Attempt to log the user in
$c->stash(error_msg => "Empty username or password.")
unless ($c->user_exists);
}
-
+
# If either of above don't work out, send to the login page
$c->stash(template => 'login.tt2');
}
C<lib/MyApp/Controller/Logout.pm> to match:
=head2 index
-
+
Logout logic
-
+
=cut
-
+
sub index :Path :Args(0) {
my ($self, $c) = @_;
-
+
# Clear the user's state
$c->logout;
-
+
# Send the user to the starting point
$c->response->redirect($c->uri_for('/'));
}
Create a login form by opening C<root/src/login.tt2> and inserting:
[% META title = 'Login' %]
-
+
<!-- Login form -->
<form method="post" action="[% c.uri_for('/login') %]">
<table>
the following method:
=head2 auto
-
+
Check if there is a user and, if not, forward to login page
-
+
=cut
-
+
# Note that 'auto' runs after 'begin' but before your actions and that
# 'auto's "chain" (all from application path to most specific class are run)
# See the 'Actions' section of 'Catalyst::Manual::Intro' for more info.
sub auto :Private {
my ($self, $c) = @_;
-
+
# Allow unauthenticated users to reach the login page. This
# allows unauthenticated users to reach any action in the Login
# controller. To lock it down to a single action, we could use:
if ($c->controller eq $c->controller('Login')) {
return 1;
}
-
+
# If a user doesn't exist, force login
if (!$c->user_exists) {
# Dump a log message to the development server debug output
# Return 0 to cancel 'post-auto' processing and prevent use of application
return 0;
}
-
+
# User found, so return 1 to continue with processing after this 'auto'
return 1;
}
text:
#!/usr/bin/perl
-
+
use strict;
use warnings;
-
+
use MyApp::Schema;
-
+
my $schema = MyApp::Schema->connect('dbi:SQLite:myapp.db');
-
+
my @users = $schema->resultset('User')->all;
-
+
foreach my $user (@users) {
$user->password('mypass');
$user->update;
changed):
=head2 delete
-
+
Delete a book
-
+
=cut
-
+
sub delete :Chained('object') :PathPart('delete') :Args(0) {
my ($self, $c) = @_;
-
+
# Use the book object saved by 'object' and delete it along
# with related 'book_authors' entries
$c->stash->{object}->delete;
-
+
# Use 'flash' to save information across requests until it's read
$c->flash->{status_msg} = "Book deleted";
-
+
# Redirect the user back to the list page
$c->response->redirect($c->uri_for($self->action_for('list')));
}
information.
-=head2 Switch To Catalyst::Plugin::StatusMessages
+=head2 Switch To Catalyst::Plugin::StatusMessages
Although the query parameter technique we used in
L<Chapter 4|Catalyst::Manual::Tutorial::04_BasicCRUD> and the C<flash>
-Debug
ConfigLoader
Static::Simple
-
+
StackTrace
-
+
Authentication
-
+
Session
Session::Store::File
Session::State::Cookie
-
+
StatusMessage
/;
sub delete :Chained('object') :PathPart('delete') :Args(0) {
my ($self, $c) = @_;
-
+
# Saved the PK id for status_msg below
my $id = $c->stash->{object}->id;
-
+
# Use the book object saved by 'object' and delete it along
# with related 'book_authors' entries
$c->stash->{object}->delete;
-
+
# Redirect the user back to the list page
$c->response->redirect($c->uri_for($self->action_for('list'),
{mid => $c->set_status_msg("Deleted book $id")}));
sub base :Chained('/') :PathPart('books') :CaptureArgs(0) {
my ($self, $c) = @_;
-
+
# Store the ResultSet in stash so it's available for other methods
$c->stash(resultset => $c->model('DB::Book'));
-
+
# Print a message to the debug log
$c->log->debug('*** INSIDE BASE METHOD ***');
-
+
# Load status messages
$c->load_status_msgs;
}
-Debug
ConfigLoader
Static::Simple
-
+
StackTrace
-
+
Authentication
Authorization::Roles
-
+
Session
Session::Store::File
Session::State::Cookie
...
<p>Hello [% c.user.username %], you have the following roles:</p>
-
+
<ul>
[% # Dump list of roles -%]
[% FOR role = c.user.roles %]<li>[% role %]</li>[% END %]
</ul>
-
+
<p>
[% # Add some simple role-specific logic to template %]
[% # Use $c->check_user_roles() to check authz -%]
[% # Give normal users a link for 'logout' %]
<a href="[% c.uri_for('/logout') %]">User Logout</a>
[% END %]
-
+
[% # Can also use $c->user->check_roles() to check authz -%]
[% IF c.check_user_roles('admin') %]
[% # Give admin users a link for 'create' %]
updating C<url_create> to match the following code:
=head2 url_create
-
+
Create a book with the supplied title and rating,
with manual authorization
-
+
=cut
-
+
sub url_create :Chained('base') :PathPart('url_create') :Args(3) {
# In addition to self & context, get the title, rating & author_id args
# from the URL. Note that Catalyst automatically puts extra information
# after the "/<controller_name>/<action_name/" into @_
my ($self, $c, $title, $rating, $author_id) = @_;
-
+
# Check the user's roles
if ($c->check_user_roles('admin')) {
# Call create() on the book model object. Pass the table
title => $title,
rating => $rating
});
-
+
# Add a record to the join table for this book, mapping to
# appropriate author
$book->add_to_book_authors({author_id => $author_id});
# Note: Above is a shortcut for this:
# $book->create_related('book_authors', {author_id => $author_id});
-
+
# Assign the Book object to the stash and set template
$c->stash(book => $book,
template => 'books/create_done.tt2');
to add it below the "C<DO NOT MODIFY ...>" line):
=head2 delete_allowed_by
-
+
Can the specified user delete the current book?
-
+
=cut
-
+
sub delete_allowed_by {
my ($self, $user) = @_;
-
+
# Only allow delete if user has 'admin' role
return $user->has_role('admin');
}
the "C<DO NOT MODIFY ...>" line:
=head2 has_role
-
+
Check if a user has the specified role
-
+
=cut
-
+
use Perl6::Junction qw/any/;
sub has_role {
my ($self, $role) = @_;
-
+
# Does this user posses the required role?
return any(map { $_->role } $self->roles) eq $role;
}
match the following code:
=head2 delete
-
+
Delete a book
-
+
=cut
-
+
sub delete :Chained('object') :PathPart('delete') :Args(0) {
my ($self, $c) = @_;
-
+
# Check permissions
$c->detach('/error_noperms')
unless $c->stash->{object}->delete_allowed_by($c->user->get_object);
-
+
# Saved the PK id for status_msg below
my $id = $c->stash->{object}->id;
-
+
# Use the book object saved by 'object' and delete it along
# with related 'book_authors' entries
$c->stash->{object}->delete;
-
+
# Redirect the user back to the list page
$c->response->redirect($c->uri_for($self->action_for('list'),
{mid => $c->set_status_msg("Deleted book $id")}));
C<lib/MyApp/Controller/Root.pm> and add this method:
=head2 error_noperms
-
+
Permissions error screen
-
+
=cut
-
+
sub error_noperms :Chained('/') :PathPart('error_noperms') :Args(0) {
my ($self, $c) = @_;
-
+
$c->stash(template => 'error_noperms.tt2');
}
=over 4
-=item *
+=item *
Fans of C<log> and C<print> statements embedded in the code.
-=item *
+=item *
Fans of interactive debuggers.
following code to a controller action method:
$c->log->info("Starting the foreach loop here");
-
+
$c->log->debug("Value of \$id is: ".$id);
Then the Catalyst development server will display your message along
# 'Context' that's used to 'glue together' the various components
# that make up the application
my ($self, $c) = @_;
-
+
$DB::single=1;
-
+
# Retrieve all of the book records as book model objects and store in the
# stash where they can be accessed by the TT template
$c->stash->{books} = [$c->model('DB::Book')->all];
-
+
# Set the TT template to use. You will almost always want to do this
# in your action methods.
$c->stash->{template} = 'books/list.tt2';
This will start the interactive debugger and produce output similar to:
- $ perl -d script/myapp_server.pl
-
+ $ perl -d script/myapp_server.pl
+
Loading DB routines from perl5db.pl version 1.3
Editor support available.
-
+
Enter h or `h h' for help, or `man perldebug' for more help.
-
+
main::(script/myapp_server.pl:16): my $debug = 0;
-
- DB<1>
+
+ DB<1>
Press the C<c> key and hit C<Enter> to continue executing the Catalyst
development server under the debugger. Although execution speed will be
MyApp::Controller::Books::list(/home/catalyst/MyApp/script/../lib/MyApp/Controller/Books.pm:48):
48: $c->stash->{books} = [$c->model('DB::Book')->all];
-
+
DB<1>
You now have the full Perl debugger at your disposal. First use the
SELECT me.id, me.title, me.rating, me.created, me.updated FROM book me:
MyApp::Controller::Books::list(/home/catalyst/MyApp/script/../lib/MyApp/Controller/Books.pm:53):
53: $c->stash->{template} = 'books/list.tt2';
-
+
DB<1>
This takes you to the next line of code where the template name is set.
_calculate_score
_collapse_cond
<lines removed for brevity>
-
+
DB<2>
We can also play with the model directly:
=over 4
-=item *
+=item *
Check the version of an installed module:
or die qq(module \"\$m\" is not installed\\n); \
print \$m->VERSION'"
-=item *
+=item *
Check if a modules contains a given method:
editor and enter the following:
#!/usr/bin/env perl
-
+
use strict;
use warnings;
use Test::More;
-
+
# Need to specify the name of your app as arg on next line
# Can also do:
# use Test::WWW::Mechanize::Catalyst "MyApp";
-
+
BEGIN { use_ok("Test::WWW::Mechanize::Catalyst" => "MyApp") }
-
+
# Create two 'user agents' to simulate two different users ('test01' & 'test02')
my $ua1 = Test::WWW::Mechanize::Catalyst->new;
my $ua2 = Test::WWW::Mechanize::Catalyst->new;
-
+
# Use a simplified for loop to do tests that are common to both users
# Use get_ok() to make sure we can hit the base URL
# Second arg = optional description of test (will be displayed for failed tests)
# Use content_contains() to match on text in the html body
$_->content_contains("You need to log in to use this application",
"Check we are NOT logged in") for $ua1, $ua2;
-
+
# Log in as each user
# Specify username and password on the URL
$ua1->get_ok("http://localhost/login?username=test01&password=mypass", "Login 'test01'");
username => 'test02',
password => 'mypass',
});
-
+
# Go back to the login page and it should show that we are already logged in
$_->get_ok("http://localhost/login", "Return to '/login'") for $ua1, $ua2;
$_->title_is("Login", "Check for login page") for $ua1, $ua2;
$_->content_contains("Please Note: You are already logged in as ",
"Check we ARE logged in" ) for $ua1, $ua2;
-
+
# 'Click' the 'Logout' link (see also 'text_regex' and 'url_regex' options)
$_->follow_link_ok({n => 4}, "Logout via first link on page") for $ua1, $ua2;
$_->title_is("Login", "Check for login title") for $ua1, $ua2;
$_->content_contains("You need to log in to use this application",
"Check we are NOT logged in") for $ua1, $ua2;
-
+
# Log back in
$ua1->get_ok("http://localhost/login?username=test01&password=mypass",
"Login 'test01'");
"Login 'test02'");
# Should be at the Book List page... do some checks to confirm
$_->title_is("Book List", "Check for book list title") for $ua1, $ua2;
-
+
$ua1->get_ok("http://localhost/books/list", "'test01' book list");
$ua1->get_ok("http://localhost/login", "Login Page");
$ua1->get_ok("http://localhost/books/list", "'test01' book list");
-
+
$_->content_contains("Book List", "Check for book list title") for $ua1, $ua2;
# Make sure the appropriate logout buttons are displayed
$_->content_contains("/logout\">User Logout</a>",
"'test01' should have a create link");
$ua2->content_lacks("/books/form_create\">Admin Create</a>",
"'test02' should NOT have a create link");
-
+
$ua1->get_ok("http://localhost/books/list", "View book list as 'test01'");
-
+
# User 'test01' should be able to create a book with the "formless create" URL
$ua1->get_ok("http://localhost/books/url_create/TestTitle/2/4",
"'test01' formless create");
# Try a regular expression to combine the previous 3 checks & account for whitespace
$ua1->content_like(qr/Added book 'TestTitle'\s+by 'Stevens'\s+with a rating of 2./,
"Regex check");
-
+
# Make sure the new book shows in the list
$ua1->get_ok("http://localhost/books/list", "'test01' book list");
$ua1->title_is("Book List", "Check logged in and at book list");
$ua1->content_contains("Book List", "Book List page test");
$ua1->content_contains("TestTitle", "Look for 'TestTitle'");
-
+
# Make sure the new book can be deleted
# Get all the Delete links on the list page
my @delLinks = $ua1->find_all_links(text => 'Delete');
# Check that delete worked
$ua1->content_contains("Book List", "Book List page test");
$ua1->content_like(qr/Deleted book \d+/, "Deleted book #");
-
+
# User 'test02' should not be able to add a book
$ua2->get_ok("http://localhost/books/url_create/TestTitle2/2/5", "'test02' add");
$ua2->content_contains("Unauthorized!", "Check 'test02' cannot add");
-
+
done_testing;
The C<live_app.t> test cases uses copious comments to explain each step
my $dsn = $ENV{MYAPP_DSN} ||= 'dbi:SQLite:myapp.db';
__PACKAGE__->config(
schema_class => 'MyApp::Schema',
-
+
connect_info => {
dsn => $dsn,
user => '',
use strict;
use warnings;
use Test::More;
-
+
BEGIN {
$ENV{ MYAPP_CONFIG_LOCAL_SUFFIX } = 'testing';
}
-
+
eval "use Test::WWW::Mechanize::Catalyst 'MyApp'";
plan $@
? ( skip_all => 'Test::WWW::Mechanize::Catalyst required' )
: ( tests => 2 );
-
+
ok( my $mech = Test::WWW::Mechanize::Catalyst->new, 'Created mech object' );
-
+
$mech->get_ok( 'http://localhost/foo' );
Catalyst::Manual::Tutorial::09_AdvancedCRUD::09_FormBuilder - Catalyst Tutorial - Chapter 9: Advanced CRUD - FormBuilder
-NOTE: This chapter of the tutorial is in progress. Feel free to
+NOTE: This chapter of the tutorial is in progress. Feel free to
volunteer to help out. :-)
=head1 OVERVIEW
following method:
=head2 formfu_create
-
+
Use HTML::FormFu to create a new book
-
+
=cut
-
+
sub formfu_create :Chained('base') :PathPart('formfu_create') :Args(0) :FormConfig {
my ($self, $c) = @_;
-
+
# Get the form that the :FormConfig attribute saved in the stash
my $form = $c->stash->{form};
-
+
# Check if the form has been submitted (vs. displaying the initial
# form) and if the data passed validation. "submitted_and_valid"
# is shorthand for "$form->submitted && !$form->has_errors"
# Add the authors to it
$select->options(\@authors);
}
-
+
# Set the template
$c->stash(template => 'books/formfu_create.tt2');
}
# This is an optional 'mouse over' title pop-up
attributes:
title: Enter a book title here
-
+
# Another text field for the numeric rating
- type: Text
name: rating
label: Rating
attributes:
title: Enter a rating between 1 and 5 here
-
+
# Add a drop-down list for the author selection. Note that we will
# dynamically fill in all the authors from the controller but we
# could manually set items in the drop-list by adding this YAML code:
- type: Select
name: authors
label: Author
-
+
# The submit button
- type: Submit
name: submit
following:
[% META title = 'Create/Update Book' %]
-
+
[%# Render the HTML::FormFu Form %]
[% form %]
-
- <p><a href="[% c.uri_for(c.controller.action_for('list'))
+
+ <p><a href="[% c.uri_for(c.controller.action_for('list'))
%]">Return to book list</a></p>
max: 40
# Override the default of 'Invalid input'
message: Length must be between 5 and 40 characters
-
+
# Another text field for the numeric rating
- type: Text
name: rating
min: 1
max: 5
message: "Must be between 1 and 5."
-
+
# Add a select list for the author selection. Note that we will
# dynamically fill in all the authors from the controller but we
# could manually set items in the select by adding this YAML code:
constraints:
# Make sure it's a number
- Integer
-
+
# The submit button
- type: Submit
name: submit
value: Submit
-
+
# Global filters and constraints.
constraints:
# The user cannot leave any fields blank
bottom:
=head2 formfu_edit
-
+
Use HTML::FormFu to update an existing book
-
+
=cut
-
- sub formfu_edit :Chained('object') :PathPart('formfu_edit') :Args(0)
+
+ sub formfu_edit :Chained('object') :PathPart('formfu_edit') :Args(0)
:FormConfig('books/formfu_create.yml') {
my ($self, $c) = @_;
-
+
# Get the specified book already saved by the 'object' method
my $book = $c->stash->{object};
-
+
# Make sure we were able to get a book
unless ($book) {
# Set an error message for the user & return to books list
{mid => $c->set_error_msg("Invalid book -- Cannot edit")}));
$c->detach;
}
-
+
# Get the form that the :FormConfig attribute saved in the stash
my $form = $c->stash->{form};
-
+
# Check if the form has been submitted (vs. displaying the initial
# form) and if the data passed validation. "submitted_and_valid"
# is shorthand for "$form->submitted && !$form->has_errors"
# Populate the form with existing values from DB
$form->model->default_values($book);
}
-
+
# Set the template
$c->stash(template => 'books/formfu_create.tt2');
}
to or from the database. This was written using HTML::FormHandler version
0.28001.
-See
+See
L<Catalyst::Manual::Tutorial::09_AdvancedCRUD>
-for additional form management options other than
+for additional form management options other than
L<HTML::FormHandler>.
Use the following command to install L<HTML::FormHandler::Model::DBIC> directly
from CPAN:
- sudo cpan HTML::FormHandler::Model::DBIC
+ sudo cpan HTML::FormHandler::Model::DBIC
-It will install L<HTML::FormHandler> as a prerequisite.
+It will install L<HTML::FormHandler> as a prerequisite.
Also, add:
=head1 HTML::FormHandler FORM CREATION
-This section looks at how L<HTML::FormHandler> can be used to
+This section looks at how L<HTML::FormHandler> can be used to
add additional functionality to the manually created form from Chapter 4.
-=head2 Using FormHandler in your controllers
+=head2 Using FormHandler in your controllers
FormHandler doesn't have a Catalyst base controller, because interfacing
to a form is only a couple of lines of code.
Open C<root/src/books/form.tt2> in your editor and enter the following:
[% META title = 'Create/Update Book' %]
-
+
[%# Render the HTML::FormHandler Form %]
[% form.render %]
-
+
<p><a href="[% c.uri_for(c.controller.action_for('list')) %]">Return to book list</a></p>
Title = "Internetworking with TCP/IP Vol. II"
Rating = "4"
Author = "Comer"
-
+
Click the Submit button, and you will be returned to the Book List page
with a "Book created" status message displayed.
Note that because the 'Author' column is a Select list, only the authors
in the database can be entered. The 'ratings' field will only accept
-integers.
+integers.
=head2 Add Constraints
-Open C<lib/MyApp/Form/Book.pm> in your editor.
+Open C<lib/MyApp/Form/Book.pm> in your editor.
Restrict the title size and make it required:
=head2 Try Out the Updated Form
-Press C<Ctrl-C> to kill the previous server instance (if it's still
+Press C<Ctrl-C> to kill the previous server instance (if it's still
running) and restart it:
$ script/myapp_server.pl
-Make sure you are still logged in as C<test01> and try adding a book
-with various errors: title less than 5 characters, non-numeric rating, a
-rating of 0 or 6, etc. Also try selecting one, two, and zero authors.
+Make sure you are still logged in as C<test01> and try adding a book
+with various errors: title less than 5 characters, non-numeric rating, a
+rating of 0 or 6, etc. Also try selecting one, two, and zero authors.
=head2 Create the 'edit' method
Edit C<lib/MyApp/Controller/Books.pm> and add the following method:
-
+
=head2 edit
Edit an existing book with FormHandler
=head2 Try Out the Edit/Update Feature
-Press C<Ctrl-C> to kill the previous server instance (if it's still
+Press C<Ctrl-C> to kill the previous server instance (if it's still
running) and restart it:
$ script/myapp_server.pl
-Make sure you are still logged in as C<test01> and go to the
-L<http://localhost:3000/books/list> URL in your browser. Click the
-"Edit" link next to "Internetworking with TCP/IP Vol. II", change the
-rating to a 3, the "II" at end of the title to the number "2", add
-Stevens as a co-author (control-click), and click Submit. You will then
-be returned to the book list with a "Book edited" message at the top in
+Make sure you are still logged in as C<test01> and go to the
+L<http://localhost:3000/books/list> URL in your browser. Click the
+"Edit" link next to "Internetworking with TCP/IP Vol. II", change the
+rating to a 3, the "II" at end of the title to the number "2", add
+Stevens as a co-author (control-click), and click Submit. You will then
+be returned to the book list with a "Book edited" message at the top in
green. Experiment with other edits to various books.
=head2 See additional documentation on FormHandler
mailing list: http://groups.google.com/group/formhandler
- code: http://github.com/gshank/html-formhandler/tree/master
+ code: http://github.com/gshank/html-formhandler/tree/master
=head1 AUTHOR
C<C-E<lt>> and C<C-E<gt>> to set the mark at the beginning and end of the
file respectively.
-Also, Stefan Kangas sent in the following tip about an alternate
-approach using the command C<indent-region> to redo the indentation
-for the currently selected region (adhering to indent rules in the
-current major mode). You can run the command by typing M-x
-indent-region or pressing the default keybinding C-M-\ in cperl-mode.
+Also, Stefan Kangas sent in the following tip about an alternate
+approach using the command C<indent-region> to redo the indentation
+for the currently selected region (adhering to indent rules in the
+current major mode). You can run the command by typing M-x
+indent-region or pressing the default keybinding C-M-\ in cperl-mode.
Additional details can be found here:
L<http://www.gnu.org/software/emacs/manual/html_node/emacs/Indentation-Commands.html>
=head2 PostgreSQL
-Use the following steps to adapt the tutorial to PostgreSQL. Thanks
-to Caelum (Rafael Kitover) for assistance with the most recent
-updates, and Louis Moore, Marcello Romani and Tom Lanyon for help with
+Use the following steps to adapt the tutorial to PostgreSQL. Thanks
+to Caelum (Rafael Kitover) for assistance with the most recent
+updates, and Louis Moore, Marcello Romani and Tom Lanyon for help with
earlier versions.
=over 4
sudo aptitude install postgresql libdbd-pg-perl libdatetime-format-pg-perl
-To configure the permissions, you can open
-C</etc/postgresql/8.3/main/pg_hba.conf> and change this line (near the
+To configure the permissions, you can open
+C</etc/postgresql/8.3/main/pg_hba.conf> and change this line (near the
bottom):
# "local" is for Unix domain socket connections only
=item *
-Create the database and a user for the database (note that we are
-using "E<lt>catalystE<gt>" to represent the hidden password of
+Create the database and a user for the database (note that we are
+using "E<lt>catalystE<gt>" to represent the hidden password of
"catalyst"):
$ sudo -u postgres createuser -P catappuser
DROP TABLE IF EXISTS users CASCADE;
DROP TABLE IF EXISTS roles CASCADE;
DROP TABLE IF EXISTS user_roles CASCADE;
-
+
--
-- Create a very simple database to hold book and author information
--
-- created TIMESTAMP NOT NULL DEFAULT now(),
-- updated TIMESTAMP
);
-
+
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
first_name TEXT,
last_name TEXT
);
-
+
-- 'book_authors' is a many-to-many join table between books & authors
CREATE TABLE book_authors (
book_id INTEGER REFERENCES books(id) ON DELETE CASCADE ON UPDATE CASCADE,
author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (book_id, author_id)
);
-
+
---
--- Load some sample data
---
Load the data:
$ psql -U catappuser -W catappdb -f myapp01_psql.sql
- Password for user catappuser:
+ Password for user catappuser:
psql:myapp01_psql.sql:8: NOTICE: CREATE TABLE will create implicit sequence "books_id_seq" for serial column "books.id"
psql:myapp01_psql.sql:8: NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "books_pkey" for table "books"
CREATE TABLE
$ psql -U catappuser -W catappdb
Password for user catappuser: <catalyst>
Welcome to psql 8.3.7, the PostgreSQL interactive terminal.
-
+
Type: \copyright for distribution terms
\h for help with SQL commands
\? for help with psql commands
\g or terminate with semicolon to execute query
\q to quit
-
+
catappdb=> \dt
List of relations
- Schema | Name | Type | Owner
+ Schema | Name | Type | Owner
--------+--------------+-------+------------
public | authors | table | catappuser
public | book_authors | table | catappuser
public | books | table | catappuser
(3 rows)
-
+
catappdb=> select * from books;
- id | title | rating
+ id | title | rating
----+------------------------------------+--------
1 | CCSP SNRS Exam Certification Guide | 5
2 | TCP/IP Illustrated, Volume 1 | 5
4 | Perl Cookbook | 5
5 | Designing with Web Standards | 5
(5 rows)
-
- catappdb=>
+
+ catappdb=>
=back
After the steps where you:
edit lib/MyApp.pm
-
+
create lib/MyAppDB.pm
-
+
create lib/MyAppDB/Book.pm
-
+
create lib/MyAppDB/Author.pm
-
+
create lib/MyAppDB/BookAuthor.pm
--
-- Add users and roles tables, along with a many-to-many join table
--
-
+
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username TEXT,
last_name TEXT,
active INTEGER
);
-
+
CREATE TABLE roles (
id SERIAL PRIMARY KEY,
role TEXT
);
-
+
CREATE TABLE user_roles (
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (user_id, role_id)
);
-
+
--
-- Load up some initial test data
--
- INSERT INTO users (username, password, email_address, first_name, last_name, active)
+ INSERT INTO users (username, password, email_address, first_name, last_name, active)
VALUES ('test01', 'mypass', 't01@na.com', 'Joe', 'Blow', 1);
- INSERT INTO users (username, password, email_address, first_name, last_name, active)
+ INSERT INTO users (username, password, email_address, first_name, last_name, active)
VALUES ('test02', 'mypass', 't02@na.com', 'Jane', 'Doe', 1);
INSERT INTO users (username, password, email_address, first_name, last_name, active)
VALUES ('test03', 'mypass', 't03@na.com', 'No', 'Go', 0);
$ psql -U catappuser -W catappdb -c "select * from users"
Password for user catappuser: <catalyst>
- id | username | password | email_address | first_name | last_name | active
+ id | username | password | email_address | first_name | last_name | active
----+----------+----------+---------------+------------+-----------+--------
1 | test01 | mypass | t01@na.com | Joe | Blow | 1
2 | test02 | mypass | t02@na.com | Jane | Doe | 1
is the C<connect> line):
#!/usr/bin/perl
-
+
use strict;
use warnings;
-
+
use MyApp::Schema;
-
+
my $schema = MyApp::Schema->connect('dbi:Pg:dbname=catappdb', 'catappuser', 'catalyst');
-
+
my @users = $schema->resultset('Users')->all;
-
+
foreach my $user (@users) {
$user->password('mypass');
$user->update;
}
-Run the C<set_hashed_passwords.pl> as per the "normal" flow of the
+Run the C<set_hashed_passwords.pl> as per the "normal" flow of the
tutorial:
$ perl -Ilib set_hashed_passwords.pl
# mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
-
- Type 'help;' or '\h' for help. Type '\c' to clear the current input
+
+ Type 'help;' or '\h' for help. Type '\c' to clear the current input
statement.
-
+
mysql> SHOW VARIABLES LIKE 'have_innodb';
+---------------+-------+
| Variable_name | Value |
| have_innodb | YES |
+---------------+-------+
1 row in set (0.01 sec)
-
+
mysql> exit
Bye
# mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
-
+
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
-
+
mysql> CREATE DATABASE `myapp`;
Query OK, 1 row affected (0.01 sec)
-
+
mysql> GRANT ALL PRIVILEGES ON myapp.* TO 'tutorial'@'localhost' IDENTIFIED BY 'yourpassword';
Query OK, 0 rows affected (0.00 sec)
-
+
mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)
-
+
mysql> exit
Bye
(3, 'Internetworking with TCP/IP Vol.1', 4),
(4, 'Perl Cookbook', 5),
(5, 'Designing with Web Standards', 5);
-
+
INSERT INTO `book_authors` (`book_id`, `author_id`) VALUES
(1, 1),
(1, 2),
(4, 6),
(4, 7),
(5, 8);
-
+
INSERT INTO `authors` (`id`, `first_name`, `last_name`) VALUES
(1, 'Greg', 'Bastien'),
(2, 'Sara', 'Nasseh'),
(6, 'Tom', 'Christiansen'),
(7, 'Nathan', 'Torkington'),
(8, 'Jeffrey', 'Zeldman');
-
+
ALTER TABLE `book_authors`
ADD CONSTRAINT `book_author_ibfk_2` FOREIGN KEY (`author_id`) REFERENCES `authors` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `book_author_ibfk_1` FOREIGN KEY (`book_id`) REFERENCES `books` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
$ mysql -u tutorial -p myapp
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
-
+
Welcome to the MySQL monitor. Commands end with ; or \g.
-
+
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
-
+
mysql> show tables;
+-----------------+
| Tables_in_myapp |
| books |
+-----------------+
3 rows in set (0.00 sec)
-
+
mysql> select * from books;
+----+------------------------------------+--------+
| id | title | rating |
| 5 | Designing with Web Standards | 5 |
+----+------------------------------------+--------+
5 rows in set (0.00 sec)
-
+
mysql>
=back
INSERT INTO `roles` (`id`, `role`) VALUES
(1, 'user'),
(2, 'admin');
-
+
INSERT INTO `users` (`id`, `username`, `password`, `email_address`, `first_name`, `last_name`, `active`) VALUES
(1, 'test01', 'mypass', 't01@na.com', 'Joe', 'Blow', 1),
(2, 'test02', 'mypass', 't02@na.com', 'Jane', 'Doe', 1),
(3, 'test03', 'mypass', 't03@na.com', 'No', 'Go', 0);
-
+
INSERT INTO `user_roles` (`user_id`, `role_id`) VALUES
(1, 1),
(2, 1),
(3, 1),
(1, 2);
-
+
ALTER TABLE `user_roles
ADD CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;