Commit | Line | Data |
fbbb9084 |
1 | =head1 NAME |
d442cc9f |
2 | |
3ab6187c |
3 | Catalyst::Manual::Tutorial::04_BasicCRUD - Catalyst Tutorial - Chapter 4: Basic CRUD |
d442cc9f |
4 | |
5 | |
6 | =head1 OVERVIEW |
7 | |
4b4d3884 |
8 | This is B<Chapter 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 | |
3ab6187c |
16 | L<Introduction|Catalyst::Manual::Tutorial::01_Intro> |
d442cc9f |
17 | |
18 | =item 2 |
19 | |
3ab6187c |
20 | L<Catalyst Basics|Catalyst::Manual::Tutorial::02_CatalystBasics> |
d442cc9f |
21 | |
22 | =item 3 |
23 | |
3ab6187c |
24 | L<More Catalyst Basics|Catalyst::Manual::Tutorial::03_MoreCatalystBasics> |
d442cc9f |
25 | |
26 | =item 4 |
27 | |
3ab6187c |
28 | B<04_Basic CRUD> |
d442cc9f |
29 | |
30 | =item 5 |
31 | |
3ab6187c |
32 | L<Authentication|Catalyst::Manual::Tutorial::05_Authentication> |
d442cc9f |
33 | |
34 | =item 6 |
35 | |
3ab6187c |
36 | L<Authorization|Catalyst::Manual::Tutorial::06_Authorization> |
d442cc9f |
37 | |
38 | =item 7 |
39 | |
3ab6187c |
40 | L<Debugging|Catalyst::Manual::Tutorial::07_Debugging> |
d442cc9f |
41 | |
42 | =item 8 |
43 | |
3ab6187c |
44 | L<Testing|Catalyst::Manual::Tutorial::08_Testing> |
d442cc9f |
45 | |
46 | =item 9 |
47 | |
3ab6187c |
48 | L<Advanced CRUD|Catalyst::Manual::Tutorial::09_AdvancedCRUD> |
3533daff |
49 | |
50 | =item 10 |
51 | |
3ab6187c |
52 | L<Appendices|Catalyst::Manual::Tutorial::10_Appendices> |
d442cc9f |
53 | |
54 | =back |
55 | |
56 | |
d442cc9f |
57 | =head1 DESCRIPTION |
58 | |
ee53cc71 |
59 | This chapter of the tutorial builds on the fairly primitive application |
22fe0f18 |
60 | created in |
61 | L<Chapter 3|Catalyst::Manual::Tutorial::03_MoreCatalystBasics> to add |
62 | basic support for Create, Read, Update, and Delete (CRUD) of C<Book> |
a9a6fb3f |
63 | objects. Note that the 'list' function in |
64 | L<Chapter 3|Catalyst::Manual::Tutorial::03_MoreCatalystBasics> already |
65 | implements the Read portion of CRUD (although Read normally refers to |
66 | reading a single object; you could implement full Read functionality |
67 | using the techniques introduced below). This section will focus on the |
68 | Create and Delete aspects of CRUD. More advanced capabilities, |
69 | including full Update functionality, will be addressed in |
22fe0f18 |
70 | L<Chapter 9|Catalyst::Manual::Tutorial::09_AdvancedCRUD>. |
ee53cc71 |
71 | |
72 | Although this chapter of the tutorial will show you how to build CRUD |
73 | functionality yourself, another option is to use a "CRUD builder" type |
74 | of tool to automate the process. You get less control, but it can be |
75 | quick and easy. For example, see L<Catalyst::Plugin::AutoCRUD>, |
76 | L<CatalystX::CRUD>, and L<CatalystX::CRUD::YUI>. |
1390ef0e |
77 | |
477a6d5b |
78 | Source code for the tutorial in included in the F</home/catalyst/Final> |
79 | directory of the Tutorial Virtual machine (one subdirectory per |
80 | chapter). There are also instructions for downloading the code in |
2217b252 |
81 | L<Catalyst::Manual::Tutorial::01_Intro>. |
d442cc9f |
82 | |
3533daff |
83 | |
d442cc9f |
84 | =head1 FORMLESS SUBMISSION |
85 | |
ee53cc71 |
86 | Our initial attempt at object creation will utilize the "URL arguments" |
22fe0f18 |
87 | feature of Catalyst (we will employ the more common form-based |
ee53cc71 |
88 | submission in the sections that follow). |
d442cc9f |
89 | |
90 | |
91 | =head2 Include a Create Action in the Books Controller |
92 | |
f4e9de4a |
93 | Edit F<lib/MyApp/Controller/Books.pm> and enter the following method: |
d442cc9f |
94 | |
95 | =head2 url_create |
7ce05098 |
96 | |
d442cc9f |
97 | Create a book with the supplied title, rating, and author |
7ce05098 |
98 | |
d442cc9f |
99 | =cut |
7ce05098 |
100 | |
f2bbfc36 |
101 | sub url_create :Local { |
55490817 |
102 | # In addition to self & context, get the title, rating, & |
103 | # author_id args from the URL. Note that Catalyst automatically |
104 | # puts extra information after the "/<controller_name>/<action_name/" |
fce83e5f |
105 | # into @_. The args are separated by the '/' char on the URL. |
d442cc9f |
106 | my ($self, $c, $title, $rating, $author_id) = @_; |
7ce05098 |
107 | |
55490817 |
108 | # Call create() on the book model object. Pass the table |
d442cc9f |
109 | # columns/field values we want to set as hash values |
3b1fa91b |
110 | my $book = $c->model('DB::Book')->create({ |
d442cc9f |
111 | title => $title, |
112 | rating => $rating |
113 | }); |
7ce05098 |
114 | |
55490817 |
115 | # Add a record to the join table for this book, mapping to |
d442cc9f |
116 | # appropriate author |
fce83e5f |
117 | $book->add_to_book_authors({author_id => $author_id}); |
d442cc9f |
118 | # Note: Above is a shortcut for this: |
fce83e5f |
119 | # $book->create_related('book_authors', {author_id => $author_id}); |
7ce05098 |
120 | |
0ed3df53 |
121 | # Assign the Book object to the stash for display and set template |
122 | $c->stash(book => $book, |
123 | template => 'books/create_done.tt2'); |
7ce05098 |
124 | |
22fe0f18 |
125 | # Disable caching for this page |
126 | $c->response->header('Cache-Control' => 'no-cache'); |
d442cc9f |
127 | } |
128 | |
129 | Notice that Catalyst takes "extra slash-separated information" from the |
22fe0f18 |
130 | URL and passes it as arguments in C<@_> (as long as the number of |
131 | arguments is not "fixed" using an attribute like C<:Args(0)>). The |
132 | C<url_create> action then uses a simple call to the DBIC C<create> |
133 | method to add the requested information to the database (with a separate |
134 | call to C<add_to_book_authors> to update the join table). As do |
135 | virtually all controller methods (at least the ones that directly handle |
136 | user input), it then sets the template that should handle this request. |
137 | |
138 | Also note that we are explicitly setting a C<no-cache> "Cache-Control" |
139 | header to force browsers using the page to get a fresh copy every time. |
140 | You could even move this to a C<auto> method in |
f4e9de4a |
141 | F<lib/MyApp/Controller/Root.pm> and it would automatically get applied |
22fe0f18 |
142 | to every page in the whole application via a single line of code |
143 | (remember from Chapter 3, that every C<auto> method gets run in the |
144 | Controller hierarchy). |
d442cc9f |
145 | |
146 | |
8a472b34 |
147 | =head2 Include a Template for the 'url_create' Action: |
d442cc9f |
148 | |
f4e9de4a |
149 | Edit F<root/src/books/create_done.tt2> and then enter: |
d442cc9f |
150 | |
151 | [% # Use the TT Dumper plugin to Data::Dumper variables to the browser -%] |
152 | [% # Not a good idea for production use, though. :-) 'Indent=1' is -%] |
153 | [% # optional, but prevents "massive indenting" of deeply nested objects -%] |
154 | [% USE Dumper(Indent=1) -%] |
7ce05098 |
155 | |
d442cc9f |
156 | [% # Set the page title. META can 'go back' and set values in templates -%] |
22fe0f18 |
157 | [% # that have been processed 'before' this template (here it's updating -%] |
158 | [% # the title in the root/src/wrapper.tt2 wrapper template). Note that -%] |
159 | [% # META only works on simple/static strings (i.e. there is no variable -%] |
160 | [% # interpolation -- if you need dynamic/interpolated content in your -%] |
161 | [% # title, set "$c->stash(title => $something)" in the controller). -%] |
d442cc9f |
162 | [% META title = 'Book Created' %] |
7ce05098 |
163 | |
fce83e5f |
164 | [% # Output information about the record that was added. First title. -%] |
d442cc9f |
165 | <p>Added book '[% book.title %]' |
7ce05098 |
166 | |
22fe0f18 |
167 | [% # Then, output the last name of the first author -%] |
fce83e5f |
168 | by '[% book.authors.first.last_name %]' |
7ce05098 |
169 | |
22fe0f18 |
170 | [% # Then, output the rating for the book that was added -%] |
d442cc9f |
171 | with a rating of [% book.rating %].</p> |
7ce05098 |
172 | |
22fe0f18 |
173 | [% # Provide a link back to the list page. 'c.uri_for' builds -%] |
174 | [% # 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> |
7ce05098 |
176 | |
d442cc9f |
177 | [% # Try out the TT Dumper (for development only!) -%] |
178 | <pre> |
179 | Dump of the 'book' variable: |
180 | [% Dumper.dump(book) %] |
181 | </pre> |
182 | |
ee53cc71 |
183 | The TT C<USE> directive allows access to a variety of plugin modules (TT |
184 | plugins, that is, not Catalyst plugins) to add extra functionality to |
185 | the base TT capabilities. Here, the plugin allows L<Data::Dumper> |
186 | "pretty printing" of objects and variables. Other than that, the rest |
187 | of the code should be familiar from the examples in Chapter 3. |
d442cc9f |
188 | |
d442cc9f |
189 | |
8a472b34 |
190 | =head2 Try the 'url_create' Feature |
d442cc9f |
191 | |
f2bbfc36 |
192 | Make sure the development server is running with the "-r" restart |
193 | option: |
d442cc9f |
194 | |
f2bbfc36 |
195 | $ DBIC_TRACE=1 script/myapp_server.pl -r |
d442cc9f |
196 | |
197 | Note that new path for C</books/url_create> appears in the startup debug |
198 | output. |
199 | |
d442cc9f |
200 | Next, use your browser to enter the following URL: |
201 | |
202 | http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4 |
203 | |
55490817 |
204 | Your browser should display "Added book 'TCPIP_Illustrated_Vol-2' by |
205 | 'Stevens' with a rating of 5." along with a dump of the new book model |
206 | object as it was returned by DBIC. You should also see the following |
ee53cc71 |
207 | DBIC debug messages displayed in the development server log messages if |
208 | you have DBIC_TRACE set: |
d442cc9f |
209 | |
3b1fa91b |
210 | INSERT INTO book (rating, title) VALUES (?, ?): `5', `TCPIP_Illustrated_Vol-2' |
211 | INSERT INTO book_author (author_id, book_id) VALUES (?, ?): `4', `6' |
d442cc9f |
212 | |
213 | The C<INSERT> statements are obviously adding the book and linking it to |
ee53cc71 |
214 | the existing record for Richard Stevens. The C<SELECT> statement |
215 | results from DBIC automatically fetching the book for the |
216 | C<Dumper.dump(book)>. |
d442cc9f |
217 | |
ee53cc71 |
218 | If you then click the "Return to list" link, you should find that there |
219 | are now six books shown (if necessary, Shift+Reload or Ctrl+Reload your |
220 | browser at the C</books/list> page). You should now see the six DBIC |
221 | debug messages similar to the following (where N=1-6): |
3b1fa91b |
222 | |
7ce05098 |
223 | SELECT author.id, author.first_name, author.last_name |
224 | FROM book_author me JOIN author author |
fce83e5f |
225 | ON author.id = me.author_id WHERE ( me.book_id = ? ): 'N' |
226 | |
d442cc9f |
227 | |
89d3dae9 |
228 | =head1 CONVERT TO A CHAINED ACTION |
229 | |
55490817 |
230 | Although the example above uses the same C<Local> action type for the |
4b4d3884 |
231 | method that we saw in the previous chapter of the tutorial, there is an |
ee53cc71 |
232 | alternate approach that allows us to be more specific while also paving |
233 | the way for more advanced capabilities. Change the method declaration |
f4e9de4a |
234 | for C<url_create> in F<lib/MyApp/Controller/Books.pm> you entered above |
ee53cc71 |
235 | to match the following: |
89d3dae9 |
236 | |
237 | sub url_create :Chained('/') :PathPart('books/url_create') :Args(3) { |
fce83e5f |
238 | # In addition to self & context, get the title, rating, & |
239 | # author_id args from the URL. Note that Catalyst automatically |
7ce05098 |
240 | # puts the first 3 arguments worth of extra information after the |
fce83e5f |
241 | # "/<controller_name>/<action_name/" into @_ because we specified |
242 | # "Args(3)". The args are separated by the '/' char on the URL. |
243 | my ($self, $c, $title, $rating, $author_id) = @_; |
7ce05098 |
244 | |
fce83e5f |
245 | ... |
89d3dae9 |
246 | |
55490817 |
247 | This converts the method to take advantage of the Chained |
ee53cc71 |
248 | action/dispatch type. Chaining lets you have a single URL automatically |
249 | dispatch to several controller methods, each of which can have precise |
250 | control over the number of arguments that it will receive. A chain can |
251 | essentially be thought of having three parts -- a beginning, a middle, |
252 | and an end. The bullets below summarize the key points behind each of |
253 | these parts of a chain: |
89d3dae9 |
254 | |
255 | |
256 | =over 4 |
257 | |
258 | |
259 | =item * |
260 | |
261 | Beginning |
262 | |
263 | =over 4 |
264 | |
265 | =item * |
266 | |
267 | B<Use "C<:Chained('/')>" to start a chain> |
268 | |
269 | =item * |
270 | |
271 | Get arguments through C<CaptureArgs()> |
272 | |
273 | =item * |
274 | |
275 | Specify the path to match with C<PathPart()> |
276 | |
277 | =back |
278 | |
279 | |
280 | =item * |
281 | |
282 | Middle |
283 | |
284 | =over 4 |
285 | |
286 | =item * |
d442cc9f |
287 | |
89d3dae9 |
288 | Link to previous part of the chain with C<:Chained('_name_')> |
289 | |
290 | =item * |
291 | |
292 | Get arguments through C<CaptureArgs()> |
293 | |
294 | =item * |
295 | |
296 | Specify the path to match with C<PathPart()> |
297 | |
298 | =back |
299 | |
300 | |
301 | =item * |
302 | |
303 | End |
304 | |
305 | =over 4 |
306 | |
307 | =item * |
308 | |
309 | Link to previous part of the chain with C<:Chained('_name_')> |
310 | |
311 | =item * |
312 | |
313 | B<Do NOT get arguments through "C<CaptureArgs()>," use "C<Args()>" instead to end a chain> |
314 | |
315 | =item * |
316 | |
317 | Specify the path to match with C<PathPart()> |
318 | |
319 | =back |
320 | |
321 | |
322 | =back |
323 | |
72609296 |
324 | In our C<url_create> method above, we have combined all three parts into |
325 | a single method: C<:Chained('/')> to start the chain, |
326 | C<:PathPart('books/url_create')> to specify the base URL to match, and |
327 | C<:Args(3)> to capture exactly three arguments and to end the chain. |
89d3dae9 |
328 | |
55490817 |
329 | As we will see shortly, a chain can consist of as many "links" as you |
ee53cc71 |
330 | wish, with each part capturing some arguments and doing some work along |
331 | the way. We will continue to use the Chained action type in this |
4b4d3884 |
332 | chapter of the tutorial and explore slightly more advanced capabilities |
ee53cc71 |
333 | with the base method and delete feature below. But Chained dispatch is |
334 | capable of far more. For additional information, see |
55490817 |
335 | L<Catalyst::Manual::Intro/Action types>, |
ee53cc71 |
336 | L<Catalyst::DispatchType::Chained>, and the 2006 Advent calendar entry |
337 | on the subject: L<http://www.catalystframework.org/calendar/2006/10>. |
89d3dae9 |
338 | |
339 | |
340 | =head2 Try the Chained Action |
341 | |
55490817 |
342 | If you look back at the development server startup logs from your |
ee53cc71 |
343 | initial version of the C<url_create> method (the one using the C<:Local> |
344 | attribute), you will notice that it produced output similar to the |
345 | following: |
89d3dae9 |
346 | |
fbbb9084 |
347 | [debug] Loaded Path actions: |
348 | .-------------------------------------+--------------------------------------. |
349 | | Path | Private | |
350 | +-------------------------------------+--------------------------------------+ |
351 | | / | /default | |
352 | | / | /index | |
353 | | /books | /books/index | |
354 | | /books/list | /books/list | |
355 | | /books/url_create | /books/url_create | |
356 | '-------------------------------------+--------------------------------------' |
89d3dae9 |
357 | |
22fe0f18 |
358 | When the development server restarts after our conversion to Chained |
359 | dispatch, the debug output should change to something along the lines of |
360 | the following: |
89d3dae9 |
361 | |
fbbb9084 |
362 | [debug] Loaded Path actions: |
363 | .-------------------------------------+--------------------------------------. |
364 | | Path | Private | |
365 | +-------------------------------------+--------------------------------------+ |
366 | | / | /default | |
367 | | / | /index | |
368 | | /books | /books/index | |
369 | | /books/list | /books/list | |
370 | '-------------------------------------+--------------------------------------' |
7ce05098 |
371 | |
fbbb9084 |
372 | [debug] Loaded Chained actions: |
373 | .-------------------------------------+--------------------------------------. |
374 | | Path Spec | Private | |
375 | +-------------------------------------+--------------------------------------+ |
376 | | /books/url_create/*/*/* | /books/url_create | |
377 | '-------------------------------------+--------------------------------------' |
89d3dae9 |
378 | |
ee53cc71 |
379 | C<url_create> has disappeared from the "Loaded Path actions" section but |
380 | it now shows up under the newly created "Loaded Chained actions" |
72609296 |
381 | section. And the "/*/*/*" portion clearly shows our requirement for |
fbbb9084 |
382 | three arguments. |
89d3dae9 |
383 | |
55490817 |
384 | As with our non-chained version of C<url_create>, use your browser to |
89d3dae9 |
385 | enter the following URL: |
386 | |
fbbb9084 |
387 | http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4 |
89d3dae9 |
388 | |
55490817 |
389 | You should see the same "Added book 'TCPIP_Illustrated_Vol-2' by |
390 | 'Stevens' with a rating of 5." along with a dump of the new book model |
72609296 |
391 | object. Click the "Return to list" link, and you should find that there |
392 | are now seven books shown (two copies of I<TCPIP_Illustrated_Vol-2>). |
89d3dae9 |
393 | |
394 | |
8a472b34 |
395 | =head2 Refactor to Use a 'base' Method to Start the Chains |
89d3dae9 |
396 | |
ee53cc71 |
397 | Let's make a quick update to our initial Chained action to show a little |
398 | more of the power of chaining. First, open |
f4e9de4a |
399 | F<lib/MyApp/Controller/Books.pm> in your editor and add the following |
89d3dae9 |
400 | method: |
401 | |
fbbb9084 |
402 | =head2 base |
7ce05098 |
403 | |
fbbb9084 |
404 | Can place common logic to start chained dispatch here |
7ce05098 |
405 | |
fbbb9084 |
406 | =cut |
7ce05098 |
407 | |
fbbb9084 |
408 | sub base :Chained('/') :PathPart('books') :CaptureArgs(0) { |
409 | my ($self, $c) = @_; |
7ce05098 |
410 | |
1cde0fd6 |
411 | # Store the ResultSet in stash so it's available for other methods |
0ed3df53 |
412 | $c->stash(resultset => $c->model('DB::Book')); |
7ce05098 |
413 | |
fbbb9084 |
414 | # Print a message to the debug log |
415 | $c->log->debug('*** INSIDE BASE METHOD ***'); |
416 | } |
417 | |
55490817 |
418 | Here we print a log message and store the DBIC ResultSet in |
429d1caf |
419 | C<< $c->stash->{resultset} >> so that it's automatically available |
55490817 |
420 | for other actions that chain off C<base>. If your controller always |
72609296 |
421 | needs a book ID as its first argument, you could have the base method |
55490817 |
422 | capture that argument (with C<:CaptureArgs(1)>) and use it to pull the |
429d1caf |
423 | book object with C<< ->find($id) >> and leave it in the stash for later |
ee53cc71 |
424 | parts of your chains to then act upon. Because we have several actions |
425 | that don't need to retrieve a book (such as the C<url_create> we are |
426 | working with now), we will instead add that functionality to a common |
427 | C<object> action shortly. |
994b66ad |
428 | |
55490817 |
429 | As for C<url_create>, let's modify it to first dispatch to C<base>. |
f4e9de4a |
430 | Open up F<lib/MyApp/Controller/Books.pm> and edit the declaration for |
994b66ad |
431 | C<url_create> to match the following: |
89d3dae9 |
432 | |
433 | sub url_create :Chained('base') :PathPart('url_create') :Args(3) { |
434 | |
f4e9de4a |
435 | Once you save F<lib/MyApp/Controller/Books.pm>, notice that the |
ee53cc71 |
436 | development server will restart and our "Loaded Chained actions" section |
f2bbfc36 |
437 | will changed slightly: |
55490817 |
438 | |
fbbb9084 |
439 | [debug] Loaded Chained actions: |
440 | .-------------------------------------+--------------------------------------. |
441 | | Path Spec | Private | |
442 | +-------------------------------------+--------------------------------------+ |
443 | | /books/url_create/*/*/* | /books/base (0) | |
444 | | | => /books/url_create | |
445 | '-------------------------------------+--------------------------------------' |
89d3dae9 |
446 | |
ee53cc71 |
447 | The "Path Spec" is the same, but now it maps to two Private actions as |
448 | we would expect. The C<base> method is being triggered by the C</books> |
449 | part of the URL. However, the processing then continues to the |
450 | C<url_create> method because this method "chained" off C<base> and |
451 | specified C<:PathPart('url_create')> (note that we could have omitted |
452 | the "PathPart" here because it matches the name of the method, but we |
444d6b27 |
453 | will include it to make the logic as explicit as possible). |
89d3dae9 |
454 | |
455 | Once again, enter the following URL into your browser: |
456 | |
fbbb9084 |
457 | http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4 |
89d3dae9 |
458 | |
ee53cc71 |
459 | The same "Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a |
460 | rating of 5." message and a dump of the new book object should appear. |
461 | Also notice the extra "INSIDE BASE METHOD" debug message in the |
462 | development server output from the C<base> method. Click the "Return to |
463 | list" link, and you should find that there are now eight books shown. |
464 | (You may have a larger number of books if you repeated any of the |
465 | "create" actions more than once. Don't worry about it as long as the |
466 | number of books is appropriate for the number of times you added new |
467 | books... there should be the original five books added via |
f4e9de4a |
468 | F<myapp01.sql> plus one additional book for each time you ran one of the |
ee53cc71 |
469 | url_create variations above.) |
d442cc9f |
470 | |
471 | |
472 | =head1 MANUALLY BUILDING A CREATE FORM |
473 | |
474 | Although the C<url_create> action in the previous step does begin to |
475 | reveal the power and flexibility of both Catalyst and DBIC, it's |
476 | obviously not a very realistic example of how users should be expected |
22fe0f18 |
477 | to enter data. This section begins to address that concern (but just |
478 | barely, see L<Chapter 9|Catalyst::Manual::Tutorial::09_AdvancedCRUD> |
479 | for better options for handling web-based forms). |
d442cc9f |
480 | |
481 | |
482 | =head2 Add Method to Display The Form |
483 | |
f4e9de4a |
484 | Edit F<lib/MyApp/Controller/Books.pm> and add the following method: |
d442cc9f |
485 | |
486 | =head2 form_create |
7ce05098 |
487 | |
d442cc9f |
488 | Display form to collect information for book to create |
7ce05098 |
489 | |
d442cc9f |
490 | =cut |
7ce05098 |
491 | |
89d3dae9 |
492 | sub form_create :Chained('base') :PathPart('form_create') :Args(0) { |
d442cc9f |
493 | my ($self, $c) = @_; |
7ce05098 |
494 | |
d442cc9f |
495 | # Set the TT template to use |
0ed3df53 |
496 | $c->stash(template => 'books/form_create.tt2'); |
d442cc9f |
497 | } |
498 | |
72609296 |
499 | This action simply invokes a view containing a form to create a book. |
d442cc9f |
500 | |
1390ef0e |
501 | |
d442cc9f |
502 | =head2 Add a Template for the Form |
503 | |
f4e9de4a |
504 | Open F<root/src/books/form_create.tt2> in your editor and enter: |
d442cc9f |
505 | |
506 | [% META title = 'Manual Form Book Create' -%] |
7ce05098 |
507 | |
8a7c5151 |
508 | <form method="post" action="[% c.uri_for('form_create_do') %]"> |
d442cc9f |
509 | <table> |
510 | <tr><td>Title:</td><td><input type="text" name="title"></td></tr> |
511 | <tr><td>Rating:</td><td><input type="text" name="rating"></td></tr> |
512 | <tr><td>Author ID:</td><td><input type="text" name="author_id"></td></tr> |
513 | </table> |
514 | <input type="submit" name="Submit" value="Submit"> |
515 | </form> |
516 | |
517 | Note that we have specified the target of the form data as |
518 | C<form_create_do>, the method created in the section that follows. |
519 | |
1390ef0e |
520 | |
d442cc9f |
521 | =head2 Add a Method to Process Form Values and Update Database |
522 | |
f4e9de4a |
523 | Edit F<lib/MyApp/Controller/Books.pm> and add the following method to |
d442cc9f |
524 | save the form information to the database: |
525 | |
526 | =head2 form_create_do |
7ce05098 |
527 | |
d442cc9f |
528 | Take information from form and add to database |
7ce05098 |
529 | |
d442cc9f |
530 | =cut |
7ce05098 |
531 | |
89d3dae9 |
532 | sub form_create_do :Chained('base') :PathPart('form_create_do') :Args(0) { |
d442cc9f |
533 | my ($self, $c) = @_; |
7ce05098 |
534 | |
d442cc9f |
535 | # Retrieve the values from the form |
536 | my $title = $c->request->params->{title} || 'N/A'; |
537 | my $rating = $c->request->params->{rating} || 'N/A'; |
538 | my $author_id = $c->request->params->{author_id} || '1'; |
7ce05098 |
539 | |
d442cc9f |
540 | # Create the book |
3b1fa91b |
541 | my $book = $c->model('DB::Book')->create({ |
d442cc9f |
542 | title => $title, |
543 | rating => $rating, |
544 | }); |
545 | # Handle relationship with author |
fce83e5f |
546 | $book->add_to_book_authors({author_id => $author_id}); |
547 | # Note: Above is a shortcut for this: |
548 | # $book->create_related('book_authors', {author_id => $author_id}); |
7ce05098 |
549 | |
0ed3df53 |
550 | # Store new model object in stash and set template |
551 | $c->stash(book => $book, |
552 | template => 'books/create_done.tt2'); |
d442cc9f |
553 | } |
554 | |
555 | |
556 | =head2 Test Out The Form |
557 | |
ee53cc71 |
558 | Notice that the server startup log reflects the two new chained methods |
559 | that we added: |
89d3dae9 |
560 | |
fbbb9084 |
561 | [debug] Loaded Chained actions: |
562 | .-------------------------------------+--------------------------------------. |
563 | | Path Spec | Private | |
564 | +-------------------------------------+--------------------------------------+ |
565 | | /books/form_create | /books/base (0) | |
566 | | | => /books/form_create | |
567 | | /books/form_create_do | /books/base (0) | |
568 | | | => /books/form_create_do | |
569 | | /books/url_create/*/*/* | /books/base (0) | |
570 | | | => /books/url_create | |
571 | '-------------------------------------+--------------------------------------' |
89d3dae9 |
572 | |
d442cc9f |
573 | Point your browser to L<http://localhost:3000/books/form_create> and |
574 | enter "TCP/IP Illustrated, Vol 3" for the title, a rating of 5, and an |
1390ef0e |
575 | author ID of 4. You should then see the output of the same |
f4e9de4a |
576 | F<create_done.tt2> template seen in earlier examples. Finally, click |
d442cc9f |
577 | "Return to list" to view the full list of books. |
578 | |
579 | B<Note:> Having the user enter the primary key ID for the author is |
fce83e5f |
580 | obviously crude; we will address this concern with a drop-down list and |
22fe0f18 |
581 | add validation to our forms in |
582 | L<Chapter 9|Catalyst::Manual::Tutorial::09_AdvancedCRUD>. |
d442cc9f |
583 | |
584 | |
585 | =head1 A SIMPLE DELETE FEATURE |
586 | |
72609296 |
587 | Turning our attention to the Delete portion of CRUD, this section |
d442cc9f |
588 | illustrates some basic techniques that can be used to remove information |
589 | from the database. |
590 | |
591 | |
592 | =head2 Include a Delete Link in the List |
593 | |
f4e9de4a |
594 | Edit F<root/src/books/list.tt2> and update it to match the following |
ee53cc71 |
595 | (two sections have changed: 1) the additional '<th>Links</th>' table |
22fe0f18 |
596 | header, and 2) the five lines for the Delete link near the bottom): |
d442cc9f |
597 | |
22fe0f18 |
598 | [% # This is a TT comment. -%] |
7ce05098 |
599 | |
22fe0f18 |
600 | [%- # Provide a title -%] |
d442cc9f |
601 | [% META title = 'Book List' -%] |
7ce05098 |
602 | |
22fe0f18 |
603 | [% # Note That the '-' at the beginning or end of TT code -%] |
604 | [% # "chomps" the whitespace/newline at that end of the -%] |
605 | [% # output (use View Source in browser to see the effect) -%] |
7ce05098 |
606 | |
22fe0f18 |
607 | [% # Some basic HTML with a loop to display books -%] |
d442cc9f |
608 | <table> |
609 | <tr><th>Title</th><th>Rating</th><th>Author(s)</th><th>Links</th></tr> |
610 | [% # Display each book in a table row %] |
611 | [% FOREACH book IN books -%] |
612 | <tr> |
613 | <td>[% book.title %]</td> |
614 | <td>[% book.rating %]</td> |
615 | <td> |
22fe0f18 |
616 | [% # NOTE: See Chapter 4 for a better way to do this! -%] |
d442cc9f |
617 | [% # First initialize a TT variable to hold a list. Then use a TT FOREACH -%] |
618 | [% # loop in 'side effect notation' to load just the last names of the -%] |
55490817 |
619 | [% # authors into the list. Note that the 'push' TT vmethod doesn't return -%] |
d442cc9f |
620 | [% # a value, so nothing will be printed here. But, if you have something -%] |
22fe0f18 |
621 | [% # in TT that does return a value and you don't want it printed, you -%] |
6d97b973 |
622 | [% # 1) assign it to a bogus value, or -%] |
623 | [% # 2) use the CALL keyword to call it and discard the return value. -%] |
d442cc9f |
624 | [% tt_authors = [ ]; |
fce83e5f |
625 | tt_authors.push(author.last_name) FOREACH author = book.authors %] |
d442cc9f |
626 | [% # Now use a TT 'virtual method' to display the author count in parens -%] |
3b1fa91b |
627 | [% # Note the use of the TT filter "| html" to escape dangerous characters -%] |
628 | ([% tt_authors.size | html %]) |
d442cc9f |
629 | [% # Use another TT vmethod to join & print the names & comma separators -%] |
3b1fa91b |
630 | [% tt_authors.join(', ') | html %] |
d442cc9f |
631 | </td> |
632 | <td> |
633 | [% # Add a link to delete a book %] |
22fe0f18 |
634 | <a href="[% |
635 | c.uri_for(c.controller.action_for('delete'), [book.id]) %]">Delete</a> |
d442cc9f |
636 | </td> |
637 | </tr> |
638 | [% END -%] |
639 | </table> |
640 | |
55490817 |
641 | The additional code is obviously designed to add a new column to the |
72609296 |
642 | right side of the table with a C<Delete> "button" (for simplicity, links |
22fe0f18 |
643 | will be used instead of full HTML buttons; but, in practice, anything |
644 | that modifies data should be handled with a form sending a POST |
645 | request). |
fe01b24f |
646 | |
ee53cc71 |
647 | Also notice that we are using a more advanced form of C<uri_for> than we |
429d1caf |
648 | have seen before. Here we use C<< $c->controller->action_for >> to |
ee53cc71 |
649 | automatically generate a URI appropriate for that action based on the |
650 | method we want to link to while inserting the C<book.id> value into the |
651 | appropriate place. Now, if you ever change C<:PathPart('delete')> in |
22fe0f18 |
652 | your controller method to something like C<:PathPart('kill')>, then your |
653 | links will automatically update without any changes to your .tt2 |
654 | template file. As long as the name of your method does not change |
655 | (here, "delete"), then your links will still be correct. There are a |
656 | few shortcuts and options when using C<action_for()>: |
0416017e |
657 | |
658 | =over 4 |
659 | |
660 | =item * |
661 | |
ee53cc71 |
662 | If you are referring to a method in the current controller, you can use |
429d1caf |
663 | C<< $self->action_for('_method_name_') >>. |
0416017e |
664 | |
665 | =item * |
666 | |
ee53cc71 |
667 | If you are referring to a method in a different controller, you need to |
668 | include that controller's name as an argument to C<controller()>, as in |
429d1caf |
669 | C<< $c->controller('_controller_name_')->action_for('_method_name_') >>. |
0416017e |
670 | |
671 | =back |
b2ad8bbd |
672 | |
55490817 |
673 | B<Note:> In practice you should B<never> use a GET request to delete a |
674 | record -- always use POST for actions that will modify data. We are |
c5d94181 |
675 | doing it here for illustrative and simplicity purposes only. |
d442cc9f |
676 | |
1390ef0e |
677 | |
994b66ad |
678 | =head2 Add a Common Method to Retrieve a Book for the Chain |
679 | |
ee53cc71 |
680 | As mentioned earlier, since we have a mixture of actions that operate on |
681 | a single book ID and others that do not, we should not have C<base> |
55490817 |
682 | capture the book ID, find the corresponding book in the database and |
683 | save it in the stash for later links in the chain. However, just |
ee53cc71 |
684 | because that logic does not belong in C<base> doesn't mean that we can't |
685 | create another location to centralize the book lookup code. In our |
686 | case, we will create a method called C<object> that will store the |
55490817 |
687 | specific book in the stash. Chains that always operate on a single |
688 | existing book can chain off this method, but methods such as |
ee53cc71 |
689 | C<url_create> that don't operate on an existing book can chain directly |
690 | off base. |
994b66ad |
691 | |
f4e9de4a |
692 | To add the C<object> method, edit F<lib/MyApp/Controller/Books.pm> and |
ee53cc71 |
693 | add the following code: |
994b66ad |
694 | |
e075db0c |
695 | =head2 object |
7ce05098 |
696 | |
e075db0c |
697 | Fetch the specified book object based on the book ID and store |
698 | it in the stash |
7ce05098 |
699 | |
e075db0c |
700 | =cut |
7ce05098 |
701 | |
994b66ad |
702 | sub object :Chained('base') :PathPart('id') :CaptureArgs(1) { |
fbbb9084 |
703 | # $id = primary key of book to delete |
994b66ad |
704 | my ($self, $c, $id) = @_; |
7ce05098 |
705 | |
994b66ad |
706 | # Find the book object and store it in the stash |
707 | $c->stash(object => $c->stash->{resultset}->find($id)); |
7ce05098 |
708 | |
994b66ad |
709 | # Make sure the lookup was successful. You would probably |
710 | # want to do something like this in a real app: |
711 | # $c->detach('/error_404') if !$c->stash->{object}; |
712 | die "Book $id not found!" if !$c->stash->{object}; |
7ce05098 |
713 | |
fce83e5f |
714 | # Print a message to the debug log |
715 | $c->log->debug("*** INSIDE OBJECT METHOD for obj id=$id ***"); |
994b66ad |
716 | } |
717 | |
ee53cc71 |
718 | Now, any other method that chains off C<object> will automatically have |
429d1caf |
719 | the appropriate book waiting for it in C<< $c->stash->{object} >>. |
994b66ad |
720 | |
994b66ad |
721 | |
d442cc9f |
722 | =head2 Add a Delete Action to the Controller |
723 | |
f4e9de4a |
724 | Open F<lib/MyApp/Controller/Books.pm> in your editor and add the |
d442cc9f |
725 | following method: |
726 | |
1390ef0e |
727 | =head2 delete |
7ce05098 |
728 | |
d442cc9f |
729 | Delete a book |
7ce05098 |
730 | |
d442cc9f |
731 | =cut |
7ce05098 |
732 | |
994b66ad |
733 | sub delete :Chained('object') :PathPart('delete') :Args(0) { |
994b66ad |
734 | my ($self, $c) = @_; |
7ce05098 |
735 | |
994b66ad |
736 | # Use the book object saved by 'object' and delete it along |
3b1fa91b |
737 | # with related 'book_author' entries |
994b66ad |
738 | $c->stash->{object}->delete; |
7ce05098 |
739 | |
d442cc9f |
740 | # Set a status message to be displayed at the top of the view |
741 | $c->stash->{status_msg} = "Book deleted."; |
7ce05098 |
742 | |
d442cc9f |
743 | # Forward to the list action/method in this controller |
744 | $c->forward('list'); |
745 | } |
746 | |
55490817 |
747 | This method first deletes the book object saved by the C<object> method. |
ee53cc71 |
748 | However, it also removes the corresponding entry from the C<book_author> |
749 | table with a cascading delete. |
d442cc9f |
750 | |
751 | Then, rather than forwarding to a "delete done" page as we did with the |
752 | earlier create example, it simply sets the C<status_msg> to display a |
753 | notification to the user as the normal list view is rendered. |
754 | |
755 | The C<delete> action uses the context C<forward> method to return the |
756 | user to the book list. The C<detach> method could have also been used. |
757 | Whereas C<forward> I<returns> to the original action once it is |
758 | completed, C<detach> does I<not> return. Other than that, the two are |
759 | equivalent. |
760 | |
761 | |
762 | =head2 Try the Delete Feature |
763 | |
ee53cc71 |
764 | Once you save the Books controller, the server should automatically |
765 | restart. The C<delete> method should now appear in the "Loaded Chained |
766 | actions" section of the startup debug output: |
89d3dae9 |
767 | |
fbbb9084 |
768 | [debug] Loaded Chained actions: |
994b66ad |
769 | .-------------------------------------+--------------------------------------. |
770 | | Path Spec | Private | |
771 | +-------------------------------------+--------------------------------------+ |
772 | | /books/id/*/delete | /books/base (0) | |
773 | | | -> /books/object (1) | |
774 | | | => /books/delete | |
775 | | /books/form_create | /books/base (0) | |
776 | | | => /books/form_create | |
777 | | /books/form_create_do | /books/base (0) | |
778 | | | => /books/form_create_do | |
779 | | /books/url_create/*/*/* | /books/base (0) | |
780 | | | => /books/url_create | |
781 | '-------------------------------------+--------------------------------------' |
89d3dae9 |
782 | |
d442cc9f |
783 | Then point your browser to L<http://localhost:3000/books/list> and click |
55490817 |
784 | the "Delete" link next to the first "TCPIP_Illustrated_Vol-2". A green |
785 | "Book deleted" status message should display at the top of the page, |
994b66ad |
786 | along with a list of the eight remaining books. You will also see the |
787 | cascading delete operation via the DBIC_TRACE output: |
788 | |
3b1fa91b |
789 | SELECT me.id, me.title, me.rating FROM book me WHERE ( ( me.id = ? ) ): '6' |
790 | DELETE FROM book WHERE ( id = ? ): '6' |
d442cc9f |
791 | |
d28e06d1 |
792 | If you get the error C<file error - books/delete.tt2: not found> then you |
793 | probably forgot to uncomment the template line in C<sub list> at the end of |
794 | chapter 3. |
d442cc9f |
795 | |
796 | =head2 Fixing a Dangerous URL |
797 | |
55490817 |
798 | Note the URL in your browser once you have performed the deletion in the |
d442cc9f |
799 | prior step -- it is still referencing the delete action: |
800 | |
acbd7bdd |
801 | http://localhost:3000/books/id/6/delete |
d442cc9f |
802 | |
55490817 |
803 | What if the user were to press reload with this URL still active? In |
ee53cc71 |
804 | this case the redundant delete is harmless (although it does generate an |
805 | exception screen, it doesn't perform any undesirable actions on the |
22fe0f18 |
806 | application or database), but in other cases this could clearly lead to |
807 | trouble. |
d442cc9f |
808 | |
809 | We can improve the logic by converting to a redirect. Unlike |
429d1caf |
810 | C<< $c->forward('list')) >> or C<< $c->detach('list')) >> that perform a |
ee53cc71 |
811 | server-side alteration in the flow of processing, a redirect is a |
812 | client-side mechanism that causes the browser to issue an entirely new |
813 | request. As a result, the URL in the browser is updated to match the |
814 | destination of the redirection URL. |
d442cc9f |
815 | |
ee53cc71 |
816 | To convert the forward used in the previous section to a redirect, open |
f4e9de4a |
817 | F<lib/MyApp/Controller/Books.pm> and edit the existing C<sub delete> |
ee53cc71 |
818 | method to match: |
d442cc9f |
819 | |
994b66ad |
820 | =head2 delete |
7ce05098 |
821 | |
d442cc9f |
822 | Delete a book |
7ce05098 |
823 | |
d442cc9f |
824 | =cut |
7ce05098 |
825 | |
994b66ad |
826 | sub delete :Chained('object') :PathPart('delete') :Args(0) { |
fbbb9084 |
827 | my ($self, $c) = @_; |
7ce05098 |
828 | |
994b66ad |
829 | # Use the book object saved by 'object' and delete it along |
3b1fa91b |
830 | # with related 'book_author' entries |
994b66ad |
831 | $c->stash->{object}->delete; |
7ce05098 |
832 | |
d442cc9f |
833 | # Set a status message to be displayed at the top of the view |
834 | $c->stash->{status_msg} = "Book deleted."; |
7ce05098 |
835 | |
0416017e |
836 | # Redirect the user back to the list page. Note the use |
837 | # of $self->action_for as earlier in this section (BasicCRUD) |
fbbb9084 |
838 | $c->response->redirect($c->uri_for($self->action_for('list'))); |
d442cc9f |
839 | } |
840 | |
841 | |
842 | =head2 Try the Delete and Redirect Logic |
843 | |
ee53cc71 |
844 | Point your browser to L<http://localhost:3000/books/list> (don't just |
845 | hit "Refresh" in your browser since we left the URL in an invalid state |
846 | in the previous section!) and delete the first copy of the remaining two |
847 | "TCPIP_Illustrated_Vol-2" books. The URL in your browser should return |
848 | to the L<http://localhost:3000/books/list> URL, so that is an |
849 | improvement, but notice that I<no green "Book deleted" status message is |
850 | displayed>. Because the stash is reset on every request (and a redirect |
851 | involves a second request), the C<status_msg> is cleared before it can |
22fe0f18 |
852 | be displayed. |
d442cc9f |
853 | |
854 | |
8a472b34 |
855 | =head2 Using 'uri_for' to Pass Query Parameters |
d442cc9f |
856 | |
ee53cc71 |
857 | There are several ways to pass information across a redirect. One option |
22fe0f18 |
858 | is to use the C<flash> technique that we will see in |
859 | L<Chapter 5|Catalyst::Manual::Tutorial::05_Authentication> of this |
ee53cc71 |
860 | tutorial; however, here we will pass the information via query |
861 | parameters on the redirect itself. Open |
f4e9de4a |
862 | F<lib/MyApp/Controller/Books.pm> and update the existing C<sub delete> |
89d3dae9 |
863 | method to match the following: |
d442cc9f |
864 | |
55490817 |
865 | =head2 delete |
7ce05098 |
866 | |
d442cc9f |
867 | Delete a book |
7ce05098 |
868 | |
d442cc9f |
869 | =cut |
7ce05098 |
870 | |
994b66ad |
871 | sub delete :Chained('object') :PathPart('delete') :Args(0) { |
fbbb9084 |
872 | my ($self, $c) = @_; |
7ce05098 |
873 | |
994b66ad |
874 | # Use the book object saved by 'object' and delete it along |
3b1fa91b |
875 | # with related 'book_author' entries |
994b66ad |
876 | $c->stash->{object}->delete; |
7ce05098 |
877 | |
d442cc9f |
878 | # Redirect the user back to the list page with status msg as an arg |
55490817 |
879 | $c->response->redirect($c->uri_for($self->action_for('list'), |
d442cc9f |
880 | {status_msg => "Book deleted."})); |
881 | } |
882 | |
883 | This modification simply leverages the ability of C<uri_for> to include |
55490817 |
884 | an arbitrary number of name/value pairs in a hash reference. Next, we |
f4e9de4a |
885 | need to update F<root/src/wrapper.tt2> to handle C<status_msg> as a |
d442cc9f |
886 | query parameter: |
887 | |
1390ef0e |
888 | ... |
d442cc9f |
889 | <div id="content"> |
1390ef0e |
890 | [%# Status and error messages %] |
ee53cc71 |
891 | <span class="message">[% |
892 | status_msg || c.request.params.status_msg | html %]</span> |
1390ef0e |
893 | <span class="error">[% error_msg %]</span> |
894 | [%# This is where TT will stick all of your template's contents. -%] |
895 | [% content %] |
896 | </div><!-- end content --> |
897 | ... |
898 | |
ee53cc71 |
899 | Although the sample above only shows the C<content> div, leave the rest |
f4e9de4a |
900 | of the file intact -- the only change we made to the F<wrapper.tt2> was |
22fe0f18 |
901 | to add "C<|| c.request.params.status_msg>" to the |
429d1caf |
902 | C<< <span class="message"> >> line. Note that we definitely want |
b3876d9e |
903 | the "C<| html>" TT filter here since it would be easy for users to |
904 | modify the message on the URL and possibly inject harmful code into the |
905 | application if we left that off. |
d442cc9f |
906 | |
907 | |
908 | =head2 Try the Delete and Redirect With Query Param Logic |
909 | |
ee53cc71 |
910 | Point your browser to L<http://localhost:3000/books/list> (you should |
911 | now be able to safely hit "refresh" in your browser). Then delete the |
912 | remaining copy of "TCPIP_Illustrated_Vol-2". The green "Book deleted" |
a608d8ce |
913 | status message should return. But notice that you can now hit the |
ee53cc71 |
914 | "Reload" button in your browser and it just redisplays the book list |
915 | (and it correctly shows it without the "Book deleted" message on |
916 | redisplay). |
d442cc9f |
917 | |
22fe0f18 |
918 | B<NOTE:> Be sure to check out |
919 | L<Authentication|Catalyst::Manual::Tutorial::05_Authentication> where we |
920 | use an improved technique that is better suited to your real world |
921 | applications. |
d442cc9f |
922 | |
923 | |
1cde0fd6 |
924 | =head1 EXPLORING THE POWER OF DBIC |
925 | |
ee53cc71 |
926 | In this section we will explore some additional capabilities offered by |
22fe0f18 |
927 | L<DBIx::Class>. Although these features have relatively little to do |
928 | with Catalyst per se, you will almost certainly want to take advantage |
929 | of them in your applications. |
1cde0fd6 |
930 | |
931 | |
1cde0fd6 |
932 | =head2 Add Datetime Columns to Our Existing Books Table |
933 | |
ee53cc71 |
934 | Let's add two columns to our existing C<books> table to track when each |
935 | book was added and when each book is updated: |
1cde0fd6 |
936 | |
937 | $ sqlite3 myapp.db |
33f1d5d0 |
938 | sqlite> ALTER TABLE book ADD created TIMESTAMP; |
939 | sqlite> ALTER TABLE book ADD updated TIMESTAMP; |
3b1fa91b |
940 | sqlite> UPDATE book SET created = DATETIME('NOW'), updated = DATETIME('NOW'); |
941 | sqlite> SELECT * FROM book; |
f2bbfc36 |
942 | 1|CCSP SNRS Exam Certification Guide|5|2010-02-16 04:15:45|2010-02-16 04:15:45 |
943 | 2|TCP/IP Illustrated, Volume 1|5|2010-02-16 04:15:45|2010-02-16 04:15:45 |
944 | 3|Internetworking with TCP/IP Vol.1|4|2010-02-16 04:15:45|2010-02-16 04:15:45 |
945 | 4|Perl Cookbook|5|2010-02-16 04:15:45|2010-02-16 04:15:45 |
946 | 5|Designing with Web Standards|5|2010-02-16 04:15:45|2010-02-16 04:15:45 |
947 | 9|TCP/IP Illustrated, Vol 3|5|2010-02-16 04:15:45|2010-02-16 04:15:45 |
1cde0fd6 |
948 | sqlite> .quit |
949 | $ |
950 | |
d5d7ee98 |
951 | Here are the commands without the surrounding sqlite3 prompt and output |
952 | in case you want to cut and paste them as a single block (but still |
953 | start sqlite3 before you paste these in): |
954 | |
955 | ALTER TABLE book ADD created TIMESTAMP; |
956 | ALTER TABLE book ADD updated TIMESTAMP; |
957 | UPDATE book SET created = DATETIME('NOW'), updated = DATETIME('NOW'); |
958 | SELECT * FROM book; |
959 | |
ee53cc71 |
960 | This will modify the C<books> table to include the two new fields and |
961 | populate those fields with the current time. |
1cde0fd6 |
962 | |
acbd7bdd |
963 | |
a46b474e |
964 | =head2 Update DBIx::Class to Automatically Handle the Datetime Columns |
1cde0fd6 |
965 | |
ee53cc71 |
966 | Next, we should re-run the DBIC helper to update the Result Classes with |
967 | the new fields: |
1cde0fd6 |
968 | |
969 | $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \ |
b6e53c1c |
970 | create=static components=TimeStamp dbi:SQLite:myapp.db \ |
b66dd084 |
971 | on_connect_do="PRAGMA foreign_keys = ON" |
477a6d5b |
972 | exists "/home/catalyst/dev/MyApp/script/../lib/MyApp/Model" |
973 | exists "/home/catalyst/dev/MyApp/script/../t" |
974 | Dumping manual schema for MyApp::Schema to directory /home/catalyst/dev/MyApp/script/../lib ... |
1cde0fd6 |
975 | Schema dump completed. |
477a6d5b |
976 | exists "/home/catalyst/dev/MyApp/script/../lib/MyApp/Model/DB.pm" |
1cde0fd6 |
977 | |
ee53cc71 |
978 | Notice that we modified our use of the helper slightly: we told it to |
979 | include the L<DBIx::Class::TimeStamp> in the C<load_components> line of |
980 | the Result Classes. |
1cde0fd6 |
981 | |
f4e9de4a |
982 | If you open F<lib/MyApp/Schema/Result/Book.pm> in your editor you should |
ee53cc71 |
983 | see that the C<created> and C<updated> fields are now included in the |
984 | call to C<add_columns()>. However, also notice that the C<many_to_many> |
985 | relationships we manually added below the "C<# DO NOT MODIFY...>" line |
986 | were automatically preserved. |
1cde0fd6 |
987 | |
f4e9de4a |
988 | While we F<lib/MyApp/Schema/Result/Book.pm> open, let's update it with |
d5d7ee98 |
989 | some additional information to have DBIC automatically handle the |
990 | updating of these two fields for us. Insert the following code at the |
991 | bottom of the file (it B<must> be B<below> the "C<# DO NOT MODIFY...>" |
992 | line and B<above> the C<1;> on the last line): |
1cde0fd6 |
993 | |
994 | # |
995 | # Enable automatic date handling |
996 | # |
997 | __PACKAGE__->add_columns( |
998 | "created", |
33f1d5d0 |
999 | { data_type => 'timestamp', set_on_create => 1 }, |
1cde0fd6 |
1000 | "updated", |
33f1d5d0 |
1001 | { data_type => 'timestamp', set_on_create => 1, set_on_update => 1 }, |
55490817 |
1002 | ); |
1cde0fd6 |
1003 | |
ee53cc71 |
1004 | This will override the definition for these fields that Schema::Loader |
1005 | placed at the top of the file. The C<set_on_create> and |
1006 | C<set_on_update> options will cause DBIx::Class to automatically update |
1007 | the timestamps in these columns whenever a row is created or modified. |
1cde0fd6 |
1008 | |
22fe0f18 |
1009 | B<Note> that adding the lines above will cause the development server to |
1010 | automatically restart if you are running it with the "-r" option. In |
1011 | other words, the development server is smart enough to restart not only |
f4e9de4a |
1012 | for code under the F<MyApp/Controller/>, F<MyApp/Model/>, and |
1013 | F<MyApp/View/> directories, but also under other directions such as our |
1014 | "external DBIC model" in F<MyApp/Schema/>. However, also note that it's |
22fe0f18 |
1015 | smart enough to B<not> restart when you edit your C<.tt2> files under |
f4e9de4a |
1016 | F<root/>. |
22fe0f18 |
1017 | |
1cde0fd6 |
1018 | Then enter the following URL into your web browser: |
1019 | |
1020 | http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4 |
1021 | |
22fe0f18 |
1022 | You should get the same "Book Created" screen we saw earlier. However, if |
ee53cc71 |
1023 | you now use the sqlite3 command-line tool to dump the C<books> table, |
1024 | you will see that the new book we added has an appropriate date and time |
1025 | entered for it (see the last line in the listing below): |
1cde0fd6 |
1026 | |
444d6b27 |
1027 | $ sqlite3 myapp.db "select * from book" |
f2bbfc36 |
1028 | 1|CCSP SNRS Exam Certification Guide|5|2010-02-16 04:15:45|2010-02-16 04:15:45 |
1029 | 2|TCP/IP Illustrated, Volume 1|5|2010-02-16 04:15:45|2010-02-16 04:15:45 |
1030 | 3|Internetworking with TCP/IP Vol.1|4|2010-02-16 04:15:45|2010-02-16 04:15:45 |
1031 | 4|Perl Cookbook|5|2010-02-16 04:15:45|2010-02-16 04:15:45 |
1032 | 5|Designing with Web Standards|5|2010-02-16 04:15:45|2010-02-16 04:15:45 |
1033 | 9|TCP/IP Illustrated, Vol 3|5|2010-02-16 04:15:45|2010-02-16 04:15:45 |
1034 | 10|TCPIP_Illustrated_Vol-2|5|2010-02-16 04:18:42|2010-02-16 04:18:42 |
1cde0fd6 |
1035 | |
55490817 |
1036 | Notice in the debug log that the SQL DBIC generated has changed to |
1cde0fd6 |
1037 | incorporate the datetime logic: |
1038 | |
7ce05098 |
1039 | INSERT INTO book ( created, rating, title, updated ) VALUES ( ?, ?, ?, ? ): |
f2bbfc36 |
1040 | '2010-02-16 04:18:42', '5', 'TCPIP_Illustrated_Vol-2', '2010-02-16 04:18:42' |
2a6eb5f9 |
1041 | INSERT INTO book_author ( author_id, book_id ) VALUES ( ?, ? ): '4', '10' |
1cde0fd6 |
1042 | |
1043 | |
1044 | =head2 Create a ResultSet Class |
1045 | |
444d6b27 |
1046 | An often overlooked but extremely powerful features of DBIC is that it |
55490817 |
1047 | allows you to supply your own subclasses of C<DBIx::Class::ResultSet>. |
22fe0f18 |
1048 | This can be used to pull complex and unsightly "query code" out of your |
1cde0fd6 |
1049 | controllers and encapsulate it in a method of your ResultSet Class. |
ee53cc71 |
1050 | These "canned queries" in your ResultSet Class can then be invoked via a |
1051 | single call, resulting in much cleaner and easier to read controller |
22fe0f18 |
1052 | code (or View code, if that's where you want to call it). |
1cde0fd6 |
1053 | |
55490817 |
1054 | To illustrate the concept with a fairly simple example, let's create a |
ee53cc71 |
1055 | method that returns books added in the last 10 minutes. Start by making |
1056 | a directory where DBIx::Class will look for our ResultSet Class: |
1cde0fd6 |
1057 | |
444d6b27 |
1058 | $ mkdir lib/MyApp/Schema/ResultSet |
1cde0fd6 |
1059 | |
f4e9de4a |
1060 | Then open F<lib/MyApp/Schema/ResultSet/Book.pm> and enter the following: |
1cde0fd6 |
1061 | |
3b1fa91b |
1062 | package MyApp::Schema::ResultSet::Book; |
7ce05098 |
1063 | |
1cde0fd6 |
1064 | use strict; |
1065 | use warnings; |
1066 | use base 'DBIx::Class::ResultSet'; |
7ce05098 |
1067 | |
1cde0fd6 |
1068 | =head2 created_after |
7ce05098 |
1069 | |
1cde0fd6 |
1070 | A predefined search for recently added books |
7ce05098 |
1071 | |
1cde0fd6 |
1072 | =cut |
7ce05098 |
1073 | |
1cde0fd6 |
1074 | sub created_after { |
fadc4ae7 |
1075 | my ($self, $datetime) = @_; |
7ce05098 |
1076 | |
b66dd084 |
1077 | my $date_str = $self->result_source->schema->storage |
fadc4ae7 |
1078 | ->datetime_parser->format_datetime($datetime); |
7ce05098 |
1079 | |
fadc4ae7 |
1080 | return $self->search({ |
1081 | created => { '>' => $date_str } |
1082 | }); |
1cde0fd6 |
1083 | } |
7ce05098 |
1084 | |
1cde0fd6 |
1085 | 1; |
1086 | |
f4e9de4a |
1087 | Then add the following method to the F<lib/MyApp/Controller/Books.pm>: |
1cde0fd6 |
1088 | |
1089 | =head2 list_recent |
7ce05098 |
1090 | |
1cde0fd6 |
1091 | List recently created books |
7ce05098 |
1092 | |
1cde0fd6 |
1093 | =cut |
7ce05098 |
1094 | |
1cde0fd6 |
1095 | sub list_recent :Chained('base') :PathPart('list_recent') :Args(1) { |
1096 | my ($self, $c, $mins) = @_; |
7ce05098 |
1097 | |
1cde0fd6 |
1098 | # Retrieve all of the book records as book model objects and store in the |
1099 | # stash where they can be accessed by the TT template, but only |
1100 | # retrieve books created within the last $min number of minutes |
0ed3df53 |
1101 | $c->stash(books => [$c->model('DB::Book') |
1102 | ->created_after(DateTime->now->subtract(minutes => $mins))]); |
7ce05098 |
1103 | |
1cde0fd6 |
1104 | # Set the TT template to use. You will almost always want to do this |
1105 | # in your action methods (action methods respond to user input in |
1106 | # your controllers). |
0ed3df53 |
1107 | $c->stash(template => 'books/list.tt2'); |
1cde0fd6 |
1108 | } |
1109 | |
ee53cc71 |
1110 | Now try different values for the "minutes" argument (the final number |
1111 | value) using the URL C<http://localhost:3000/books/list_recent/_#_> in |
1112 | your browser. For example, this would list all books added in the last |
1113 | fifteen minutes: |
1cde0fd6 |
1114 | |
1115 | http://localhost:3000/books/list_recent/15 |
1116 | |
ee53cc71 |
1117 | Depending on how recently you added books, you might want to try a |
1118 | higher or lower value for the minutes. |
1cde0fd6 |
1119 | |
1120 | |
1121 | =head2 Chaining ResultSets |
1122 | |
22fe0f18 |
1123 | One of the most helpful and powerful features in C<DBIx::Class> is that |
1124 | it allows you to "chain together" a series of queries (note that this |
1125 | has nothing to do with the "Chained Dispatch" for Catalyst that we were |
1126 | discussing earlier). Because each ResultSet method returns another |
1127 | ResultSet, you can take an initial query and immediately feed that into |
1128 | a second query (and so on for as many queries you need). Note that no |
1129 | matter how many ResultSets you chain together, the database itself will |
1130 | not be hit until you use a method that attempts to access the data. And, |
1131 | because this technique carries over to the ResultSet Class feature we |
ee53cc71 |
1132 | implemented in the previous section for our "canned search", we can |
1133 | combine the two capabilities. For example, let's add an action to our |
1134 | C<Books> controller that lists books that are both recent I<and> have |
f4e9de4a |
1135 | "TCP" in the title. Open up F<lib/MyApp/Controller/Books.pm> and add |
ee53cc71 |
1136 | the following method: |
1cde0fd6 |
1137 | |
acbd7bdd |
1138 | =head2 list_recent_tcp |
7ce05098 |
1139 | |
1cde0fd6 |
1140 | List recently created books |
7ce05098 |
1141 | |
1cde0fd6 |
1142 | =cut |
7ce05098 |
1143 | |
1cde0fd6 |
1144 | sub list_recent_tcp :Chained('base') :PathPart('list_recent_tcp') :Args(1) { |
1145 | my ($self, $c, $mins) = @_; |
7ce05098 |
1146 | |
1cde0fd6 |
1147 | # Retrieve all of the book records as book model objects and store in the |
1148 | # stash where they can be accessed by the TT template, but only |
1149 | # retrieve books created within the last $min number of minutes |
1150 | # AND that have 'TCP' in the title |
22fe0f18 |
1151 | $c->stash(books => [ |
1152 | $c->model('DB::Book') |
1153 | ->created_after(DateTime->now->subtract(minutes => $mins)) |
1154 | ->search({title => {'like', '%TCP%'}}) |
1155 | ]); |
7ce05098 |
1156 | |
1cde0fd6 |
1157 | # Set the TT template to use. You will almost always want to do this |
1158 | # in your action methods (action methods respond to user input in |
1159 | # your controllers). |
0ed3df53 |
1160 | $c->stash(template => 'books/list.tt2'); |
1cde0fd6 |
1161 | } |
1162 | |
f2bbfc36 |
1163 | To try this out, enter the following URL into your browser: |
1cde0fd6 |
1164 | |
1165 | http://localhost:3000/books/list_recent_tcp/100 |
1166 | |
55490817 |
1167 | And you should get a list of books added in the last 100 minutes that |
1168 | contain the string "TCP" in the title. However, if you look at all |
ee53cc71 |
1169 | books within the last 100 minutes, you should get a longer list (again, |
1170 | you might have to adjust the number of minutes depending on how recently |
1171 | you added books to your database): |
1cde0fd6 |
1172 | |
1173 | http://localhost:3000/books/list_recent/100 |
1174 | |
55490817 |
1175 | Take a look at the DBIC_TRACE output in the development server log for |
1cde0fd6 |
1176 | the first URL and you should see something similar to the following: |
1177 | |
7ce05098 |
1178 | SELECT me.id, me.title, me.rating, me.created, me.updated FROM book me |
f2bbfc36 |
1179 | WHERE ( ( title LIKE ? AND created > ? ) ): '%TCP%', '2010-02-16 02:49:32' |
1cde0fd6 |
1180 | |
ee53cc71 |
1181 | However, let's not pollute our controller code with this raw "TCP" query |
1182 | -- it would be cleaner to encapsulate that code in a method on our |
f4e9de4a |
1183 | ResultSet Class. To do this, open F<lib/MyApp/Schema/ResultSet/Book.pm> |
ee53cc71 |
1184 | and add the following method: |
1cde0fd6 |
1185 | |
1186 | =head2 title_like |
7ce05098 |
1187 | |
1cde0fd6 |
1188 | A predefined search for books with a 'LIKE' search in the string |
7ce05098 |
1189 | |
1cde0fd6 |
1190 | =cut |
7ce05098 |
1191 | |
1cde0fd6 |
1192 | sub title_like { |
fadc4ae7 |
1193 | my ($self, $title_str) = @_; |
7ce05098 |
1194 | |
fadc4ae7 |
1195 | return $self->search({ |
1196 | title => { 'like' => "%$title_str%" } |
1197 | }); |
1cde0fd6 |
1198 | } |
1199 | |
55490817 |
1200 | We defined the search string as C<$title_str> to make the method more |
1201 | flexible. Now update the C<list_recent_tcp> method in |
f4e9de4a |
1202 | F<lib/MyApp/Controller/Books.pm> to match the following (we have |
429d1caf |
1203 | replaced the C<< ->search >> line with the C<< ->title_like >> line |
1cde0fd6 |
1204 | shown here -- the rest of the method should be the same): |
1205 | |
1206 | =head2 list_recent_tcp |
7ce05098 |
1207 | |
1cde0fd6 |
1208 | List recently created books |
7ce05098 |
1209 | |
1cde0fd6 |
1210 | =cut |
7ce05098 |
1211 | |
1cde0fd6 |
1212 | sub list_recent_tcp :Chained('base') :PathPart('list_recent_tcp') :Args(1) { |
1213 | my ($self, $c, $mins) = @_; |
7ce05098 |
1214 | |
1cde0fd6 |
1215 | # Retrieve all of the book records as book model objects and store in the |
1216 | # stash where they can be accessed by the TT template, but only |
1217 | # retrieve books created within the last $min number of minutes |
1218 | # AND that have 'TCP' in the title |
22fe0f18 |
1219 | $c->stash(books => [ |
1220 | $c->model('DB::Book') |
1221 | ->created_after(DateTime->now->subtract(minutes => $mins)) |
1222 | ->title_like('TCP') |
1223 | ]); |
7ce05098 |
1224 | |
1cde0fd6 |
1225 | # Set the TT template to use. You will almost always want to do this |
1226 | # in your action methods (action methods respond to user input in |
1227 | # your controllers). |
0ed3df53 |
1228 | $c->stash(template => 'books/list.tt2'); |
1cde0fd6 |
1229 | } |
1230 | |
ee53cc71 |
1231 | Try out the C<list_recent_tcp> and C<list_recent> URLs as we did above. |
1232 | They should work just the same, but our code is obviously cleaner and |
1233 | more modular, while also being more flexible at the same time. |
1cde0fd6 |
1234 | |
1235 | |
1236 | =head2 Adding Methods to Result Classes |
1237 | |
ee53cc71 |
1238 | In the previous two sections we saw a good example of how we could use |
1239 | DBIx::Class ResultSet Classes to clean up our code for an entire query |
1240 | (for example, our "canned searches" that filtered the entire query). We |
1241 | can do a similar improvement when working with individual rows as well. |
1242 | Whereas the ResultSet construct is used in DBIC to correspond to an |
1243 | entire query, the Result Class construct is used to represent a row. |
1244 | Therefore, we can add row-specific "helper methods" to our Result |
f4e9de4a |
1245 | Classes stored in F<lib/MyApp/Schema/Result/>. For example, open |
1246 | F<lib/MyApp/Schema/Result/Author.pm> and add the following method (as |
a46b474e |
1247 | always, it must be above the closing "C<1;>"): |
1cde0fd6 |
1248 | |
1249 | # |
a608d8ce |
1250 | # Row-level helper methods |
1cde0fd6 |
1251 | # |
1252 | sub full_name { |
1253 | my ($self) = @_; |
7ce05098 |
1254 | |
1cde0fd6 |
1255 | return $self->first_name . ' ' . $self->last_name; |
1256 | } |
1257 | |
ee53cc71 |
1258 | This will allow us to conveniently retrieve both the first and last name |
f4e9de4a |
1259 | for an author in one shot. Now open F<root/src/books/list.tt2> and |
ee53cc71 |
1260 | change the definition of C<tt_authors> from this: |
1cde0fd6 |
1261 | |
acbd7bdd |
1262 | ... |
1cde0fd6 |
1263 | [% tt_authors = [ ]; |
fce83e5f |
1264 | tt_authors.push(author.last_name) FOREACH author = book.authors %] |
acbd7bdd |
1265 | ... |
1cde0fd6 |
1266 | |
1267 | to: |
1268 | |
acbd7bdd |
1269 | ... |
1cde0fd6 |
1270 | [% tt_authors = [ ]; |
fce83e5f |
1271 | tt_authors.push(author.full_name) FOREACH author = book.authors %] |
acbd7bdd |
1272 | ... |
1cde0fd6 |
1273 | |
ee53cc71 |
1274 | (Only C<author.last_name> was changed to C<author.full_name> -- the rest |
1275 | of the file should remain the same.) |
1cde0fd6 |
1276 | |
f2bbfc36 |
1277 | Now go to the standard book list URL: |
1cde0fd6 |
1278 | |
1279 | http://localhost:3000/books/list |
1280 | |
55490817 |
1281 | The "Author(s)" column will now contain both the first and last name. |
ee53cc71 |
1282 | And, because the concatenation logic was encapsulated inside our Result |
1283 | Class, it keeps the code inside our TT template nice and clean |
55490817 |
1284 | (remember, we want the templates to be as close to pure HTML markup as |
1285 | possible). Obviously, this capability becomes even more useful as you |
0ed0d69a |
1286 | use it to remove even more complicated row-specific logic from your |
1cde0fd6 |
1287 | templates! |
1288 | |
1289 | |
fce83e5f |
1290 | =head2 Moving Complicated View Code to the Model |
1291 | |
ee53cc71 |
1292 | The previous section illustrated how we could use a Result Class method |
1293 | to print the full names of the authors without adding any extra code to |
1294 | our view, but it still left us with a fairly ugly mess (see |
f4e9de4a |
1295 | F<root/src/books/list.tt2>): |
fce83e5f |
1296 | |
1297 | ... |
1298 | <td> |
1299 | [% # NOTE: See Chapter 4 for a better way to do this! -%] |
1300 | [% # First initialize a TT variable to hold a list. Then use a TT FOREACH -%] |
1301 | [% # loop in 'side effect notation' to load just the last names of the -%] |
1302 | [% # authors into the list. Note that the 'push' TT vmethod does not print -%] |
1303 | [% # a value, so nothing will be printed here. But, if you have something -%] |
1304 | [% # in TT that does return a method and you don't want it printed, you -%] |
1305 | [% # can: 1) assign it to a bogus value, or 2) use the CALL keyword to -%] |
1306 | [% # call it and discard the return value. -%] |
1307 | [% tt_authors = [ ]; |
1308 | tt_authors.push(author.full_name) FOREACH author = book.authors %] |
1309 | [% # Now use a TT 'virtual method' to display the author count in parens -%] |
1310 | [% # Note the use of the TT filter "| html" to escape dangerous characters -%] |
1311 | ([% tt_authors.size | html %]) |
1312 | [% # Use another TT vmethod to join & print the names & comma separators -%] |
1313 | [% tt_authors.join(', ') | html %] |
1314 | </td> |
1315 | ... |
1316 | |
ee53cc71 |
1317 | Let's combine some of the techniques used earlier in this section to |
1318 | clean this up. First, let's add a method to our Book Result Class to |
1319 | return the number of authors for a book. Open |
f4e9de4a |
1320 | F<lib/MyApp/Schema/Result/Book.pm> and add the following method: |
fce83e5f |
1321 | |
444d6b27 |
1322 | =head2 author_count |
7ce05098 |
1323 | |
444d6b27 |
1324 | Return the number of authors for the current book |
7ce05098 |
1325 | |
fce83e5f |
1326 | =cut |
7ce05098 |
1327 | |
fce83e5f |
1328 | sub author_count { |
1329 | my ($self) = @_; |
7ce05098 |
1330 | |
fce83e5f |
1331 | # Use the 'many_to_many' relationship to fetch all of the authors for the current |
1332 | # and the 'count' method in DBIx::Class::ResultSet to get a SQL COUNT |
1333 | return $self->authors->count; |
1334 | } |
1335 | |
1336 | Next, let's add a method to return a list of authors for a book to the |
f4e9de4a |
1337 | same F<lib/MyApp/Schema/Result/Book.pm> file: |
fce83e5f |
1338 | |
1339 | =head2 author_list |
7ce05098 |
1340 | |
fce83e5f |
1341 | Return a comma-separated list of authors for the current book |
7ce05098 |
1342 | |
fce83e5f |
1343 | =cut |
7ce05098 |
1344 | |
fce83e5f |
1345 | sub author_list { |
1346 | my ($self) = @_; |
7ce05098 |
1347 | |
1348 | # Loop through all authors for the current book, calling all the 'full_name' |
fce83e5f |
1349 | # Result Class method for each |
1350 | my @names; |
1351 | foreach my $author ($self->authors) { |
1352 | push(@names, $author->full_name); |
1353 | } |
7ce05098 |
1354 | |
fce83e5f |
1355 | return join(', ', @names); |
1356 | } |
1357 | |
ee53cc71 |
1358 | This method loops through each author, using the C<full_name> Result |
f4e9de4a |
1359 | Class method we added to F<lib/MyApp/Schema/Result/Author.pm> in the |
fce83e5f |
1360 | prior section. |
1361 | |
1362 | Using these two methods, we can simplify our TT code. Open |
f4e9de4a |
1363 | F<root/src/books/list.tt2> and update the "Author(s)" table cell to |
fce83e5f |
1364 | match the following: |
1365 | |
1366 | ... |
1367 | <td> |
1368 | [% # Print count and author list using Result Class methods -%] |
1369 | ([% book.author_count | html %]) [% book.author_list | html %] |
1370 | </td> |
1371 | ... |
1372 | |
ee53cc71 |
1373 | Although most of the code we removed comprised comments, the overall |
1374 | effect is dramatic... because our view code is so simple, we don't need |
22fe0f18 |
1375 | huge comments to clue people in to the gist of our code. The view code |
ee53cc71 |
1376 | is now self-documenting and readable enough that you could probably get |
22fe0f18 |
1377 | by with no comments at all. All of the "complex" work is being done in |
ee53cc71 |
1378 | our Result Class methods (and, because we have broken the code into |
1379 | nice, modular chunks, the Result Class code is hardly something you |
f2bbfc36 |
1380 | would call complex). |
fce83e5f |
1381 | |
ee53cc71 |
1382 | As we saw in this section, always strive to keep your view AND |
1383 | controller code as simple as possible by pulling code out into your |
22fe0f18 |
1384 | model objects. Because L<DBIx::Class> can be easily extended in so many |
ee53cc71 |
1385 | ways, it's an excellent to way accomplish this objective. It will make |
1386 | your code cleaner, easier to write, less error-prone, and easier to |
1387 | debug and maintain. |
fce83e5f |
1388 | |
ee53cc71 |
1389 | Before you conclude this section, hit Refresh in your browser... the |
1390 | output should be the same even though the backend code has been trimmed |
1391 | down. |
444d6b27 |
1392 | |
fce83e5f |
1393 | |
24acc5d7 |
1394 | You can jump to the next chapter of the tutorial here: |
1395 | L<Authentication|Catalyst::Manual::Tutorial::05_Authentication> |
1396 | |
1397 | |
d442cc9f |
1398 | =head1 AUTHOR |
1399 | |
1400 | Kennedy Clark, C<hkclark@gmail.com> |
1401 | |
53243324 |
1402 | Feel free to contact the author for any errors or suggestions, but the |
1403 | best way to report issues is via the CPAN RT Bug system at |
bb0999d3 |
1404 | L<https://rt.cpan.org/Public/Dist/Display.html?Name=Catalyst-Manual>. |
53243324 |
1405 | |
bb0999d3 |
1406 | Copyright 2006-2011, Kennedy Clark, under the |
ec3ef4ad |
1407 | Creative Commons Attribution Share-Alike License Version 3.0 |
95674086 |
1408 | (L<http://creativecommons.org/licenses/by-sa/3.0/us/>). |