File Coverage

lib/Class/DBI/Lite/Pager.pm
Criterion Covered Total %
statement 73 78 93.5
branch 27 34 79.4
condition 10 18 55.5
subroutine 16 16 100.0
pod 12 12 100.0
total 138 158 87.3


line stmt bran cond sub pod time code
1              
2             package Class::DBI::Lite::Pager;
3              
4 17     17   98 use strict;
  17         34  
  17         508  
5 17     17   82 use warnings 'all';
  17         33  
  17         14799  
6              
7              
8             sub new
9             {
10 9     9 1 79 my ($class, %args) = @_;
11            
12 9         126 my $s = bless {
13             data_sql => undef,
14             count_sql => undef,
15             sql_args => [ ],
16             where => undef,
17             order_by => undef,
18             class => undef,
19             page_number => 1,
20             page_size => 10,
21             total_items => 0,
22             start_item => 1,
23             stop_item => 0,
24             %args,
25             _fetched_once => 0,
26             }, $class;
27              
28 9         44 ($s->{stop_item}) = sort { $a <=> $b } (
  9         42  
29             $s->page_number * $s->page_size,
30             $s->total_items
31             );
32 9         30 $s->{start_item} = ( $s->page_number - 1 ) * $s->page_size + 1;
33              
34 9         114 return $s;
35             }# end new()
36              
37              
38             # Public read-write properties:
39             sub page_number
40             {
41 145     145 1 288 my $s = shift;
42 145 50       352 if( @_ )
43             {
44 0         0 $s->{page_number} = shift;
45              
46 0         0 $s->{_fetched_once} = 0;
47 0         0 ($s->{stop_item}) = sort { $a <=> $b } (
  0         0  
48             $s->page_number * $s->page_size,
49             $s->total_items
50             );
51 0         0 $s->{start_item} = ( $s->page_number - 1 ) * $s->page_size + 1;
52             }
53             else
54             {
55 145         556 return $s->{page_number};
56             }# end if()
57             }# end page_number()
58              
59              
60             # Public read-only properties:
61 142     142 1 590 sub page_size { shift->{page_size} }
62 73     73 1 362 sub total_items { shift->{total_items} }
63 41     41 1 279 sub total_pages { shift->{total_pages} }
64 24     24 1 152 sub start_item { shift->{start_item} }
65 44     44 1 189 sub stop_item { shift->{stop_item} }
66 58     58 1 1900 sub has_prev { shift->{page_number} > 1 }
67              
68             sub has_next
69             {
70 38     38 1 2404 my $s = shift;
71 38         217 $s->{total_pages} > $s->{page_number};
72             }# end has_next()
73              
74             sub _has_more
75             {
76 22     22   128 my $s = shift;
77            
78 22 100       84 if( $s->{_fetched_once} )
79             {
80 18         95 $s->{page_number} < $s->{total_pages};
81             }
82             else
83             {
84 4         25 $s->{total_pages} >= $s->{page_number};
85             }# end if()
86             }# end _has_more()
87              
88             *items = \&next_page;
89              
90             sub next_page
91             {
92 22     22 1 2482 my $s = shift;
93            
94 22 100       69 return unless $s->_has_more;
95            
96 20 100       83 if( $s->{_fetched_once}++ )
97             {
98 16         34 $s->{page_number}++;
99             }# end if()
100              
101 20         64 ($s->{stop_item}) = sort { $a <=> $b } (
  20         80  
102             $s->page_number * $s->page_size,
103             $s->total_items
104             );
105 20         58 $s->{start_item} = ( $s->page_number - 1 ) * $s->page_size + 1;
106            
107 20         64 my $offset = $s->_offset;
108            
109 20 100       57 if( $s->{data_sql} )
110             {
111 10         27 my $limit = " LIMIT $offset, @{[ $s->{page_size} ]} ";
  10         39  
112 10 50       31 my $order_by = $s->{order_by} ? " ORDER BY $s->{order_by} " : "";
113 10         46 my $sth = $s->{class}->db_Main->prepare( "$s->{data_sql} $order_by $limit" );
114 10         2234 $sth->execute( @{ $s->{sql_args} } );
  10         2343  
115 10         82 return $s->{class}->sth_to_objects( $sth );
116             }
117             else
118             {
119             return $s->{class}->search_where(
120             $s->{where},
121             {
122             order_by => $s->{order_by} || undef,
123 10   50     147 limit => $s->page_size,
124             offset => $offset,
125             }
126             );
127             }# end if()
128             }# end next_page()
129              
130              
131             sub prev_page
132             {
133 20     20 1 2611 my $s = shift;
134            
135 20 50       64 return unless $s->has_prev;
136            
137 20 100       85 $s->{page_number}-- if $s->{_fetched_once}++;
138 20         76 $s->{stop_item} = $s->page_number * $s->page_size;
139 20 50       60 $s->{stop_item} = $s->total_items if $s->stop_item > $s->total_items;
140 20         52 $s->{start_item} = ( $s->page_number - 1 ) * $s->page_size + 1;
141 20 50       67 $s->{start_item} = 0 if $s->{start_item} < 0;
142            
143 20         57 my $offset = $s->_offset;
144            
145 20 100       68 if( $s->{data_sql} )
146             {
147 10         25 my $limit = " LIMIT $offset, @{[ $s->{page_size} ]} ";
  10         35  
148 10         46 my $sth = $s->{class}->db_Main->prepare( "$s->{data_sql} $limit" );
149 10         2129 $sth->execute( @{ $s->{sql_args} } );
  10         1236  
150 10         72 return $s->{class}->sth_to_objects( $sth );
151             }
152             else
153             {
154             return $s->{class}->search_where(
155             $s->{where},
156             {
157             order_by => $s->{order_by} || undef,
158 10   50     55 limit => $s->page_size,
159             offset => $offset,
160             }
161             );
162             }# end if()
163             }# end prev_page()
164              
165              
166             sub navigations
167             {
168 3     3 1 46 my ($s, $padding) = @_;
169            
170 3   50     7 $padding ||= 5;
171            
172             # Wiggle the start and stop out of the data we have:
173 3 100       7 my $start = $s->page_number - $padding > 0
174             ? $s->page_number - $padding
175             : 1;
176 3 100       6 my $stop = $s->page_number + $padding <= $s->total_pages
177             ? $s->page_number + $padding
178             : $s->total_pages;
179            
180             # Now:
181 3 50       10 if( $stop - $start < ( $padding * 2 ) + 1 )
182             {
183             # Need to add more pages:
184 3 100 66     12 if( $start == 1 && $stop < $s->total_pages )
    50 33        
185             {
186 2   66     9 while( ( $stop - $start < ( $padding * 2 ) ) && $stop < $s->total_pages )
187             {
188 10         22 $stop++;
189             }# end while()
190             }
191             elsif( $stop == $s->total_pages && $start > 1 )
192             {
193 1   66     8 while( ( $stop - $start < ( $padding * 2 ) ) && $start > 1 )
194             {
195 5         17 $start--;
196             }# end while()
197             }# end if()
198             }# end if()
199            
200 3         10 return ( $start, $stop );
201             }# end navigations()
202              
203              
204             sub _offset
205             {
206 40     40   75 my $s = shift;
207 40 100       149 $s->{page_number} == 1 ? 0 : ($s->{page_number} - 1) * $s->{page_size};
208             }# end _offset()
209              
210             1;# return true:
211              
212             =pod
213              
214             =head1 NAME
215              
216             Class::DBI::Lite::Pager - Page through your records, easily.
217              
218             =head1 SYNOPSIS
219              
220             =head2 Paged Navigation Through Large Datasets
221              
222             # Say we're on page 1 of a list of all 'Rock' artists:
223             my $pager = app::artist->pager({
224             genre => 'Rock',
225             }, {
226             order_by => 'name ASC',
227             page_number => 1,
228             page_size => 20,
229             });
230              
231             # -------- OR -----------
232             my $pager = app::artist->sql_pager({
233             data_sql => "SELECT * FROM artists WHERE genre = ?",
234             count_sql => "SELECT COUNT(*) FROM artists WHERE genre = ?",
235             sql_args => [ 'Rock' ],
236             }, {
237             page_number => 1,
238             page_size => 20,
239             });
240            
241             # Get the first page of items from the pager:
242             my @artists = $pager->items;
243            
244             # Is the a 'previous' page?:
245             if( $pager->has_prev ) {
246             print "Prev page number is " . ( $pager->page_number - 1 ) . "\n";
247             }
248            
249             # Say where we are in the total scheme of things:
250             print "Page " . $pager->page_number . " of " . $pager->total_pages . "\n";
251             print "Showing items " . $pager->start_item . " through " . $pager->stop_item . " out of " . $pager->total_items . "\n";
252            
253             # Is there a 'next' page?:
254             if( $pager->has_next ) {
255             print "Next page number is " . ( $pager->page_number + 1 ) . "\n";
256             }
257            
258             # Get the 'start' and 'stop' page numbers for a navigation strip with
259             # up to 5 pages before and after the 'current' page:
260             my ($start, $stop) = $pager->navigations( 5 );
261             for( $start..$stop ) {
262             print "Page $_ | ";
263             }
264              
265             =head2 Fetch Huge Datasets in Small Chunks
266              
267             # Fetch 300,000,000 records, 100 records at a time:
268             my $pager = app::citizen->pager({
269             country => 'USA'
270             }, {
271             order_by => 'last_name, first_name',
272             page_size => 100,
273             page_number => 1,
274             });
275             while( my @people = $pager->next_page ) {
276             # We only got 100 people, instead of swamping the
277             # database by asking for 300M records all at once:
278             }
279              
280             =head1 DESCRIPTION
281              
282             Paging through records should be easy. C B it easy.
283              
284             =head1 CAVEAT EMPTOR
285              
286             This has been tested with MySQL 5.x and SQLite. It should work with any database
287             that provides some kind of C construct.
288              
289             To discover the total number of pages and items, 2 queries must be performed:
290              
291             =over 4
292              
293             =item 1 First we do a C
294              
295             =item 2 One or more queries to get the records you've requested.
296              
297             If running 2 queries is going to cause your database server to catch fire, please consider rolling your own pager
298             or finding some other method of doing this.
299              
300             =back
301              
302             =head1 CONSTRUCTOR
303              
304             =head2 new( page_number => 1, page_size => 10 )
305              
306             Returns a new Pager object at the page number and page size specified.
307              
308             =head1 PUBLIC PROPERTIES
309              
310             =head2 page_number
311              
312             Read-write. Sets/gets the page number.
313              
314             =head2 page_size
315              
316             Read only. Returns the page size.
317              
318             =head2 total_pages
319              
320             Read only. Returns the total number of pages in the Pager.
321              
322             =head2 total_items
323              
324             Read only. Returns the total number of records in all the pages combined.
325              
326             =head2 start_item
327              
328             Read only. Returns the index of the first item in this page's records.
329              
330             =head2 stop_item
331              
332             Read only. Returns the index of the last item in this page's records.
333              
334             =head2 has_next
335              
336             Read only. Returns true or false depending on whether there are more pages B the current page.
337              
338             =head2 has_prev
339              
340             Read only. Returns true or false depending on whether there are more pages B the current page.
341              
342             =head1 PUBLIC METHODS
343              
344             =head2 items( )
345              
346             Returns the next page of results. Same as calling C. Purely for syntax alone.
347              
348             =head2 next_page( )
349              
350             Returns the next page of results. If called in list context, returns an array. If
351             called in scalar context, returns a L.
352              
353             If there is not a next page, returns undef.
354              
355             =head2 prev_page( )
356              
357             Returns the previous page of results. If called in list context, returns an array. If
358             called in scalar context, returns a L.
359              
360             If there is not a previous page, returns undef.
361              
362             =head2 navigations( [$padding = 5] )
363              
364             OK - grab a cup of coffee, then come back for the explanation.
365              
366             Ready? Good.
367              
368             Say you have a C<$pager>:
369              
370             my $pager = app::album->pager(undef, {
371             page_size => 10,
372             page_number => 1,
373             });
374              
375             Then you want to make your paging navigation with at least 10 pages shown, and a
376             maximum of 5 pages to either side of the "current" page (like Google).
377              
378             1 2 3 4 5 6 7 8 9 10 11
379              
380             On the first page you I just do:
381              
382             for( 1..10 ) {
383             # print a link to that page.
384             }
385              
386             ...but...when you get toward the middle or off to the end, it gets weird.
387              
388             Tah-Dah!
389              
390             my ($start, $stop) = $pager->navigations( 5 );
391              
392             Now you can simply do:
393              
394             for( $start..$stop ) {
395             # print a link to that page:
396             }
397              
398             B will always do the right thing - will I?
399              
400             So when you're on page 7 it will look like this:
401              
402             2 3 4 5 6 7 8 9 10 11 12
403              
404             Then, if there were 20 pages in your entire resultset, page 20 would look like this:
405              
406             10 11 12 13 14 15 16 17 18 19 20
407              
408             Great, huh?
409              
410             =head1 AUTHOR
411              
412             Copyright John Drago . All rights reserved.
413              
414             =head1 LICENSE
415              
416             This software is B software and may be used and redistributed under the
417             same terms as perl itself.
418              
419             =cut
420