Commit | Line | Data |
e075db0c |
1 | =head1 NAME |
d442cc9f |
2 | |
3533daff |
3 | Catalyst::Manual::Tutorial::BasicCRUD - Catalyst Tutorial - Part 4: Basic CRUD |
d442cc9f |
4 | |
5 | |
6 | =head1 OVERVIEW |
7 | |
3533daff |
8 | This is B<Part 4 of 10> for the Catalyst tutorial. |
d442cc9f |
9 | |
10 | L<Tutorial Overview|Catalyst::Manual::Tutorial> |
11 | |
12 | =over 4 |
13 | |
14 | =item 1 |
15 | |
16 | L<Introduction|Catalyst::Manual::Tutorial::Intro> |
17 | |
18 | =item 2 |
19 | |
20 | L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics> |
21 | |
22 | =item 3 |
23 | |
3533daff |
24 | L<More Catalyst Basics|Catalyst::Manual::Tutorial::MoreCatalystBasics> |
d442cc9f |
25 | |
26 | =item 4 |
27 | |
3533daff |
28 | B<Basic CRUD> |
d442cc9f |
29 | |
30 | =item 5 |
31 | |
3533daff |
32 | L<Authentication|Catalyst::Manual::Tutorial::Authentication> |
d442cc9f |
33 | |
34 | =item 6 |
35 | |
3533daff |
36 | L<Authorization|Catalyst::Manual::Tutorial::Authorization> |
d442cc9f |
37 | |
38 | =item 7 |
39 | |
3533daff |
40 | L<Debugging|Catalyst::Manual::Tutorial::Debugging> |
d442cc9f |
41 | |
42 | =item 8 |
43 | |
3533daff |
44 | L<Testing|Catalyst::Manual::Tutorial::Testing> |
d442cc9f |
45 | |
46 | =item 9 |
47 | |
3533daff |
48 | L<Advanced CRUD|Catalyst::Manual::Tutorial::AdvancedCRUD> |
49 | |
50 | =item 10 |
51 | |
d442cc9f |
52 | L<Appendices|Catalyst::Manual::Tutorial::Appendices> |
53 | |
54 | =back |
55 | |
56 | |
d442cc9f |
57 | =head1 DESCRIPTION |
58 | |
59 | This part of the tutorial builds on the fairly primitive application |
3533daff |
60 | created in Part 3 to add basic support for Create, Read, Update, and |
d442cc9f |
61 | Delete (CRUD) of C<Book> objects. Note that the 'list' function in Part |
62 | 2 already implements the Read portion of CRUD (although Read normally |
63 | refers to reading a single object; you could implement full read |
64 | functionality using the techniques introduced below). This section will |
65 | focus on the Create and Delete aspects of CRUD. More advanced |
66 | capabilities, including full Update functionality, will be addressed in |
3533daff |
67 | Part 9. |
d442cc9f |
68 | |
1390ef0e |
69 | Although this part of the tutorial will show you how to build CRUD |
70 | functionality yourself, another option is to use a "CRUD builder" type |
71 | of tool to automate the process. You get less control, but it's quick |
72 | and easy. For example, see |
73 | L<CatalystX::ListFramework::Builder|CatalystX::ListFramework::Builder>, |
74 | L<CatalystX::CRUD|CatalystX::CRUD>, and |
7edc5484 |
75 | L<CatalystX::CRUD::YUI|CatalystX::CRUD::YUI>. |
1390ef0e |
76 | |
d442cc9f |
77 | You can checkout the source code for this example from the catalyst |
78 | subversion repository as per the instructions in |
1390ef0e |
79 | L<Catalyst::Manual::Tutorial::Intro|Catalyst::Manual::Tutorial::Intro>. |
d442cc9f |
80 | |
3533daff |
81 | |
d442cc9f |
82 | =head1 FORMLESS SUBMISSION |
83 | |
3533daff |
84 | Our initial attempt at object creation will utilize the "URL |
85 | arguments" feature of Catalyst (we will employ the more common form- |
86 | based submission in the sections that follow). |
d442cc9f |
87 | |
88 | |
89 | =head2 Include a Create Action in the Books Controller |
90 | |
91 | Edit C<lib/MyApp/Controller/Books.pm> and enter the following method: |
92 | |
93 | =head2 url_create |
94 | |
95 | Create a book with the supplied title, rating, and author |
96 | |
97 | =cut |
98 | |
99 | sub url_create : Local { |
100 | # In addition to self & context, get the title, rating, & |
101 | # author_id args from the URL. Note that Catalyst automatically |
102 | # puts extra information after the "/<controller_name>/<action_name/" |
103 | # into @_ |
104 | my ($self, $c, $title, $rating, $author_id) = @_; |
105 | |
106 | # Call create() on the book model object. Pass the table |
107 | # columns/field values we want to set as hash values |
d0496197 |
108 | my $book = $c->model('DB::Books')->create({ |
d442cc9f |
109 | title => $title, |
110 | rating => $rating |
111 | }); |
112 | |
113 | # Add a record to the join table for this book, mapping to |
114 | # appropriate author |
115 | $book->add_to_book_authors({author_id => $author_id}); |
116 | # Note: Above is a shortcut for this: |
117 | # $book->create_related('book_authors', {author_id => $author_id}); |
118 | |
119 | # Assign the Book object to the stash for display in the view |
120 | $c->stash->{book} = $book; |
121 | |
122 | # This is a hack to disable XSUB processing in Data::Dumper |
123 | # (it's used in the view). This is a work-around for a bug in |
124 | # the interaction of some versions or Perl, Data::Dumper & DBIC. |
125 | # You won't need this if you aren't using Data::Dumper (or if |
126 | # you are running DBIC 0.06001 or greater), but adding it doesn't |
127 | # hurt anything either. |
128 | $Data::Dumper::Useperl = 1; |
129 | |
130 | # Set the TT template to use |
131 | $c->stash->{template} = 'books/create_done.tt2'; |
132 | } |
133 | |
134 | Notice that Catalyst takes "extra slash-separated information" from the |
135 | URL and passes it as arguments in C<@_>. The C<url_create> action then |
136 | uses a simple call to the DBIC C<create> method to add the requested |
137 | information to the database (with a separate call to |
138 | C<add_to_book_authors> to update the join table). As do virtually all |
139 | controller methods (at least the ones that directly handle user input), |
140 | it then sets the template that should handle this request. |
141 | |
142 | |
143 | =head2 Include a Template for the C<url_create> Action: |
144 | |
145 | Edit C<root/src/books/create_done.tt2> and then enter: |
146 | |
147 | [% # Use the TT Dumper plugin to Data::Dumper variables to the browser -%] |
148 | [% # Not a good idea for production use, though. :-) 'Indent=1' is -%] |
149 | [% # optional, but prevents "massive indenting" of deeply nested objects -%] |
150 | [% USE Dumper(Indent=1) -%] |
151 | |
152 | [% # Set the page title. META can 'go back' and set values in templates -%] |
153 | [% # that have been processed 'before' this template (here it's for -%] |
154 | [% # root/lib/site/html and root/lib/site/header). Note that META on -%] |
155 | [% # simple strings (e.g., no variable interpolation). -%] |
156 | [% META title = 'Book Created' %] |
157 | |
158 | [% # Output information about the record that was added. First title. -%] |
159 | <p>Added book '[% book.title %]' |
160 | |
161 | [% # Output the last name of the first author. This is complicated by an -%] |
162 | [% # issue in TT 2.15 where blessed hash objects are not handled right. -%] |
163 | [% # First, fetch 'book.authors' from the DB once. -%] |
164 | [% authors = book.authors %] |
165 | [% # Now use IF statements to test if 'authors.first' is "working". If so, -%] |
166 | [% # we use it. Otherwise we use a hack that seems to keep TT 2.15 happy. -%] |
167 | by '[% authors.first.last_name IF authors.first; |
168 | authors.list.first.value.last_name IF ! authors.first %]' |
169 | |
170 | [% # Output the rating for the book that was added -%] |
171 | with a rating of [% book.rating %].</p> |
172 | |
173 | [% # Provide a link back to the list page -%] |
174 | [% # 'uri_for()' builds a full URI; e.g., 'http://localhost:3000/books/list' -%] |
8a7c5151 |
175 | <p><a href="[% c.uri_for('/books/list') %]">Return to list</a></p> |
d442cc9f |
176 | |
177 | [% # Try out the TT Dumper (for development only!) -%] |
178 | <pre> |
179 | Dump of the 'book' variable: |
180 | [% Dumper.dump(book) %] |
181 | </pre> |
182 | |
1390ef0e |
183 | The TT C<USE> directive allows access to a variety of plugin modules |
184 | (TT plugins, that is, not Catalyst plugins) to add extra functionality |
185 | to the base TT capabilities. Here, the plugin allows |
186 | L<Data::Dumper|Data::Dumper> "pretty printing" of objects and |
187 | variables. Other than that, the rest of the code should be familiar |
188 | from the examples in Part 3. |
d442cc9f |
189 | |
d442cc9f |
190 | |
191 | =head2 Try the C<url_create> Feature |
192 | |
193 | If the application is still running from before, use C<Ctrl-C> to kill |
194 | it. Then restart the server: |
195 | |
1390ef0e |
196 | $ DBIC_TRACE=1 script/myapp_server.pl |
d442cc9f |
197 | |
198 | Note that new path for C</books/url_create> appears in the startup debug |
199 | output. |
200 | |
201 | B<TIP>: You can use C<script/myapp_server.pl -r> to have the development |
202 | server auto-detect changed files and reload itself (if your browser acts |
203 | odd, you should also try throwing in a C<-k>). If you make changes to |
204 | the TT templates only, you do not need to reload the development server |
205 | (only changes to "compiled code" such as Controller and Model C<.pm> |
206 | files require a reload). |
207 | |
208 | Next, use your browser to enter the following URL: |
209 | |
210 | http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4 |
211 | |
89d3dae9 |
212 | Your browser should display "Added book 'TCPIP_Illustrated_Vol-2' by |
d442cc9f |
213 | 'Stevens' with a rating of 5." along with a dump of the new book model |
214 | object. You should also see the following DBIC debug messages displayed |
3533daff |
215 | in the development server log messages if you have DBIC_TRACE set: |
d442cc9f |
216 | |
217 | INSERT INTO books (rating, title) VALUES (?, ?): `5', `TCPIP_Illustrated_Vol-2' |
218 | INSERT INTO book_authors (author_id, book_id) VALUES (?, ?): `4', `6' |
219 | SELECT author.id, author.first_name, author.last_name |
220 | FROM book_authors me JOIN authors author |
221 | ON ( author.id = me.author_id ) WHERE ( me.book_id = ? ): '6' |
222 | |
223 | The C<INSERT> statements are obviously adding the book and linking it to |
224 | the existing record for Richard Stevens. The C<SELECT> statement results |
225 | from DBIC automatically fetching the book for the C<Dumper.dump(book)>. |
226 | |
1390ef0e |
227 | If you then click the "Return to list" link, you should find that |
228 | there are now six books shown (if necessary, Shift+Reload or |
229 | Ctrl+Reload your browser at the C</books/list> page). |
d442cc9f |
230 | |
d442cc9f |
231 | |
89d3dae9 |
232 | =head1 CONVERT TO A CHAINED ACTION |
233 | |
234 | Although the example above uses the same C<Local> action type for the |
235 | method that we saw in the previous part of the tutorial, there is an |
236 | alternate approach that allows us to be more specific while also |
237 | paving the way for more advanced capabilities. Change the method |
238 | declaration for C<url_create> in C<lib/MyApp/Controller/Books.pm> you |
239 | entered above to match the following: |
240 | |
241 | sub url_create :Chained('/') :PathPart('books/url_create') :Args(3) { |
242 | |
243 | This converts the method to take advantage of the Chained |
244 | action/dispatch type. Chaining let's you have a single URL |
245 | automatically dispatch to several controller methods, each of which |
246 | can have precise control over the number of arguments that it will |
247 | receive. A chain can essentially be thought of having three parts -- |
248 | a beginning, a middle and an end. The bullets below summarize the key |
249 | points behind each of these parts of a chain: |
250 | |
251 | |
252 | =over 4 |
253 | |
254 | |
255 | =item * |
256 | |
257 | Beginning |
258 | |
259 | =over 4 |
260 | |
261 | =item * |
262 | |
263 | B<Use "C<:Chained('/')>" to start a chain> |
264 | |
265 | =item * |
266 | |
267 | Get arguments through C<CaptureArgs()> |
268 | |
269 | =item * |
270 | |
271 | Specify the path to match with C<PathPart()> |
272 | |
273 | =back |
274 | |
275 | |
276 | =item * |
277 | |
278 | Middle |
279 | |
280 | =over 4 |
281 | |
282 | =item * |
d442cc9f |
283 | |
89d3dae9 |
284 | Link to previous part of the chain with C<:Chained('_name_')> |
285 | |
286 | =item * |
287 | |
288 | Get arguments through C<CaptureArgs()> |
289 | |
290 | =item * |
291 | |
292 | Specify the path to match with C<PathPart()> |
293 | |
294 | =back |
295 | |
296 | |
297 | =item * |
298 | |
299 | End |
300 | |
301 | =over 4 |
302 | |
303 | =item * |
304 | |
305 | Link to previous part of the chain with C<:Chained('_name_')> |
306 | |
307 | =item * |
308 | |
309 | B<Do NOT get arguments through "C<CaptureArgs()>," use "C<Args()>" instead to end a chain> |
310 | |
311 | =item * |
312 | |
313 | Specify the path to match with C<PathPart()> |
314 | |
315 | =back |
316 | |
317 | |
318 | =back |
319 | |
320 | In our C<url_create> method above, we have combined all 3 parts into a |
321 | single method: C<:Chained('/')> to start the chain, |
322 | C<:PathPart('books/url_create')> to specify the base URL to match, |
323 | along with C<:Args(3)> to capture exactly 3 arguments and also end the |
324 | chain. |
325 | |
326 | As we will see shortly, a chain can consist of as many "links" as you |
327 | wish, with each part capturing some arguments and doing some work |
328 | along the way. We will continue to use the Chained action type in this |
329 | part of the tutorial and explore slightly more advanced capabilities |
330 | with the base method and delete feature below. But Chained dispatch |
331 | is capable of far more. For additional information, see |
332 | L<Catalyst::Manual::Intro/Action types>, |
333 | L<Catalyst::DispatchType::Chained|Catalyst::DispatchType::Chained>, |
334 | and the 2006 advent calendar entry on the subject: |
335 | L<http://www.catalystframework.org/calendar/2006/10>. |
336 | |
337 | |
338 | =head2 Try the Chained Action |
339 | |
340 | If you look back at the development server startup logs from your |
341 | initial version of the C<url_create> method (the one using the |
342 | C<:Local> attribute), you will notice that it produced output similar |
343 | to the following: |
344 | |
345 | [debug] Loaded Path actions: |
346 | .-------------------------------------+--------------------------------------. |
347 | | Path | Private | |
348 | +-------------------------------------+--------------------------------------+ |
349 | | / | /default | |
350 | | / | /index | |
351 | | /books | /books/index | |
352 | | /books/list | /books/list | |
353 | | /books/url_create | /books/url_create | |
354 | '-------------------------------------+--------------------------------------' |
355 | |
356 | Now start the development server with our basic chained method in |
357 | place and the startup debug output should change to something along |
358 | the lines of the following: |
359 | |
360 | [debug] Loaded Path actions: |
361 | .-------------------------------------+--------------------------------------. |
362 | | Path | Private | |
363 | +-------------------------------------+--------------------------------------+ |
364 | | / | /default | |
365 | | / | /index | |
366 | | /books | /books/index | |
367 | | /books/list | /books/list | |
368 | '-------------------------------------+--------------------------------------' |
369 | |
370 | [debug] Loaded Chained actions: |
371 | .-------------------------------------+--------------------------------------. |
372 | | Path Spec | Private | |
373 | +-------------------------------------+--------------------------------------+ |
374 | | /books/url_create/*/*/* | /books/url_create | |
375 | '-------------------------------------+--------------------------------------' |
376 | |
377 | C<url_create> has disappeared form the "Loaded Path actions" section |
378 | but it now shows up under the newly created "Loaded Chained actions" |
379 | section. And, the "/*/*/*" portion clearly shows that we have |
380 | specified that 3 arguments are required. |
381 | |
382 | As with our non-chained version of C<url_create>, use your browser to |
383 | enter the following URL: |
384 | |
385 | http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4 |
386 | |
387 | You should see the same "Added book 'TCPIP_Illustrated_Vol-2' by |
388 | 'Stevens' with a rating of 5." along with a dump of the new book model |
389 | object. Click the "Return to list" link, you should find that there |
390 | are now seven books shown (two copies of TCPIP_Illustrated_Vol-2). |
391 | |
392 | |
994b66ad |
393 | =head2 Refactor to Use a "Base" Method to Start the Chains |
89d3dae9 |
394 | |
395 | Let's make a quick update to our initial Chained action to show a |
396 | little more of the power of chaining. First, open |
397 | C<lib/MyApp/Controller/Books.pm> in your editor and add the following |
398 | method: |
399 | |
400 | =head2 base |
401 | |
402 | Can place common logic to start chained dispatch here |
403 | |
404 | =cut |
405 | |
406 | sub base :Chained('/') :PathPart('books') :CaptureArgs(0) { |
407 | my ($self, $c) = @_; |
994b66ad |
408 | |
409 | # Store the resultset in stash so it's available for other methods |
410 | $c->stash->{resultset} = $c->model('DB::Books'); |
89d3dae9 |
411 | |
994b66ad |
412 | # Print a message to the debug log |
89d3dae9 |
413 | $c->log->debug('*** INSIDE BASE METHOD ***'); |
414 | } |
415 | |
994b66ad |
416 | Here we print a log message and store the resultset in |
417 | C<$c-E<gt>stash-E<gt>{resultset}> so that it's automatically available |
418 | for other actions that chain off C<base>. If your controller always |
419 | needs a book ID as it's first argument, you could have the base method |
420 | capture that argument (with C<:CaptureArgs(1)>) and use it to pull the |
421 | book object with that ID from the database and leave it in the stash for |
422 | later parts of your chains to then act upon. Because we have several |
423 | actions that don't need to retrieve a book (such as the C<url_create> |
424 | we are working with now), we will instead add that functionality |
425 | to a common C<object> action shortly. |
426 | |
427 | As for C<url_create>, let's modify it to first dispatch to C<base>. |
428 | Open up C<lib/MyApp/Controller/Books.pm> and edit the declaration for |
429 | C<url_create> to match the following: |
89d3dae9 |
430 | |
431 | sub url_create :Chained('base') :PathPart('url_create') :Args(3) { |
432 | |
433 | Next, let's try out our refactored chain. Restart the development |
434 | server and notice that our "Loaded Chained actions" section has |
435 | changed slightly: |
436 | |
437 | [debug] Loaded Chained actions: |
438 | .-------------------------------------+--------------------------------------. |
439 | | Path Spec | Private | |
440 | +-------------------------------------+--------------------------------------+ |
441 | | /books/url_create/*/*/* | /books/base (0) | |
442 | | | => /books/url_create | |
443 | '-------------------------------------+--------------------------------------' |
444 | |
445 | The "Path Spec" is the same, but now it maps to two Private actions as |
446 | we would expect. |
447 | |
448 | Once again, enter the following URL into your browser: |
449 | |
450 | http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4 |
451 | |
452 | The same "Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a |
453 | rating of 5." and dump of the new book object should appear. Also |
454 | notice the extra debug message in the development server output from |
455 | the C<base> method. Click the "Return to list" link, you should find |
456 | that there are now eight books shown. |
d442cc9f |
457 | |
458 | |
459 | =head1 MANUALLY BUILDING A CREATE FORM |
460 | |
461 | Although the C<url_create> action in the previous step does begin to |
462 | reveal the power and flexibility of both Catalyst and DBIC, it's |
463 | obviously not a very realistic example of how users should be expected |
464 | to enter data. This section begins to address that concern. |
465 | |
466 | |
467 | =head2 Add Method to Display The Form |
468 | |
469 | Edit C<lib/MyApp/Controller/Books.pm> and add the following method: |
470 | |
471 | =head2 form_create |
472 | |
473 | Display form to collect information for book to create |
474 | |
475 | =cut |
476 | |
89d3dae9 |
477 | sub form_create :Chained('base') :PathPart('form_create') :Args(0) { |
d442cc9f |
478 | my ($self, $c) = @_; |
479 | |
480 | # Set the TT template to use |
481 | $c->stash->{template} = 'books/form_create.tt2'; |
482 | } |
483 | |
484 | This action simply invokes a view containing a book creation form. |
485 | |
1390ef0e |
486 | |
d442cc9f |
487 | =head2 Add a Template for the Form |
488 | |
489 | Open C<root/src/books/form_create.tt2> in your editor and enter: |
490 | |
491 | [% META title = 'Manual Form Book Create' -%] |
492 | |
8a7c5151 |
493 | <form method="post" action="[% c.uri_for('form_create_do') %]"> |
d442cc9f |
494 | <table> |
495 | <tr><td>Title:</td><td><input type="text" name="title"></td></tr> |
496 | <tr><td>Rating:</td><td><input type="text" name="rating"></td></tr> |
497 | <tr><td>Author ID:</td><td><input type="text" name="author_id"></td></tr> |
498 | </table> |
499 | <input type="submit" name="Submit" value="Submit"> |
500 | </form> |
501 | |
502 | Note that we have specified the target of the form data as |
503 | C<form_create_do>, the method created in the section that follows. |
504 | |
1390ef0e |
505 | |
d442cc9f |
506 | =head2 Add a Method to Process Form Values and Update Database |
507 | |
508 | Edit C<lib/MyApp/Controller/Books.pm> and add the following method to |
509 | save the form information to the database: |
510 | |
511 | =head2 form_create_do |
512 | |
513 | Take information from form and add to database |
514 | |
515 | =cut |
516 | |
89d3dae9 |
517 | sub form_create_do :Chained('base') :PathPart('form_create_do') :Args(0) { |
d442cc9f |
518 | my ($self, $c) = @_; |
519 | |
520 | # Retrieve the values from the form |
521 | my $title = $c->request->params->{title} || 'N/A'; |
522 | my $rating = $c->request->params->{rating} || 'N/A'; |
523 | my $author_id = $c->request->params->{author_id} || '1'; |
524 | |
525 | # Create the book |
d0496197 |
526 | my $book = $c->model('DB::Books')->create({ |
d442cc9f |
527 | title => $title, |
528 | rating => $rating, |
529 | }); |
530 | # Handle relationship with author |
531 | $book->add_to_book_authors({author_id => $author_id}); |
532 | |
533 | # Store new model object in stash |
534 | $c->stash->{book} = $book; |
535 | |
536 | # Avoid Data::Dumper issue mentioned earlier |
537 | # You can probably omit this |
538 | $Data::Dumper::Useperl = 1; |
539 | |
540 | # Set the TT template to use |
541 | $c->stash->{template} = 'books/create_done.tt2'; |
542 | } |
543 | |
544 | |
545 | =head2 Test Out The Form |
546 | |
547 | If the application is still running from before, use C<Ctrl-C> to kill |
548 | it. Then restart the server: |
549 | |
550 | $ script/myapp_server.pl |
551 | |
89d3dae9 |
552 | Notice that the server startup log reflects the two new chained |
553 | methods that we added: |
554 | |
555 | [debug] Loaded Chained actions: |
556 | .-------------------------------------+--------------------------------------. |
557 | | Path Spec | Private | |
558 | +-------------------------------------+--------------------------------------+ |
559 | | /books/form_create | /books/base (0) | |
560 | | | => /books/form_create | |
561 | | /books/form_create_do | /books/base (0) | |
562 | | | => /books/form_create_do | |
563 | | /books/url_create/*/*/* | /books/base (0) | |
564 | | | => /books/url_create | |
565 | '-------------------------------------+--------------------------------------' |
566 | |
d442cc9f |
567 | Point your browser to L<http://localhost:3000/books/form_create> and |
568 | enter "TCP/IP Illustrated, Vol 3" for the title, a rating of 5, and an |
1390ef0e |
569 | author ID of 4. You should then see the output of the same |
d442cc9f |
570 | C<create_done.tt2> template seen in earlier examples. Finally, click |
571 | "Return to list" to view the full list of books. |
572 | |
573 | B<Note:> Having the user enter the primary key ID for the author is |
574 | obviously crude; we will address this concern with a drop-down list in |
3533daff |
575 | Part 9. |
d442cc9f |
576 | |
577 | |
578 | =head1 A SIMPLE DELETE FEATURE |
579 | |
580 | Turning our attention to the delete portion of CRUD, this section |
581 | illustrates some basic techniques that can be used to remove information |
582 | from the database. |
583 | |
584 | |
585 | =head2 Include a Delete Link in the List |
586 | |
587 | Edit C<root/src/books/list.tt2> and update it to the following (two |
588 | sections have changed: 1) the additional '<th>Links</th>' table header, |
589 | and 2) the four lines for the Delete link near the bottom). |
590 | |
591 | [% # This is a TT comment. The '-' at the end "chomps" the newline. You won't -%] |
592 | [% # see this "chomping" in your browser because HTML ignores blank lines, but -%] |
593 | [% # it WILL eliminate a blank line if you view the HTML source. It's purely -%] |
594 | [%- # optional, but both the beginning and the ending TT tags support chomping. -%] |
595 | |
596 | [% # Provide a title to root/lib/site/header -%] |
597 | [% META title = 'Book List' -%] |
598 | |
599 | <table> |
600 | <tr><th>Title</th><th>Rating</th><th>Author(s)</th><th>Links</th></tr> |
601 | [% # Display each book in a table row %] |
602 | [% FOREACH book IN books -%] |
603 | <tr> |
604 | <td>[% book.title %]</td> |
605 | <td>[% book.rating %]</td> |
606 | <td> |
607 | [% # First initialize a TT variable to hold a list. Then use a TT FOREACH -%] |
608 | [% # loop in 'side effect notation' to load just the last names of the -%] |
609 | [% # authors into the list. Note that the 'push' TT vmethod does not -%] |
610 | [% # a value, so nothing will be printed here. But, if you have something -%] |
611 | [% # in TT that does return a method and you don't want it printed, you -%] |
612 | [% # can: 1) assign it to a bogus value, or 2) use the CALL keyword to -%] |
613 | [% # call it and discard the return value. -%] |
614 | [% tt_authors = [ ]; |
615 | tt_authors.push(author.last_name) FOREACH author = book.authors %] |
616 | [% # Now use a TT 'virtual method' to display the author count in parens -%] |
617 | ([% tt_authors.size %]) |
618 | [% # Use another TT vmethod to join & print the names & comma separators -%] |
619 | [% tt_authors.join(', ') %] |
620 | </td> |
621 | <td> |
622 | [% # Add a link to delete a book %] |
e075db0c |
623 | <a href="[% c.uri_for(c.controller.action_for('delete'), [book.id]) %]">Delete</a> |
d442cc9f |
624 | </td> |
625 | </tr> |
626 | [% END -%] |
627 | </table> |
628 | |
fe01b24f |
629 | The additional code is obviously designed to add a new column to the |
630 | right side of the table with a C<Delete> "button" (for simplicity, |
631 | links will be used instead of full HTML buttons). |
632 | |
b2ad8bbd |
633 | Also notice that we are using a more advanced form of C<uri_for> than |
634 | we have seen before. Here we use C<$c-E<gt>controller-E<gt>action_for> |
635 | to automatically generate a URI appropriate for that action while |
636 | inserting the C<book.id> value into the appropriate place. Now, if |
637 | you ever change C<:PathPart('delete')> in your controller method to |
638 | C<:PathPart('kill')>, then your links will automatically update without |
639 | any changes to your .tt2 template file. |
640 | |
fe01b24f |
641 | B<Note:> You should use more than just a simple link with your |
642 | applications. Consider using some sort of of confirmation page |
643 | (typically with unique actions in your controller for both the |
644 | confirmation and the actual delete operation). Also, you should try |
645 | to use an HTTP POST operation (versus the GET used here) for |
646 | operations that change the state of your application (e.g., the |
647 | database). |
d442cc9f |
648 | |
1390ef0e |
649 | |
994b66ad |
650 | =head2 Add a Common Method to Retrieve a Book for the Chain |
651 | |
652 | As mentioned earlier, since we have a mixture of actions that operate on |
653 | a single book ID and others that do no, we should not have C<base> |
654 | capture the book ID, find the corresponding book in the database and |
655 | save it in the stash for later links in the chain. However, just |
656 | because that logic does not belong in C<base> doesn't mean that we can't |
657 | create another location to centralize that logic. In our case, we will |
658 | create a method called C<object> that will store the specific book in |
659 | the stash. Chains that always operate on a single existing book can |
660 | chain off this method, but methods such as C<url_create> that don't |
661 | operate on an existing book can chain directly off base. |
662 | |
663 | To add the C<object> method, edit C<lib/MyApp/Controller/Books.pm> |
664 | and add the following code: |
665 | |
e075db0c |
666 | =head2 object |
667 | |
668 | Fetch the specified book object based on the book ID and store |
669 | it in the stash |
670 | |
671 | =cut |
672 | |
994b66ad |
673 | sub object :Chained('base') :PathPart('id') :CaptureArgs(1) { |
674 | my ($self, $c, $id) = @_; |
675 | |
676 | # Find the book object and store it in the stash |
677 | $c->stash(object => $c->stash->{resultset}->find($id)); |
678 | |
679 | # Make sure the lookup was successful. You would probably |
680 | # want to do something like this in a real app: |
681 | # $c->detach('/error_404') if !$c->stash->{object}; |
682 | die "Book $id not found!" if !$c->stash->{object}; |
683 | } |
684 | |
685 | Now, any other method that chains off C<object> will automatically |
686 | have the appropriate book waiting for it in |
687 | C<$c-E<gt>stash-Egt>{object}>. |
688 | |
689 | Also note that we are using different technique for setting |
690 | C<$c-E<gt>stash>. The advantage of this style is that it let's you |
691 | set multiple stash variables at a time. For example: |
692 | |
693 | $c->stash(object => $c->stash->{resultset}->find($id), |
694 | another_thing => 1); |
695 | |
696 | or as a hashref: |
697 | |
698 | $c->stash({object => $c->stash->{resultset}->find($id), |
699 | another_thing => 1}); |
700 | |
701 | Either format works, but the C<$c-E<gt>stash(name => value);> |
702 | style is growing in popularity -- you may which to use it all |
703 | the time (even when you are only setting a single value). |
704 | |
705 | |
d442cc9f |
706 | =head2 Add a Delete Action to the Controller |
707 | |
708 | Open C<lib/MyApp/Controller/Books.pm> in your editor and add the |
709 | following method: |
710 | |
1390ef0e |
711 | =head2 delete |
d442cc9f |
712 | |
713 | Delete a book |
714 | |
715 | =cut |
716 | |
994b66ad |
717 | sub delete :Chained('object') :PathPart('delete') :Args(0) { |
d442cc9f |
718 | # $id = primary key of book to delete |
994b66ad |
719 | my ($self, $c) = @_; |
d442cc9f |
720 | |
994b66ad |
721 | # Use the book object saved by 'object' and delete it along |
722 | # with related 'book_authors' entries |
723 | $c->stash->{object}->delete; |
d442cc9f |
724 | |
725 | # Set a status message to be displayed at the top of the view |
726 | $c->stash->{status_msg} = "Book deleted."; |
727 | |
728 | # Forward to the list action/method in this controller |
729 | $c->forward('list'); |
730 | } |
731 | |
994b66ad |
732 | This method first deletes the book object saved by the C<object> method. |
733 | However, it also removes the corresponding entry from the |
734 | C<book_authors> table. Note that C<delete> will cascade to also delete |
735 | the related join table entries in C<book_authors>. |
d442cc9f |
736 | |
737 | Then, rather than forwarding to a "delete done" page as we did with the |
738 | earlier create example, it simply sets the C<status_msg> to display a |
739 | notification to the user as the normal list view is rendered. |
740 | |
741 | The C<delete> action uses the context C<forward> method to return the |
742 | user to the book list. The C<detach> method could have also been used. |
743 | Whereas C<forward> I<returns> to the original action once it is |
744 | completed, C<detach> does I<not> return. Other than that, the two are |
745 | equivalent. |
746 | |
747 | |
748 | =head2 Try the Delete Feature |
749 | |
750 | If the application is still running from before, use C<Ctrl-C> to kill |
751 | it. Then restart the server: |
752 | |
994b66ad |
753 | $ DBIC_TRACE=1 script/myapp_server.pl |
d442cc9f |
754 | |
89d3dae9 |
755 | The C<delete> method now appears in the "Loaded Chained actions" section |
756 | of the startup debug output: |
757 | |
758 | [debug] Loaded Chained actions: |
994b66ad |
759 | .-------------------------------------+--------------------------------------. |
760 | | Path Spec | Private | |
761 | +-------------------------------------+--------------------------------------+ |
762 | | /books/id/*/delete | /books/base (0) | |
763 | | | -> /books/object (1) | |
764 | | | => /books/delete | |
765 | | /books/form_create | /books/base (0) | |
766 | | | => /books/form_create | |
767 | | /books/form_create_do | /books/base (0) | |
768 | | | => /books/form_create_do | |
769 | | /books/url_create/*/*/* | /books/base (0) | |
770 | | | => /books/url_create | |
771 | '-------------------------------------+--------------------------------------' |
89d3dae9 |
772 | |
d442cc9f |
773 | Then point your browser to L<http://localhost:3000/books/list> and click |
774 | the "Delete" link next to the first "TCPIP_Illustrated_Vol-2". A green |
775 | "Book deleted" status message should display at the top of the page, |
994b66ad |
776 | along with a list of the eight remaining books. You will also see the |
777 | cascading delete operation via the DBIC_TRACE output: |
778 | |
779 | DELETE FROM books WHERE ( id = ? ): '6' |
780 | SELECT me.book_id, me.author_id FROM book_authors me WHERE ( me.book_id = ? ): '6' |
781 | DELETE FROM book_authors WHERE ( author_id = ? AND book_id = ? ): '4', '6' |
d442cc9f |
782 | |
783 | |
784 | =head2 Fixing a Dangerous URL |
785 | |
5edc2aae |
786 | Note the URL in your browser once you have performed the deletion in the |
d442cc9f |
787 | prior step -- it is still referencing the delete action: |
788 | |
789 | http://localhost:3000/books/delete/6 |
790 | |
791 | What if the user were to press reload with this URL still active? In |
792 | this case the redundant delete is harmless, but in other cases this |
793 | could clearly be extremely dangerous. |
794 | |
795 | We can improve the logic by converting to a redirect. Unlike |
796 | C<$c-E<gt>forward('list'))> or C<$c-E<gt>detach('list'))> that perform |
797 | a server-side alteration in the flow of processing, a redirect is a |
3533daff |
798 | client-side mechanism that causes the browser to issue an entirely |
d442cc9f |
799 | new request. As a result, the URL in the browser is updated to match |
800 | the destination of the redirection URL. |
801 | |
802 | To convert the forward used in the previous section to a redirect, |
803 | open C<lib/MyApp/Controller/Books.pm> and edit the existing |
804 | C<sub delete> method to match: |
805 | |
994b66ad |
806 | =head2 delete |
d442cc9f |
807 | |
808 | Delete a book |
994b66ad |
809 | |
d442cc9f |
810 | =cut |
811 | |
994b66ad |
812 | sub delete :Chained('object') :PathPart('delete') :Args(0) { |
d442cc9f |
813 | # $id = primary key of book to delete |
814 | my ($self, $c, $id) = @_; |
815 | |
994b66ad |
816 | # Use the book object saved by 'object' and delete it along |
817 | # with related 'book_authors' entries |
818 | $c->stash->{object}->delete; |
d442cc9f |
819 | |
820 | # Set a status message to be displayed at the top of the view |
821 | $c->stash->{status_msg} = "Book deleted."; |
822 | |
823 | # Redirect the user back to the list page |
e075db0c |
824 | $c->response->redirect($c->uri_for($self->action_for('list')); |
d442cc9f |
825 | } |
826 | |
827 | |
828 | =head2 Try the Delete and Redirect Logic |
829 | |
830 | Restart the development server and point your browser to |
994b66ad |
831 | L<http://localhost:3000/books/list> (don't just hit "Refresh" in your |
832 | browser since we left the URL in an invalid state in the previous |
833 | section!) and delete the first copy of the remaining two |
834 | "TCPIP_Illustrated_Vol-2" books. The URL in your browser should return |
835 | to the L<http://localhost:3000/books/list> URL, so that is an |
836 | improvement, but notice that I<no green "Book deleted" status message is |
837 | displayed>. Because the stash is reset on every request (and a redirect |
838 | involves a second request), the C<status_msg> is cleared before it can |
839 | be displayed. |
d442cc9f |
840 | |
841 | |
842 | =head2 Using C<uri_for> to Pass Query Parameters |
843 | |
89d3dae9 |
844 | There are several ways to pass information across a redirect. One |
845 | option is to use the C<flash> technique that we will see in Part 5 of |
846 | the tutorial; however, here we will pass the information via query |
847 | parameters on the redirect itself. Open |
848 | C<lib/MyApp/Controller/Books.pm> and update the existing C<sub delete> |
849 | method to match the following: |
d442cc9f |
850 | |
851 | =head2 delete |
852 | |
853 | Delete a book |
854 | |
855 | =cut |
856 | |
994b66ad |
857 | sub delete :Chained('object') :PathPart('delete') :Args(0) { |
d442cc9f |
858 | # $id = primary key of book to delete |
859 | my ($self, $c, $id) = @_; |
860 | |
994b66ad |
861 | # Use the book object saved by 'object' and delete it along |
862 | # with related 'book_authors' entries |
863 | $c->stash->{object}->delete; |
d442cc9f |
864 | |
865 | # Redirect the user back to the list page with status msg as an arg |
e075db0c |
866 | $c->response->redirect($c->uri_for($self->action_for('list'), |
d442cc9f |
867 | {status_msg => "Book deleted."})); |
868 | } |
869 | |
870 | This modification simply leverages the ability of C<uri_for> to include |
871 | an arbitrary number of name/value pairs in a hash reference. Next, we |
89d3dae9 |
872 | need to update C<root/src/wrapper.tt2> to handle C<status_msg> as a |
d442cc9f |
873 | query parameter: |
874 | |
1390ef0e |
875 | ... |
d442cc9f |
876 | <div id="content"> |
1390ef0e |
877 | [%# Status and error messages %] |
878 | <span class="message">[% status_msg || c.request.params.status_msg %]</span> |
879 | <span class="error">[% error_msg %]</span> |
880 | [%# This is where TT will stick all of your template's contents. -%] |
881 | [% content %] |
882 | </div><!-- end content --> |
883 | ... |
884 | |
885 | Although the sample above only shows the C<content> div, leave the |
886 | rest of the file intact -- the only change we made to the C<wrapper.tt2> |
887 | was to add "C<|| c.request.params.status_msg>" to the |
888 | C<E<lt>span class="message"E<gt>> line. |
d442cc9f |
889 | |
890 | |
891 | =head2 Try the Delete and Redirect With Query Param Logic |
892 | |
893 | Restart the development server and point your browser to |
994b66ad |
894 | L<http://localhost:3000/books/list> (you should now be able to safely |
895 | hit "refresh" in your browser). Then delete the remaining copy of |
896 | "TCPIP_Illustrated_Vol-2". The green "Book deleted" status message |
d442cc9f |
897 | should return. |
898 | |
89d3dae9 |
899 | B<NOTE:> Another popular method for maintaining server-side |
900 | information across a redirect is to use the C<flash> technique we |
901 | discuss in the next part of the tutorial, |
902 | L<Authentication|Catalyst::Manual::Tutorial::Authentication>. While |
903 | C<flash> is a "slicker" mechanism in that it's all handled by the |
904 | server and doesn't "pollute" your URLs, B<it is important to note that |
905 | C<flash> can lead to situations where the wrong information shows up |
906 | in the wrong browser window if the user has multiple windows or |
994b66ad |
907 | browser tabs open.> For example, Window A causes something to be |
89d3dae9 |
908 | placed in the stash, but before that window performs a redirect, |
909 | Window B makes a request to the server and gets the status information |
994b66ad |
910 | that should really go to Window A. For this reason, you may wish |
89d3dae9 |
911 | to use the "query param" technique shown here in your applications. |
d442cc9f |
912 | |
913 | |
914 | =head1 AUTHOR |
915 | |
916 | Kennedy Clark, C<hkclark@gmail.com> |
917 | |
918 | Please report any errors, issues or suggestions to the author. The |
919 | most recent version of the Catalyst Tutorial can be found at |
82ab4bbf |
920 | L<http://dev.catalyst.perl.org/repos/Catalyst/Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/>. |
d442cc9f |
921 | |
45c7830f |
922 | Copyright 2006-2008, Kennedy Clark, under Creative Commons License |
95674086 |
923 | (L<http://creativecommons.org/licenses/by-sa/3.0/us/>). |