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);
}