X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=catagits%2FCatalyst-Manual.git;a=blobdiff_plain;f=lib%2FCatalyst%2FManual%2FTutorial%2F04_BasicCRUD.pod;h=3addacb122868c94bacb7283d5c230dbdd21d648;hp=fc938df7ebc1cadb60d7e40f434df86bd17f43e7;hb=429d1caf111575afa4c25287cc48d7ed712af327;hpb=22fe0f18ac89a984b2a27ff0eb146f485cfb9a8c diff --git a/lib/Catalyst/Manual/Tutorial/04_BasicCRUD.pod b/lib/Catalyst/Manual/Tutorial/04_BasicCRUD.pod index fc938df..3addacb 100644 --- a/lib/Catalyst/Manual/Tutorial/04_BasicCRUD.pod +++ b/lib/Catalyst/Manual/Tutorial/04_BasicCRUD.pod @@ -60,12 +60,13 @@ This chapter of the tutorial builds on the fairly primitive application created in L to add basic support for Create, Read, Update, and Delete (CRUD) of C -objects. Note that the 'list' function in Chapter 3 already implements -the Read portion of CRUD (although Read normally refers to reading a -single object; you could implement full Read functionality using the -techniques introduced below). This section will focus on the Create and -Delete aspects of CRUD. More advanced capabilities, including full -Update functionality, will be addressed in +objects. Note that the 'list' function in +L already +implements the Read portion of CRUD (although Read normally refers to +reading a single object; you could implement full Read functionality +using the techniques introduced below). This section will focus on the +Create and Delete aspects of CRUD. More advanced capabilities, +including full Update functionality, will be addressed in L. Although this chapter of the tutorial will show you how to build CRUD @@ -74,8 +75,9 @@ of tool to automate the process. You get less control, but it can be quick and easy. For example, see L, L, and L. -You can check out the source code for this example from the Catalyst -Subversion repository as per the instructions in +Source code for the tutorial in included in the F +directory of the Tutorial Virtual machine (one subdirectory per +chapter). There are also instructions for downloading the code in L. @@ -91,35 +93,35 @@ submission in the sections that follow). Edit C 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 "//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'); } @@ -150,7 +152,7 @@ Edit C and then enter: [% # 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 -%] @@ -158,20 +160,20 @@ Edit C and then enter: [% # 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. -%]

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 %].

- + [% # Provide a link back to the list page. 'c.uri_for' builds -%] [% # a full URI; e.g., 'http://localhost:3000/books/list' -%]

Return to list

- + [% # Try out the TT Dumper (for development only!) -%]
     Dump of the 'book' variable:
@@ -218,8 +220,8 @@ are now six books shown (if necessary, Shift+Reload or Ctrl+Reload your
 browser at the C 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'
 
 
@@ -235,11 +237,11 @@ to match the following:
     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
         # "// 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-Estash-E{resultset}> so that it's automatically available
+C<< $c->stash->{resultset} >> so that it's automatically available
 for other actions that chain off C.  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<-Efind($id)> and leave it in the stash for later
+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 we are
 working with now), we will instead add that functionality to a common
@@ -482,14 +484,14 @@ for better options for handling web-based forms).
 Edit C 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');
     }
@@ -502,7 +504,7 @@ This action simply invokes a view containing a form to create a book.
 Open C in your editor and enter:
 
     [% META title = 'Manual Form Book Create' -%]
-    
+
     
@@ -522,19 +524,19 @@ Edit C 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, @@ -544,7 +546,7 @@ save the form information to the database: $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'); @@ -643,7 +645,7 @@ that modifies data should be handled with a form sending a POST request). Also notice that we are using a more advanced form of C than we -have seen before. Here we use C<$c-Econtroller-Eaction_for> to +have seen before. Here we use C<< $c->controller->action_for >> to automatically generate a URI appropriate for that action based on the method we want to link to while inserting the C value into the appropriate place. Now, if you ever change C<:PathPart('delete')> in @@ -658,13 +660,13 @@ few shortcuts and options when using C: =item * If you are referring to a method in the current controller, you can use -C<$self-Eaction_for('_method_name_')>. +C<< $self->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, as in -C<$c-Econtroller('_controller_name_')-Eaction_for('_method_name_')>. +C<< $c->controller('_controller_name_')->action_for('_method_name_') >>. =back @@ -691,30 +693,30 @@ To add the C method, edit C 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}; - + # 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 will automatically have -the appropriate book waiting for it in C<$c-Estash-E{object}>. +the appropriate book waiting for it in C<< $c->stash->{object} >>. =head2 Add a Delete Action to the Controller @@ -723,21 +725,21 @@ Open C 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'); } @@ -786,9 +788,10 @@ 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' - SELECT me.book_id, me.author_id FROM book_author me WHERE ( me.book_id = ? ): '6' - DELETE FROM book_author WHERE ( author_id = ? AND book_id = ? ): '4', '6' +If you get the error C then you +probably forgot to uncomment the template line in C at the end of +chapter 3. =head2 Fixing a Dangerous URL @@ -804,7 +807,7 @@ 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-Eforward('list'))> or C<$c-Edetach('list'))> that perform a +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 @@ -815,21 +818,21 @@ C and edit the existing C 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'))); @@ -860,18 +863,18 @@ C and update the existing C 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."})); @@ -896,7 +899,10 @@ query parameter: Although the sample above only shows the C div, leave the rest of the file intact -- the only change we made to the C was to add "C<|| c.request.params.status_msg>" to the -Cspan class="message"E> line. +C<< >> line. Note that we definitely want +the "C<| html>" TT filter here since it would be easy for users to +modify the message on the URL and possibly inject harmful code into the +application if we left that off. =head2 Try the Delete and Redirect With Query Param Logic @@ -942,6 +948,15 @@ book was added and when each book is updated: sqlite> .quit $ +Here are the commands without the surrounding sqlite3 prompt and output +in case you want to cut and paste them as a single block (but still +start sqlite3 before you paste these in): + + ALTER TABLE book ADD created TIMESTAMP; + ALTER TABLE book ADD updated TIMESTAMP; + UPDATE book SET created = DATETIME('NOW'), updated = DATETIME('NOW'); + SELECT * FROM book; + This will modify the C table to include the two new fields and populate those fields with the current time. @@ -954,11 +969,11 @@ the new fields: $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \ create=static components=TimeStamp dbi:SQLite:myapp.db \ on_connect_do="PRAGMA foreign_keys = ON" - exists "/root/dev/MyApp/script/../lib/MyApp/Model" - exists "/root/dev/MyApp/script/../t" - Dumping manual schema for MyApp::Schema to directory /root/dev/MyApp/script/../lib ... + exists "/home/catalyst/dev/MyApp/script/../lib/MyApp/Model" + exists "/home/catalyst/dev/MyApp/script/../t" + Dumping manual schema for MyApp::Schema to directory /home/catalyst/dev/MyApp/script/../lib ... Schema dump completed. - exists "/root/dev/MyApp/script/../lib/MyApp/Model/DB.pm" + exists "/home/catalyst/dev/MyApp/script/../lib/MyApp/Model/DB.pm" Notice that we modified our use of the helper slightly: we told it to include the L in the C line of @@ -970,11 +985,11 @@ call to C. However, also notice that the C relationships we manually added below the "C<# DO NOT MODIFY...>" line were automatically preserved. -While we have this file open, let's update it with some additional -information to have DBIC automatically handle the updating of these two -fields for us. Insert the following code at the bottom of the file (it -B be B the "C<# DO NOT MODIFY...>" line and B the -C<1;> on the last line): +While we C open, let's update it with +some additional information to have DBIC automatically handle the +updating of these two fields for us. Insert the following code at the +bottom of the file (it B be B the "C<# DO NOT MODIFY...>" +line and B the C<1;> on the last line): # # Enable automatic date handling @@ -1021,7 +1036,7 @@ entered for it (see the last line in the listing below): 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' @@ -1045,47 +1060,47 @@ a directory where DBIx::Class will look for our ResultSet Class: Then open C 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: =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). @@ -1121,14 +1136,14 @@ C controller that lists books that are both recent I have 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 @@ -1138,7 +1153,7 @@ the following method: ->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). @@ -1160,7 +1175,7 @@ you added books to your database): 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 @@ -1169,14 +1184,14 @@ ResultSet Class. To do this, open C 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%" } }); @@ -1185,18 +1200,18 @@ and add the following method: We defined the search string as C<$title_str> to make the method more flexible. Now update the C method in C to match the following (we have -replaced the C<-Esearch> line with the C<-Etitle_like> line +replaced the C<< ->search >> line with the C<< ->title_like >> line 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 @@ -1206,7 +1221,7 @@ shown here -- the rest of the method should be the same): ->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). @@ -1236,7 +1251,7 @@ always, it must be above the closing "C<1;>"): # sub full_name { my ($self) = @_; - + return $self->first_name . ' ' . $self->last_name; } @@ -1305,14 +1320,14 @@ return the number of authors for a book. Open C 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; @@ -1322,21 +1337,21 @@ Next, let's add a method to return a list of authors for a book to the same C 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); } @@ -1376,17 +1391,18 @@ output should be the same even though the backend code has been trimmed down. +You can jump to the next chapter of the tutorial here: +L + + =head1 AUTHOR Kennedy Clark, C Feel free to contact the author for any errors or suggestions, but the best way to report issues is via the CPAN RT Bug system at -. - -The most recent version of the Catalyst Tutorial can be found at -L. +L. -Copyright 2006-2010, Kennedy Clark, under the +Copyright 2006-2011, Kennedy Clark, under the Creative Commons Attribution Share-Alike License Version 3.0 (L).
Title: