Catalyst-Manual

 view release on metacpan or  search on metacpan

lib/Catalyst/Manual/Tutorial/04_BasicCRUD.pod  view on Meta::CPAN


B<< Do NOT get arguments through "C<CaptureArgs()>," use "C<Args()>" instead to end a chain >>

=item *

Specify the path to match with C<PathPart()>

=back


=back

In our C<url_create> method above, we have combined all three parts into
a single method: C<:Chained('/')> to start the chain,
C<:PathPart('books/url_create')> to specify the base URL to match, and
C<:Args(3)> to capture exactly three arguments and to end the chain.

As we will see shortly, a chain can consist of as many "links" as you
wish, with each part capturing some arguments and doing some work along
the way.  We will continue to use the Chained action type in this
chapter of the tutorial and explore slightly more advanced capabilities
with the base method and delete feature below.  But Chained dispatch is
capable of far more.  For additional information, see
L<Catalyst::Manual::Intro/Action types>,
L<Catalyst::DispatchType::Chained>, and the 2006 Advent calendar entry
on the subject: L<http://www.catalystframework.org/calendar/2006/10>.


=head2 Try the Chained Action

If you look back at the development server startup logs from your
initial version of the C<url_create> method (the one using the 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                    |
    '-------------------------------------+--------------------------------------'

When the development server restarts after our conversion to Chained
dispatch, the 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                    |
    '-------------------------------------+--------------------------------------'

C<url_create> has disappeared from the "Loaded Path actions" section but
it now shows up under the newly created "Loaded Chained actions"
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

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
object.  Click the "Return to list" link, and you should find that there
are now seven books shown (two copies of I<TCPIP_Illustrated_Vol-2>).


=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
F<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) = @_;

        # 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 ***');
    }

Here we print a log message and store the DBIC ResultSet in
C<< $c->stash->{resultset} >> so that it's automatically available
for other actions that chain off C<base>.  If your controller always
needs a book ID as its 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<< ->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 F<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) {

Once you save F<lib/MyApp/Controller/Books.pm>, notice that the
development server will restart and our "Loaded Chained actions" section
will 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.  The C<base> method is being triggered by the C</books>
part of the URL.  However, the processing then continues to the
C<url_create> method because this method "chained" off C<base> and
specified C<:PathPart('url_create')> (note that we could have omitted
the "PathPart" here because it matches the name of the method, but we
will include it to make the logic as explicit as possible).

Once again, enter the following URL into your browser:

    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." message and a dump of the new book object should appear.
Also notice the extra "INSIDE BASE METHOD" debug message in the
development server output from the C<base> method.  Click the "Return to
list" link, and you should find that there are now eight books shown.
(You may have a larger number of books if you repeated any of the
"create" actions more than once.  Don't worry about it as long as the
number of books is appropriate for the number of times you added new
books... there should be the original five books added via
F<myapp01.sql> plus one additional book for each time you ran one of the
url_create variations above.)


=head1 MANUALLY BUILDING A CREATE FORM

Although the C<url_create> action in the previous step does begin to
reveal the power and flexibility of both Catalyst and DBIC, it's
obviously not a very realistic example of how users should be expected
to enter data.  This section begins to address that concern (but just
barely, see L<Chapter 9|Catalyst::Manual::Tutorial::09_AdvancedCRUD>
for better options for handling web-based forms).


=head2 Add Method to Display The Form

Edit F<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');
    }

This action simply invokes a view containing a form to create a book.

lib/Catalyst/Manual/Tutorial/04_BasicCRUD.pod  view on Meta::CPAN


=head2 Add a Template for the Form

Open F<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>
        <tr><td>Rating:</td><td><input type="text" name="rating"></td></tr>
        <tr><td>Author ID:</td><td><input type="text" name="author_id"></td></tr>
    </table>
    <input type="submit" name="Submit" value="Submit">
    </form>

Note that we have specified the target of the form data as
C<form_create_do>, the method created in the section that follows.


=head2 Add a Method to Process Form Values and Update Database

Edit F<lib/MyApp/Controller/Books.pm> and add the following method to
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,
                rating  => $rating,
            });
        # Handle relationship with 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});

        # Store new model object in stash and set template
        $c->stash(book     => $book,
                  template => 'books/create_done.tt2');
    }


=head2 Test Out The 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                 |
    '-------------------------------------+--------------------------------------'

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
author ID of 4.  You should then see the output of the same
F<create_done.tt2> template seen in earlier examples.  Finally, click
"Return to list" to view the full list of books.

B<Note:> Having the user enter the primary key ID for the author is
obviously crude; we will address this concern with a drop-down list and
add validation to our forms in
L<Chapter 9|Catalyst::Manual::Tutorial::09_AdvancedCRUD>.


=head1 A SIMPLE DELETE FEATURE

Turning our attention to the Delete portion of CRUD, this section
illustrates some basic techniques that can be used to remove information
from the database.


=head2 Include a Delete Link in the List

Edit F<root/src/books/list.tt2> and update it to match the following
(two sections have changed: 1) the additional '<th>Links</th>' table
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>
    [% # Display each book in a table row %]
    [% FOREACH book IN books -%]
        <tr>
            <td>[% book.title %]</td>
            <td>[% book.rating %]</td>
            <td>
                [% # NOTE: See Chapter 4 for a better way to do this!                      -%]
                [% # First initialize a TT variable to hold a list.  Then use a TT FOREACH -%]
                [% # loop in 'side effect notation' to load just the last names of the     -%]
                [% # authors into the list. Note that the 'push' TT vmethod doesn't return -%]
                [% # a value, so nothing will be printed here.  But, if you have something -%]
                [% # in TT that does return a value and you don't want it printed, you     -%]

lib/Catalyst/Manual/Tutorial/04_BasicCRUD.pod  view on Meta::CPAN


        # 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 ***");
    }

Now, any other method that chains off C<object> will automatically have
the appropriate book waiting for it in C<< $c->stash->{object} >>.


=head2 Add a Delete Action to the Controller

Open F<lib/MyApp/Controller/Books.pm> in your editor and add the
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');
    }

This method first deletes the book object saved by the C<object> method.
However, it also removes the corresponding entry from the C<book_author>
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
notification to the user as the normal list view is rendered.

The C<delete> action uses the context C<forward> method to return the
user to the book list.  The C<detach> method could have also been used.
Whereas C<forward> I<returns> to the original action once it is
completed, C<detach> does I<not> return.  Other than that, the two are
equivalent.


=head2 Try the Delete Feature

Once you save the Books controller, the server should automatically
restart.  The C<delete> method should now appear in the "Loaded Chained
actions" section of the startup debug output:

    [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.  You will also see the
cascading delete operation via the DBIC_TRACE output:

    SELECT me.id, me.title, me.rating FROM book me WHERE ( ( me.id = ? ) ): '6'
    DELETE FROM book WHERE ( id = ? ): '6'

If you get the error C<file error - books/delete.tt2: not found> then you
probably forgot to uncomment the template line in C<sub list> at the end of
chapter 3.

=head2 Fixing a Dangerous URL

Note the URL in your browser once you have performed the deletion in the
prior step -- it is still referencing the delete action:

    http://localhost:3000/books/id/6/delete

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 lead to
trouble.

We can improve the logic by converting to a redirect.  Unlike
C<< $c->forward('list')) >> or C<< $c->detach('list')) >> that perform a
server-side alteration in the flow of processing, a redirect is a
client-side mechanism that causes the browser to issue an entirely new
request.  As a result, the URL in the browser is updated to match the
destination of the redirection URL.

To convert the forward used in the previous section to a redirect, open
F<lib/MyApp/Controller/Books.pm> and edit the existing C<sub delete>
method to match:

    =head2 delete

    Delete a book

    =cut

    sub delete :Chained('object') :PathPart('delete') :Args(0) {
        my ($self, $c) = @_;



( run in 1.376 second using v1.01-cache-2.11-cpan-71847e10f99 )