http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
-Your browser should display "Added book 'TCPIP_Illustrated_Vol-2' by
-'Stevens' with a rating of 5." along with a dump of the new book model
-object. You should also see the following DBIC debug messages displayed
-in the development server log messages if you have DBIC_TRACE set:
+Your browser should display "Added book 'TCPIP_Illustrated_Vol-2' by
+'Stevens' with a rating of 5." along with a dump of the new book model
+object as it was returned by DBIC. You should also see the following
+DBIC debug messages displayed in the development server log messages
+if you have DBIC_TRACE set:
INSERT INTO books (rating, title) VALUES (?, ?): `5', `TCPIP_Illustrated_Vol-2'
INSERT INTO book_authors (author_id, book_id) VALUES (?, ?): `4', `6'
C<:Local> attribute), you will notice that it produced output similar
to the following:
- [debug] Loaded Path actions:
- .-------------------------------------+--------------------------------------.
- | Path | Private |
- +-------------------------------------+--------------------------------------+
- | / | /default |
- | / | /index |
- | /books | /books/index |
- | /books/list | /books/list |
- | /books/url_create | /books/url_create |
- '-------------------------------------+--------------------------------------'
+ [debug] Loaded Path actions:
+ .-------------------------------------+--------------------------------------.
+ | Path | Private |
+ +-------------------------------------+--------------------------------------+
+ | / | /default |
+ | / | /index |
+ | /books | /books/index |
+ | /books/list | /books/list |
+ | /books/url_create | /books/url_create |
+ '-------------------------------------+--------------------------------------'
Now start the development server with our basic chained method in
place and the startup debug output should change to something along
the lines of the following:
- [debug] Loaded Path actions:
- .-------------------------------------+--------------------------------------.
- | Path | Private |
- +-------------------------------------+--------------------------------------+
- | / | /default |
- | / | /index |
- | /books | /books/index |
- | /books/list | /books/list |
- '-------------------------------------+--------------------------------------'
-
- [debug] Loaded Chained actions:
- .-------------------------------------+--------------------------------------.
- | Path Spec | Private |
- +-------------------------------------+--------------------------------------+
- | /books/url_create/*/*/* | /books/url_create |
- '-------------------------------------+--------------------------------------'
+ [debug] Loaded Path actions:
+ .-------------------------------------+--------------------------------------.
+ | Path | Private |
+ +-------------------------------------+--------------------------------------+
+ | / | /default |
+ | / | /index |
+ | /books | /books/index |
+ | /books/list | /books/list |
+ '-------------------------------------+--------------------------------------'
+
+ [debug] Loaded Chained actions:
+ .-------------------------------------+--------------------------------------.
+ | Path Spec | Private |
+ +-------------------------------------+--------------------------------------+
+ | /books/url_create/*/*/* | /books/url_create |
+ '-------------------------------------+--------------------------------------'
C<url_create> has disappeared form the "Loaded Path actions" section
but it now shows up under the newly created "Loaded Chained actions"
-section. And, the "/*/*/*" portion clearly shows that we have
-specified that 3 arguments are required.
+section. And, the "/*/*/*" portion clearly shows our requirement for
+three arguments.
As with our non-chained version of C<url_create>, use your browser to
enter the following URL:
- http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
+ http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
You should see the same "Added book 'TCPIP_Illustrated_Vol-2' by
'Stevens' with a rating of 5." along with a dump of the new book model
are now seven books shown (two copies of TCPIP_Illustrated_Vol-2).
-=head2 Refactor to Use a "Base" Method to Start The Chains
+=head2 Refactor to Use a "Base" Method to Start the Chains
Let's make a quick update to our initial Chained action to show a
little more of the power of chaining. First, open
C<lib/MyApp/Controller/Books.pm> in your editor and add the following
method:
- =head2 base
-
- Can place common logic to start chained dispatch here
-
- =cut
-
- sub base :Chained('/') :PathPart('books') :CaptureArgs(0) {
- my ($self, $c) = @_;
-
- $c->log->debug('*** INSIDE BASE METHOD ***');
- }
-
-Although we only use the C<base> method to create a log message, we
-could obviously do any number of things here. For example, if your
-controller always needs a book ID as it's first argument, you could
-have the base method capture that argument (with C<:CaptureArgs(1)>)
-and use it to pull the book object with that ID from the database and
-leave it in the stash for later parts of your chains to then act upon.
-
-In our case, let's modify our C<url_create> method to first call
-C<base>. Open up C<lib/MyApp/Controller/Books.pm> and edit the
-declaration for C<url_create> to match the following:
+ =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::Books');
+
+ # Print a message to the debug log
+ $c->log->debug('*** INSIDE BASE METHOD ***');
+ }
+
+Here we print a log message and store the DBIC resultset in
+C<$c-E<gt>stash-E<gt>{resultset}> so that it's automatically available
+for other actions that chain off C<base>. If your controller always
+needs a book ID as it's first argument, you could have the base method
+capture that argument (with C<:CaptureArgs(1)>) and use it to pull the
+book object with C<-E<gt>find($id)> and leave it in the stash for
+later parts of your chains to then act upon. Because we have several
+actions that don't need to retrieve a book (such as the C<url_create>
+we are working with now), we will instead add that functionality
+to a common C<object> action shortly.
+
+As for C<url_create>, let's modify it to first dispatch to C<base>.
+Open up C<lib/MyApp/Controller/Books.pm> and edit the declaration for
+C<url_create> to match the following:
sub url_create :Chained('base') :PathPart('url_create') :Args(3) {
-Next, let's try out our refactored chain. Restart the development
-server and notice that our "Loaded Chained actions" section has
-changed slightly:
-
- [debug] Loaded Chained actions:
- .-------------------------------------+--------------------------------------.
- | Path Spec | Private |
- +-------------------------------------+--------------------------------------+
- | /books/url_create/*/*/* | /books/base (0) |
- | | => /books/url_create |
- '-------------------------------------+--------------------------------------'
+Next, try out the refactored chain by restarting the development
+server. Notice that our "Loaded Chained actions" section has changed
+slightly:
+
+ [debug] Loaded Chained actions:
+ .-------------------------------------+--------------------------------------.
+ | Path Spec | Private |
+ +-------------------------------------+--------------------------------------+
+ | /books/url_create/*/*/* | /books/base (0) |
+ | | => /books/url_create |
+ '-------------------------------------+--------------------------------------'
The "Path Spec" is the same, but now it maps to two Private actions as
we would expect.
Once again, enter the following URL into your browser:
- http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
+ http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
The same "Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a
-rating of 5." and dump of the new book object should appear. Also
-notice the extra debug message in the development server output from
-the C<base> method. Click the "Return to list" link, you should find
-that there are now eight books shown.
+rating of 5" message and dump of the new book object should appear.
+Also notice the extra debug message in the development server output
+from the C<base> method. Click the "Return to list" link, you should
+find that there are now eight books shown.
=head1 MANUALLY BUILDING A CREATE FORM
Notice that the server startup log reflects the two new chained
methods that we added:
- [debug] Loaded Chained actions:
- .-------------------------------------+--------------------------------------.
- | Path Spec | Private |
- +-------------------------------------+--------------------------------------+
- | /books/form_create | /books/base (0) |
- | | => /books/form_create |
- | /books/form_create_do | /books/base (0) |
- | | => /books/form_create_do |
- | /books/url_create/*/*/* | /books/base (0) |
- | | => /books/url_create |
- '-------------------------------------+--------------------------------------'
+ [debug] Loaded Chained actions:
+ .-------------------------------------+--------------------------------------.
+ | Path Spec | Private |
+ +-------------------------------------+--------------------------------------+
+ | /books/form_create | /books/base (0) |
+ | | => /books/form_create |
+ | /books/form_create_do | /books/base (0) |
+ | | => /books/form_create_do |
+ | /books/url_create/*/*/* | /books/base (0) |
+ | | => /books/url_create |
+ '-------------------------------------+--------------------------------------'
Point your browser to L<http://localhost:3000/books/form_create> and
enter "TCP/IP Illustrated, Vol 3" for the title, a rating of 5, and an
</td>
<td>
[% # Add a link to delete a book %]
- <a href="[% c.uri_for('delete', book.id) %]">Delete</a>
+ <a href="[% c.uri_for(c.controller.action_for('delete'), [book.id]) %]">Delete</a>
</td>
</tr>
[% END -%]
right side of the table with a C<Delete> "button" (for simplicity,
links will be used instead of full HTML buttons).
-B<Note:> You should use more than just a simple link with your
-applications. Consider using some sort of of confirmation page
-(typically with unique actions in your controller for both the
-confirmation and the actual delete operation). Also, you should try
-to use an HTTP POST operation (versus the GET used here) for
-operations that change the state of your application (e.g., the
-database).
+Also notice that we are using a more advanced form of C<uri_for> than
+we have seen before. Here we use C<$c-E<gt>controller-
+E<gt>action_for> to automatically generate a URI appropriate for that
+action based on the method we want to link to while inserting the
+C<book.id> value into the appropriate place. Now, if you ever change
+C<:PathPart('delete')> in your controller method to
+C<:PathPart('kill')>, then your links will automatically update
+without any changes to your .tt2 template file. As long as the name
+of your method does not changed ("delete" here), then your links will
+still be correct. There are a few shortcuts and options when using
+C<action_for()>:
+
+=over 4
+
+=item *
+
+If you are referring to a method in the current controller, you can
+use C<$self-E<gt>action_for('_method_name_')>.
+
+=item *
+
+If you are referring to a method in a different controller, you need
+to include that controller's name as an argument to C<controller()>, as in
+C<$c-E<gt>controller('_controller_name_')-E<gt>action_for('_method_name_')>.
+
+=back
+
+B<Note:> In practice you should B<never> use a GET request to delete a
+record -- always use POST for actions that will modify data. We are
+doing it here for illustrative and simplicity purposes only.
+
+
+=head2 Add a Common Method to Retrieve a Book for the Chain
+
+As mentioned earlier, since we have a mixture of actions that operate
+on a single book ID and others that do no, we should not have C<base>
+capture the book ID, find the corresponding book in the database and
+save it in the stash for later links in the chain. However, just
+because that logic does not belong in C<base> doesn't mean that we
+can't create another location to centralize the book lookup code. In
+our case, we will create a method called C<object> that will store the
+specific book in the stash. Chains that always operate on a single
+existing book can chain off this method, but methods such as
+C<url_create> that don't operate on an existing book can chain
+directly off base.
+
+To add the C<object> method, edit C<lib/MyApp/Controller/Books.pm>
+and 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};
+ }
+
+Now, any other method that chains off C<object> will automatically
+have the appropriate book waiting for it in
+C<$c-E<gt>stash-Egt>{object}>.
+
+Also note that we are using different technique for setting
+C<$c-E<gt>stash>. The advantage of this style is that it let's you
+set multiple stash variables at a time. For example:
+
+ $c->stash(object => $c->stash->{resultset}->find($id),
+ another_thing => 1);
+
+or as a hashref:
+
+ $c->stash({object => $c->stash->{resultset}->find($id),
+ another_thing => 1});
+
+Either format works, but the C<$c-E<gt>stash(name =E<gt> value);>
+style is growing in popularity -- you may which to use it all
+the time (even when you are only setting a single value).
=head2 Add a Delete Action to the Controller
=cut
- sub delete :Chained('base') :PathPart('delete') :Args(1) {
- # $id = primary key of book to delete
- my ($self, $c, $id) = @_;
+ sub delete :Chained('object') :PathPart('delete') :Args(0) {
+ my ($self, $c) = @_;
- # Search for the book and then delete it
- $c->model('DB::Books')->search({id => $id})->delete_all;
+ # Use the book object saved by 'object' and delete it along
+ # with related 'book_authors' entries
+ $c->stash->{object}->delete;
# Set a status message to be displayed at the top of the view
$c->stash->{status_msg} = "Book deleted.";
$c->forward('list');
}
-This method first deletes the book with the specified primary key ID.
-However, it also removes the corresponding entry from the
-C<book_authors> table. Note that C<delete_all> was used instead of
-C<delete>: whereas C<delete_all> also removes the join table entries in
-C<book_authors>, C<delete> does not (only use C<delete_all> if you
-really need the cascading deletes... otherwise you are wasting resources).
+This method first deletes the book object saved by the C<object> method.
+However, it also removes the corresponding entry from the
+C<book_authors> table with a cascading delete.
Then, rather than forwarding to a "delete done" page as we did with the
earlier create example, it simply sets the C<status_msg> to display a
If the application is still running from before, use C<Ctrl-C> to kill
it. Then restart the server:
- $ script/myapp_server.pl
+ $ DBIC_TRACE=1 script/myapp_server.pl
The C<delete> method now appears in the "Loaded Chained actions" section
of the startup debug output:
- [debug] Loaded Chained actions:
- .-------------------------------------+--------------------------------------.
- | Path Spec | Private |
- +-------------------------------------+--------------------------------------+
- | /books/delete/* | /books/base (0) |
- | | => /books/delete |
- | /books/form_create | /books/base (0) |
- | | => /books/form_create |
- | /books/form_create_do | /books/base (0) |
- | | => /books/form_create_do |
- | /books/url_create/*/*/* | /books/base (0) |
- | | => /books/url_create |
- '-------------------------------------+--------------------------------------'
+ [debug] Loaded Chained actions:
+ .-------------------------------------+--------------------------------------.
+ | Path Spec | Private |
+ +-------------------------------------+--------------------------------------+
+ | /books/id/*/delete | /books/base (0) |
+ | | -> /books/object (1) |
+ | | => /books/delete |
+ | /books/form_create | /books/base (0) |
+ | | => /books/form_create |
+ | /books/form_create_do | /books/base (0) |
+ | | => /books/form_create_do |
+ | /books/url_create/*/*/* | /books/base (0) |
+ | | => /books/url_create |
+ '-------------------------------------+--------------------------------------'
Then point your browser to L<http://localhost:3000/books/list> and click
the "Delete" link next to the first "TCPIP_Illustrated_Vol-2". A green
"Book deleted" status message should display at the top of the page,
-along with a list of the eight remaining books.
+along with a list of the eight remaining books. You will also see the
+cascading delete operation via the DBIC_TRACE output:
+
+ DELETE FROM books WHERE ( id = ? ): '6'
+ SELECT me.book_id, me.author_id FROM book_authors me WHERE ( me.book_id = ? ): '6'
+ DELETE FROM book_authors WHERE ( author_id = ? AND book_id = ? ): '4', '6'
=head2 Fixing a Dangerous URL
http://localhost:3000/books/delete/6
-What if the user were to press reload with this URL still active? In
-this case the redundant delete is harmless, but in other cases this
-could clearly be extremely dangerous.
+What if the user were to press reload with this URL still active? In
+this case the redundant delete is harmless (although it does generate
+an exception screen, it doesn't perform any undesirable actions on the
+application or database), but in other cases this could clearly be
+extremely dangerous.
We can improve the logic by converting to a redirect. Unlike
C<$c-E<gt>forward('list'))> or C<$c-E<gt>detach('list'))> that perform
open C<lib/MyApp/Controller/Books.pm> and edit the existing
C<sub delete> method to match:
- =head2 delete
+ =head2 delete
Delete a book
-
+
=cut
- sub delete :Chained('base') :PathPart('delete') :Args(1) {
- # $id = primary key of book to delete
- my ($self, $c, $id) = @_;
+ sub delete :Chained('object') :PathPart('delete') :Args(0) {
+ my ($self, $c) = @_;
- # Search for the book and then delete it
- $c->model('DB::Books')->search({id => $id})->delete_all;
+ # Use the book object saved by 'object' and delete it along
+ # with related 'book_authors' 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
- $c->response->redirect($c->uri_for('/books/list'));
+ # 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')));
}
=head2 Try the Delete and Redirect Logic
Restart the development server and point your browser to
-L<http://localhost:3000/books/list> and delete the first copy of the
-remaining two "TCPIP_Illustrated_Vol-2" books. The URL in your
-browser should return to the L<http://localhost:3000/books/list> URL,
-so that is an improvement, but notice that I<no green "Book deleted"
-status message is displayed>. Because the stash is reset on every
-request (and a redirect involves a second request), the C<status_msg>
-is cleared before it can be displayed.
+L<http://localhost:3000/books/list> (don't just hit "Refresh" in your
+browser since we left the URL in an invalid state in the previous
+section!) and delete the first copy of the remaining two
+"TCPIP_Illustrated_Vol-2" books. The URL in your browser should return
+to the L<http://localhost:3000/books/list> URL, so that is an
+improvement, but notice that I<no green "Book deleted" status message is
+displayed>. Because the stash is reset on every request (and a redirect
+involves a second request), the C<status_msg> is cleared before it can
+be displayed.
=head2 Using C<uri_for> to Pass Query Parameters
=cut
- sub delete :Chained('base') :PathPart('delete') :Args(1) {
- # $id = primary key of book to delete
- my ($self, $c, $id) = @_;
+ sub delete :Chained('object') :PathPart('delete') :Args(0) {
+ my ($self, $c) = @_;
- # Search for the book and then delete it
- $c->model('DB::Books')->search({id => $id})->delete_all;
+ # 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 with status msg as an arg
- $c->response->redirect($c->uri_for('/books/list',
+ $c->response->redirect($c->uri_for($self->action_for('list'),
{status_msg => "Book deleted."}));
}
=head2 Try the Delete and Redirect With Query Param Logic
Restart the development server and point your browser to
-L<http://localhost:3000/books/list>. Then delete the remaining copy
-of "TCPIP_Illustrated_Vol-2". The green "Book deleted" status message
+L<http://localhost:3000/books/list> (you should now be able to safely
+hit "refresh" in your browser). Then delete the remaining copy of
+"TCPIP_Illustrated_Vol-2". The green "Book deleted" status message
should return.
B<NOTE:> Another popular method for maintaining server-side
server and doesn't "pollute" your URLs, B<it is important to note that
C<flash> can lead to situations where the wrong information shows up
in the wrong browser window if the user has multiple windows or
-browser tabs open.> (For example, Window A causes something to be
+browser tabs open.> For example, Window A causes something to be
placed in the stash, but before that window performs a redirect,
Window B makes a request to the server and gets the status information
-that should really go to Window A.) For this reason, you may wish
+that should really go to Window A. For this reason, you may wish
to use the "query param" technique shown here in your applications.