File Coverage

blib/lib/Dancer2/Plugin/DoFile.pm
Criterion Covered Total %
statement 73 235 31.0
branch 20 106 18.8
condition 20 80 25.0
subroutine 9 13 69.2
pod 0 7 0.0
total 122 441 27.6


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::DoFile;
2              
3 1     1   1096511 use strict;
  1         3  
  1         29  
4 1     1   6 use warnings;
  1         2  
  1         36  
5              
6             $Dancer2::Plugin::DoFile::VERSION = '0.14';
7             # ABSTRACT: File-based MVC plugin for Dancer2
8              
9 1     1   545 use Dancer2::Plugin;
  1         13183  
  1         7  
10              
11 1     1   8943 use JSON;
  1         9160  
  1         6  
12 1     1   632 use HTTP::Accept;
  1         3214  
  1         41  
13 1     1   541 use Time::HiRes qw(time);
  1         1452  
  1         6  
14              
15             # Not sure if this is necessary at this point, as the model
16             # Should in general not be dynamically loaded...
17             #has model_loc => (
18             # is => 'rw',
19             # default => sub {'dofiles/models'},
20             #);
21             has controller_loc => (
22             is => 'rw',
23             default => sub {'dofiles/controllers'},
24             );
25             has controller_extension_list => (
26             is => 'rw',
27             default => sub { ['.ctl','.do'] }
28             );
29             has view_loc => (
30             is => 'rw',
31             default => sub {'dofiles/views'},
32             );
33             has view_extension_list => (
34             is => 'rw',
35             default => sub { ['.view','.po'] }
36             );
37              
38             has default_file => (
39             is => 'rw',
40             default => sub {'index'},
41             );
42             has timings => (
43             is => 'rw',
44             default => sub { 0; }
45             );
46              
47             # This is old config syntax and should not be used
48             # Only preserved as temporary backwards compatibility
49             has page_loc => (
50             is => 'rw',
51             default => sub {'dofiles/pages'},
52             );
53             # This list will change over time to remove "do" and "po" in favour of "ctl" (controllers) and "view" (views)
54             has extension_list => (
55             is => 'rw',
56             default => sub { ['.ctl', '.do', '.po', '.view']}
57             );
58              
59             # Old method
60             plugin_keywords 'dofile';
61              
62             # New methods
63             plugin_keywords 'controller';
64             plugin_keywords 'view';
65              
66             my %dofiles;
67             my $acceptext = {
68             '' => '',
69             'text/html' => '.html',
70             'application/json' => '.json'
71             };
72              
73             sub BUILD {
74 1     1 0 90 my $self = shift;
75 1         21 my $settings = $self->config;
76              
77             $settings->{$_} and $self->$_( $settings->{$_} )
78 1   100     84 for qw/ page_loc default_file extension_list controller_loc controller_extension_list view_loc view_extension_list timings /;
79             }
80              
81             sub controller {
82 0     0 0 0 my $plugin = shift;
83 0         0 my $arg = shift;
84 0         0 my %opts = @_;
85              
86 0         0 my $app = $plugin->app;
87 0         0 my $settings = $app->settings;
88 0         0 my $method = $app->request->method;
89 0         0 my $pageroot = $settings->{appdir};
90 0 0       0 if ($pageroot !~ /\/$/) {
91 0         0 $pageroot .= '/';
92             }
93 0         0 $pageroot .= $plugin->controller_loc;
94              
95 0   0     0 my $path = $arg || $app->request->path;
96              
97             # If any one of these returns content then we stop processing any more of them
98             # Content is defined as an array ref (it's an Obj2HTML array), a hashref with a "content" element, or a scalar (assumed HTML string - it's not checked!)
99             # This can lead to some interesting results if someone doesn't explicitly return undef when they want to fall through to the next file
100             # as perl will return the last evaluated value, which would be intepretted as content according to the above rules
101              
102 0   0     0 my $stash = $opts{stash} || {};
103              
104             # Safety first...
105 0         0 $path =~ s|/$|'/'.$plugin->default_file|e;
  0         0  
106 0         0 $path =~ s|^/+||;
107 0         0 $path =~ s|\.\./||g;
108 0         0 $path =~ s|~||g;
109              
110 0 0       0 if (!$path) { $path = $plugin->default_file; }
  0         0  
111 0 0       0 if (-d $pageroot."/$path") {
112 0 0       0 if ($path !~ /\/$/) {
113 0         0 $path .= '/'.$plugin->default_file;
114             } else {
115             return {
116 0         0 url => "/$path/",
117             redirect => 1,
118             done => 1
119             };
120             }
121             }
122              
123 0 0       0 if (!defined $stash->{dofiles_executed}) { $stash->{dofiles_executed} = 0; }
  0         0  
124             OUTER:
125 0         0 foreach my $ext (@{$plugin->controller_extension_list}) {
  0         0  
126 0         0 foreach my $m ('', "-$method", '-ANY') {
127 0         0 my $cururl = $path;
128 0         0 my @path = ();
129              
130             # This iterates back through the path to find the closest FILE downstream, using the rest of the url as a "path" argument
131 0   0     0 while (!-f $pageroot.'/'.$cururl.$m.$ext && $cururl =~ s/\/([^\/]*)$//) {
132 0 0       0 if ($1) { unshift(@path, $1); }
  0         0  
133             }
134              
135             # "Do" the file
136 0 0       0 if ($cururl) {
137 0         0 my $result;
138 0 0       0 if (defined $dofiles{$pageroot.'/'.$cururl.$m.$ext}) {
    0          
139 0         0 $stash->{dofiles}->{$cururl.$m.$ext} = { type => 'controller', origin => 'cache', order => $stash->{dofiles_executed}++ };
140 0 0       0 if ($plugin->timings) {
141 0         0 my $start = time();
142 0         0 $result = $dofiles{$pageroot.'/'.$cururl.$m.$ext}->({path => \@path, this_url => $cururl, dofile_plugin => $plugin, stash => $stash, env => $app->request->env});
143 0         0 $stash->{dofiles}->{$cururl.$m.$ext}->{time} = time() - $start;
144             } else {
145 0         0 $result = $dofiles{$pageroot.'/'.$cururl.$m.$ext}->({path => \@path, this_url => $cururl, dofile_plugin => $plugin, stash => $stash, env => $app->request->env});
146             }
147              
148             } elsif (-f $pageroot.'/'.$cururl.$m.$ext) {
149 0         0 $stash->{dofiles}->{$cururl.$m.$ext} = { type => 'controller', origin => 'file', order => $stash->{dofiles_executed}++ };
150              
151 0         0 our $args = { path => \@path, this_url => $cururl, dofile_plugin => $plugin, stash => $stash, env => $app->request->env };
152              
153 0         0 $result = do($pageroot.'/'.$cururl.$m.$ext);
154 0 0 0     0 if ($@ || $!) { $plugin->app->log( error => "Error processing $pageroot / $cururl.$m.$ext: $@ $!\n"); }
  0         0  
155 0 0       0 if (ref $result eq 'CODE') {
156 0         0 $stash->{dofiles}->{$cururl.$m.$ext}->{cached} = 1;
157 0         0 $dofiles{$pageroot.'/'.$cururl.$m.$ext} = $result;
158 0 0       0 if ($plugin->timings) {
159 0         0 my $start = time();
160 0         0 $result = $result->($args);
161 0         0 $stash->{dofiles}->{$cururl.$m.$ext}->{time} = time() - $start;
162             } else {
163 0         0 $result = $result->($args);
164             }
165             }
166             }
167              
168 0 0 0     0 if (defined $result && ref $result eq 'HASH') {
    0 0        
    0          
169 0         0 merge($stash, $result);
170 0 0 0     0 if (defined $result->{url} && !defined $result->{done}) {
171 0         0 $path = $result->{url};
172 0         0 next OUTER;
173             }
174 0 0 0     0 if (defined $result->{view} && $result->{done}) {
    0 0        
      0        
175 0         0 $stash->{dofiles}->{$cururl.$m.$ext}->{last} = 1;
176 0         0 $stash->{'controller_result'} = $result;
177 0         0 return $plugin->view($result->{view}, path => \@path, this_url => $cururl, dofile_plugin => $plugin, stash => $stash, env => $app->request->env );
178              
179             } elsif (defined $result->{content} || $result->{url} || $result->{done}) {
180 0         0 $stash->{dofiles}->{$cururl.$m.$ext}->{last} = 1;
181 0         0 return $result;
182             }
183             # Move on to the next file
184              
185             } elsif (ref $result eq 'ARRAY') {
186 0         0 $stash->{dofiles}->{$cururl.$m.$ext}->{last} = 1;
187 0         0 return { content => $result };
188              
189             } elsif (!ref $result && $result) {
190             # do we assume this is HTML? Or a file to use in templating? Who knows!
191 0         0 $stash->{dofiles}->{$cururl.$m.$ext}->{last} = 1;
192 0         0 return { content => $result };
193              
194             }
195             }
196             }
197             }
198              
199             # If we got here we didn't find a controller. We should fail over to see if it's just a view on its own (effectively this module or the route acts as the controller)
200 0         0 $opts{stash} = $stash;
201 0 0       0 if ($stash->{view}) {
202 0         0 $opts{'controller_arg'} = $arg;
203 0         0 return $plugin->view($stash->{view}, %opts);
204             } else {
205 0         0 return $plugin->view($arg, %opts);
206             }
207              
208             }
209              
210             sub view {
211 0     0 0 0 my $plugin = shift;
212 0         0 my $arg = shift;
213 0         0 my %opts = @_;
214              
215 0         0 my $app = $plugin->app;
216 0         0 my $settings = $app->settings;
217 0         0 my $method = $app->request->method;
218              
219 0         0 my $accept = HTTP::Accept->new( $app->request->accept )->values();
220 0         0 push @{$accept}, '';
  0         0  
221              
222 0         0 my $pageroot = $settings->{appdir};
223 0 0       0 if ($pageroot !~ /\/$/) {
224 0         0 $pageroot .= '/';
225             }
226 0         0 $pageroot .= $plugin->view_loc;
227              
228 0   0     0 my $path = $arg || $app->request->path;
229              
230             # If any one of these returns content then we stop processing any more of them
231             # Content is defined as an array ref (it's an Obj2HTML array), a hashref with a "content" element, or a scalar (assumed HTML string - it's not checked!)
232             # This can lead to some interesting results if someone doesn't explicitly return undef when they want to fall through to the next file
233             # as perl will return the last evaluated value, which would be intepretted as content according to the above rules
234              
235 0   0     0 my $stash = $opts{stash} || {};
236              
237             # Safety first...
238 0         0 $path =~ s|/$|'/'.$plugin->default_file|e;
  0         0  
239 0         0 $path =~ s|^/+||;
240 0         0 $path =~ s|\.\./||g;
241 0         0 $path =~ s|~||g;
242              
243 0 0       0 if (!$path) { $path = $plugin->default_file; }
  0         0  
244 0 0       0 if (-d $pageroot."/$path") {
245 0 0       0 if ($path !~ /\/$/) {
246 0         0 $path .= '/'.$plugin->default_file;
247             } else {
248             return {
249 0         0 url => "/$path/",
250             redirect => 1,
251             done => 1
252             };
253             }
254             }
255             OUTER:
256 0         0 foreach my $ext (@{$plugin->view_extension_list}) {
  0         0  
257 0         0 foreach my $fmt (@{$accept}) {
  0         0  
258 0 0       0 if (defined $acceptext->{$fmt}) {
259 0         0 foreach my $m ('', "-$method", '-ANY') {
260 0         0 my $cururl = $path;
261 0         0 my @path = ();
262             # This iterates back through the path to find the closest FILE downstream, using the rest of the url as a "path" argument
263 0   0     0 while (!-f $pageroot.'/'.$cururl.$m.$acceptext->{$fmt}.$ext && $cururl =~ s/\/([^\/]*)$//) {
264 0 0       0 if ($1) { unshift(@path, $1); }
  0         0  
265             }
266              
267             # "Do" the file
268 0 0       0 if ($cururl) {
269 0         0 my $result;
270 0 0       0 if (defined $dofiles{$pageroot.'/'.$cururl.$m.$acceptext->{$fmt}.$ext}) {
    0          
271 0         0 $stash->{dofiles}->{$cururl.$m.$acceptext->{$fmt}.$ext} = { type => 'view', origin => 'cache', order => $stash->{dofiles_executed}++ };
272 0 0       0 if ($plugin->timings) {
273 0         0 my $start = time();
274 0         0 $result = $dofiles{$pageroot.'/'.$cururl.$m.$acceptext->{$fmt}.$ext}->({path => \@path, this_url => $cururl, dofile_plugin => $plugin, stash => $stash, env => $app->request->env});
275 0         0 $stash->{dofiles}->{$cururl.$m.$acceptext->{$fmt}.$ext}->{time} = time() - $start;
276             } else {
277 0         0 $result = $dofiles{$pageroot.'/'.$cururl.$m.$acceptext->{$fmt}.$ext}->({path => \@path, this_url => $cururl, dofile_plugin => $plugin, stash => $stash, env => $app->request->env});
278             }
279              
280             } elsif (-f $pageroot.'/'.$cururl.$m.$acceptext->{$fmt}.$ext) {
281 0         0 $stash->{dofiles}->{$cururl.$m.$acceptext->{$fmt}.$ext} = { type => 'view', origin => 'file', order => $stash->{dofiles_executed}++ };
282              
283 0         0 our $args = { path => \@path, this_url => $cururl, dofile_plugin => $plugin, stash => $stash, env => $app->request->env };
284              
285 0         0 $result = do($pageroot.'/'.$cururl.$m.$acceptext->{$fmt}.$ext);
286 0 0 0     0 if ($@ || $!) { $plugin->app->log( error => "Error processing $pageroot / $cururl.$m.$acceptext->{$fmt}.$ext: $@ $!\n"); }
  0         0  
287 0 0       0 if (ref $result eq 'CODE') {
288 0         0 $stash->{dofiles}->{$cururl.$m.$acceptext->{$fmt}.$ext}->{cached} = 1;
289 0         0 $dofiles{$pageroot.'/'.$cururl.$m.$acceptext->{$fmt}.$ext} = $result;
290 0 0       0 if ($plugin->timings) {
291 0         0 my $start = time();
292 0         0 $result = $result->($args);
293 0         0 $stash->{dofiles}->{$cururl.$m.$acceptext->{$fmt}.$ext}->{time} = time() - $start;
294             } else {
295 0         0 $result = $result->($args);
296             }
297             }
298             }
299              
300 0 0 0     0 if (defined $result && ref $result eq 'HASH') {
    0 0        
    0          
301 0         0 $result->{'content-type'} = $acceptext->{$fmt};
302 0         0 $stash->{dofiles}->{$cururl.$m.$acceptext->{$fmt}.$ext}->{last} = 1;
303 0         0 return $result;
304              
305             } elsif (ref $result eq 'ARRAY') {
306 0         0 $stash->{dofiles}->{$cururl.$m.$acceptext->{$fmt}.$ext}->{last} = 1;
307 0         0 return { 'content-type' => $acceptext->{$fmt}, content => $result };
308              
309             } elsif (!ref $result && $result) {
310             # do we assume this is HTML? Or a file to use in templating? Who knows!
311 0         0 $stash->{dofiles}->{$cururl.$m.$acceptext->{$fmt}.$ext}->{last} = 1;
312 0         0 return { 'content-type' => $acceptext->{$fmt}, content => $result };
313              
314             }
315             }
316             }
317             }
318             }
319             }
320              
321             # If we got here we didn't find a do file that returned some content
322 0         0 return { status => 404 };
323              
324             }
325              
326              
327             # Backward compatibility
328             sub dofile {
329 7     7 0 212726 my $plugin = shift;
330 7         21 my $arg = shift;
331 7         42 my %opts = @_;
332              
333 7         32 my $app = $plugin->app;
334 7         23 my $settings = $app->settings;
335 7         468 my $method = $app->request->method;
336 7         52 my $pageroot = $settings->{appdir};
337 7 50       33 if ($pageroot !~ /\/$/) {
338 7         19 $pageroot .= '/';
339             }
340 7         42 $pageroot .= $plugin->page_loc;
341              
342 7   33     40 my $path = $arg || $app->request->path;
343              
344             # If any one of these returns content then we stop processing any more of them
345             # Content is defined as an array ref (it's an Obj2HTML array), a hashref with a "content" element, or a scalar (assumed HTML string - it's not checked!)
346             # This can lead to some interesting results if someone doesn't explicitly return undef when they want to fall through to the next file
347             # as perl will return the last evaluated value, which would be intepretted as content according to the above rules
348              
349 7   50     77 my $stash = $opts{stash} || {};
350              
351             # Safety first...
352 7         20 $path =~ s|/$|'/'.$plugin->default_file|e;
  1         6  
353 7         34 $path =~ s|^/+||;
354 7         21 $path =~ s|\.\./||g;
355 7         13 $path =~ s|~||g;
356              
357 7 50       19 if (!$path) { $path = $plugin->default_file; }
  0         0  
358 7 50       313 if (-d $pageroot."/$path") {
359 0 0       0 if ($path =~ /\/$/) {
360 0         0 $path .= '/'.$plugin->default_file;
361             } else {
362             return {
363 0         0 url => "/$path/",
364             redirect => 1,
365             done => 1
366             };
367             }
368             }
369              
370 7 50       33 if (!defined $stash->{dofiles_executed}) { $stash->{dofiles_executed} = 0; }
  7         21  
371             OUTER:
372 7         14 foreach my $ext (@{$plugin->extension_list}) {
  7         35  
373 24         51 foreach my $m ('', "-$method") {
374 42         77 my $cururl = $path;
375 42         66 my @path = ();
376              
377             # This iterates back through the path to find the closest FILE downstream, using the rest of the url as a "path" argument
378 42   100     827 while (!-f $pageroot.'/'.$cururl.$m.$ext && $cururl =~ s/\/([^\/]*)$//) {
379 13 50       48 if ($1) { unshift(@path, $1); }
  13         260  
380             }
381              
382             # "Do" the file
383 42 50       115 if ($cururl) {
384 42         63 my $result;
385 42 50       531 if (defined $dofiles{$pageroot.'/'.$cururl.$m.$ext}) {
    100          
386 0         0 $stash->{dofiles}->{$cururl.$m.$ext} = { origin => 'cache', order => $stash->{dofiles_executed}++ };
387 0         0 $result = $dofiles{$pageroot.'/'.$cururl.$m.$ext}->({path => \@path, this_url => $cururl, dofile_plugin => $plugin, stash => $stash, env => $app->request->env});
388              
389             } elsif (-f $pageroot.'/'.$cururl.$m.$ext) {
390 10         85 $stash->{dofiles}->{$cururl.$m.$ext} = { origin => 'file', order => $stash->{dofiles_executed}++ };
391              
392 10         68 our $args = { path => \@path, this_url => $cururl, dofile_plugin => $plugin, stash => $stash, env => $app->request->env };
393              
394 10         3949 $result = do($pageroot.'/'.$cururl.$m.$ext);
395 10 50 33     237 if ($@ || $!) { $plugin->app->log( error => "Error processing $pageroot / $cururl.$m.$ext: $@ $!\n"); }
  0         0  
396 10 50       35 if (ref $result eq 'CODE') {
397 0         0 $stash->{dofiles}->{$cururl.$m.$ext}->{cached} = 1;
398 0         0 $dofiles{$pageroot.'/'.$cururl.$m.$ext} = $result;
399 0         0 $result = $result->($args);
400             }
401             }
402              
403 42 100 66     272 if (defined $result && ref $result eq 'HASH') {
    50 33        
    50          
404 10 100 100     36 if (defined $result->{url} && !defined $result->{done}) {
405 1         3 $path = $result->{url};
406 1         6 next OUTER;
407             }
408 9 100 100     42 if (defined $result->{content} || $result->{url} || $result->{done}) {
      66        
409 6         24 $stash->{dofiles}->{$cururl.$m.$ext}->{last} = 1;
410 6         53 return $result;
411             } else {
412 3         10 merge($stash, $result);
413             }
414             # Move on to the next file
415              
416             } elsif (ref $result eq 'ARRAY') {
417 0         0 $stash->{dofiles}->{$cururl.$m.$ext}->{last} = 1;
418 0         0 return { content => $result };
419              
420             } elsif (!ref $result && $result) {
421             # do we assume this is HTML? Or a file to use in templating? Who knows!
422 0         0 $stash->{dofiles}->{$cururl.$m.$ext}->{last} = 1;
423 0         0 return { content => $result };
424              
425             }
426             }
427             }
428             }
429              
430             # If we got here we didn't find a do file that returned some content
431 1         10 return { status => 404 };
432             }
433              
434             sub view_pathname {
435 0     0 0 0 my ( $self, $view ) = @_;
436 0         0 return path($view);
437             }
438             sub layout_pathname {
439 0     0 0 0 my ( $self, $layout ) = @_;
440 0         0 return $layout;
441             }
442              
443             # Why not use Hash::Merge? I need to merge the result into the stash hashref, not
444             # clone or make a new hashref, so that the dofile stats can be accumulated (in
445             # $stash->{dofiles})
446             sub merge {
447 3     3 0 7 my $src = shift;
448 3         5 my $dup = shift;
449 3         7 foreach my $k (keys %{$dup}) {
  3         11  
450 3 50 33     12 if (ref $dup->{$k} eq 'HASH' && $src->{$k} && ref $src->{$k} eq 'HASH') {
      0        
451 0         0 merge($src->{$k}, $dup->{$k});
452             } else {
453 3         16 $src->{$k} = $dup->{$k};
454             }
455             }
456             }
457              
458             1;
459              
460             __END__
461              
462             =pod
463              
464             =head1 NAME
465              
466             Dancer2::Plugin::DoFile - A file based MVC style plugin for Dancer2
467              
468             =head1 SYNOPSYS
469              
470             In your config.yml
471              
472             plugins:
473             DoFile:
474             controller_loc: 'dofiles/controllers'
475             controller_extension_list: ['.ctl','.do']
476             view_loc: 'dofiles/views'
477             view_extension_list: ['.view','.po']
478             default_file: "index"
479              
480             Make sure you have created the directory used for the locations in your dancer application root.
481              
482             Within a route in dancer2:
483              
484             my $result = controller 'path/to/file'
485              
486             You must not include the extension of the file as part of the path, as this will
487             be added per the settings.
488              
489             An example route in Dancer2, not using HTML::Obj2HTML (the controller returns the
490             layout and tokens directly):
491              
492             get 'dashboard' => sub {
493             my $self = shift;
494             my $result = controller 'dashboards/user-dashboard';
495             return template $result->{layout} => $result->{tokens};
496             }
497              
498             An example default route that will search for controllers (or views) based on the
499             URI requested, and some handling of other controller return keys:
500              
501             prefix '/';
502             any qr{.*} => sub {
503             my $self = shift;
504              
505             my $result = controller undef; # Not specifying the controller to use will use the URI to guess
506              
507             # My controller might return all manner of different things; this is an example:
508             if ($result && ref $result eq "HASH") {
509             if (defined $result->{url}) {
510             if (defined $result->{redirect} && $result->{redirect} eq "forward") {
511             return forward $result->{url};
512             } else {
513             return redirect $result->{url};
514             }
515             }
516             if (defined $result->{status}) {
517             status $result->{status};
518             }
519             if (defined $result->{template}) {
520             set layout => $result->{template};
521             }
522             if (defined $result->{headers}) {
523             headers %{$result->{headers}};
524             }
525             if (defined $result->{content}) {
526             return template $result->{content}, $result->{tokens};
527             }
528             };
529             };
530              
531              
532             When the 1st parameter to 'controller' is undef it'll use the request URI to work
533             out what the file(s) to execute are.
534              
535             =head1 DESCRIPTION
536              
537             DoFile is a way of automatically pulling multiple perl files to execute as a way
538             to simplify routing complexity in Dancer2 for very large applications. In
539             particular it was designed to split out larger controllers into logical partitions
540             based on heirarchy of files compared to what's being requested.
541              
542             The magic will look through your filesystem for files to 'do' (execute), and
543             there may be several.
544              
545             An added benefit of using DoFile is it's ability to execute multiple files per
546             request, effectively allowing you to split controllers into sub-parts. For example,
547             you might have a "DoFile" that is always executed for /some/uri, and another
548             for POST or GET, and even another for the fact you hade /some too.
549              
550             =head2 File Search Ordering
551              
552             When presented with the URI C<path/to/file> DoFile will begin searching for
553             files that can be executed for this request, until it finds one that returns
554             something that looks like content, a URL or is told you're done, when it stops.
555              
556             Files are searched:
557              
558             =over 4
559              
560             =item * By extension
561              
562             The default extensions .ctl and .view are checked (.do and .po are legacy extensions),
563             unless defined in your config.yml. The intention here is that .do files contain
564             controller code and don't typically return content, but may return redirects. After
565             .do files have been executed, .view files are executed. These are expected to return
566             content.
567              
568             You can define as many extensions as you like. You could, for example have:
569             C<['.init','.do','.view','.final']>
570              
571             =item * Root/HTTP request method
572              
573             For each extension, first the "root" file C<file.ext> is tested, then a file
574             that matches C<file-METHOD.ext> is tested (where METHOD is the HTTP request
575             method for this request, .ext is the extension). Finally C<file-ANY.ext> is
576             checked.
577              
578             =item * Iterating up the directory tree
579              
580             If your call to C<path/to/file> results in a miss for C<path/to/file.ctl>, DoFile
581             will then test for C<path/to.ctl> and finally C<path.ctl> before moving on to
582             C<path/to/file-METHOD.ctl>
583              
584             Once DoFile has found one it will not transcend the directory tree any further.
585             Therefore defining C<path/to/file.ctl> and C<path/to.ctl> will not result in
586             both being executed for the URI C<path/to/file> - only the first will be
587             executed.
588              
589             =back
590              
591             If you define files like so:
592              
593             path.do
594             path/
595             to.view
596             to/
597             file-POST.do
598              
599             A POST to the URI C<path/to/file> will execute C<path.do>, then
600             C<path/to/file-POST.do> and finally C<path/to.view>.
601              
602             =head2 Arguments to the executed files
603              
604             During execution of the file a hashref called $args is available that contains
605             some important things.
606              
607             If the executed file returns a coderef, the coderef is executed with this same
608             hashref as the only argument.
609              
610             =over 4
611              
612             =item * path (arrayref)
613              
614             Anything that appears after the currently executing file on the URI. For example
615             if I request C</path/to/file> and DoFile is executing C<path-POST.do>, the
616             C<path> element will contain ['to','file']
617              
618             =item * this_url (string)
619              
620             The currently executing file without any extension. In the above example this
621             would be C<path>.
622              
623             =item * stash (hashref)
624              
625             The stash can be initially passed from the router:
626              
627             dofile 'path/to/file', stash => { option => 1 }
628              
629             The stash can be read/written to from each file that executes:
630              
631             if ($args->{stash}->{option} == 1) {
632             $args->{stash}->{anotheroption} = 2;
633             }
634              
635             Or if the file being executed returns a hashref that does not contain any of
636             the elements C<contents>, C<url> or C<done> (see below), it's merged into the
637             stash automatically for passing on to the next file to be executed
638              
639             The stash is used to pass internal state down the file chain.
640              
641             =item * dofile_plugin (object)
642              
643             Just in case the file being executed wants to mess about with Dancer2 or
644             the plugin's internals.
645              
646             =back
647              
648             =head2 How DoFile interprets individual executed files response
649              
650             The result (returned value) of each file is checked; if something is returned
651             DoFile will inspect the value to determine what to do next.
652              
653             =head3 Coderef (anonymous sub)
654              
655             You can return a coderef; this will be cached within the plugin and the file
656             will not be checked again, but the coderef will be executed each time that
657             "file" is requested. Generally whether the file should be checked or not is
658             left up to the application (e.g. C<plackup -R ./ -r ...>).
659              
660             In the case a coderef is used, when the code is executed it is passed one
661             argument, a hashref, which is the stash. This saves needing to import the stash
662             from within the code of the file.
663              
664             The return of the coderef will be evaluated exactly as below.
665              
666             =head3 Internal Redirects
667              
668             If a hashref is returned it's checked for a C<url> element but NO C<done>
669             element. In this case, the DoFile restarts from the begining using the new URL.
670             This is a method for internally redirecting. For example, returning:
671              
672             {
673             url => "account/login"
674             }
675              
676             Will cause DoFile to start over with the new URI C<account/login>, without
677             processing any more files from the old URI. The stash is preserved.
678              
679             =head3 Content
680              
681             If a scalar or arrayref is returned, it's wrapped into a hashref into the
682             C<contents> element and sent back to the router.
683              
684             If a hashref is returned and contains a C<contents> element, no more files will
685             be processed. The entire hashref is returned to the router. NB: the
686             C<contents> element must contain something that evals to true, else it's
687             considered not there.
688              
689             =head3 Done
690              
691             If a hashref is returned and there is a C<done> element that evals to a true
692             value, DoFile will stop processing files and return the returned hashref to
693             the router.
694              
695             =head3 Continue
696              
697             If a hashref is returned and there is no C<url>, C<content> or C<done> element
698             then the contents of the hasref is combined with the stash and DoFile will look
699             for the next file.
700              
701             If nothing is returned at all, DoFile will continue with the next file.
702              
703             =head2 What the router gets back
704              
705             DoFile will always return a hashref, even if the files being executed do not
706             return a hashref. This hashref may have anything, but the recommended design
707             is to return one of the following:
708              
709             =over 4
710              
711             =item * A C<contents> element
712              
713             The implication is that you've had the web page to be served back. Note that
714             DoFile doesn't care if this is a scalar string or an arrayref. This Plugin
715             was designed to work with Obj2HTML, so in the case of an arrayref the
716             implication is that Obj2HTML should be asked to convert that to HTML.
717              
718             =item * A C<url> element
719              
720             In this case the router should probably send a 30x response redirecting the
721             client, or perform an internal forward... implementors choice.
722              
723             =item * A C<status> element
724              
725             This could be used to set the status code for returning to the client
726              
727             =back
728              
729             DoFile may however return pretty much whatever you want to handle in your final
730             router code.
731              
732             =head1 DEBUGGING DONE-FILES
733              
734             From 0.14 you can see exactly what DoFile is doing, including the
735             option to see how long each done-file took to execute so that you may improve
736             the performance of your web app.
737              
738             All you need to do is make sure that you pass an initial "stash" to the
739             controller (or view), and DoFiles will store some analysis in a hashref under a
740             key called "dofiles".
741              
742             The key for the hasref is the filename executed. The contents of the hash will
743             contain one or more elements giving some information about what's gone on.
744              
745             my $stash = {};
746             my $result = controller "some/controller", stash => $stash;
747              
748             if (defined $stash->{dofiles}) {
749             my @files = ();
750             foreach my $f (sort { $stash->{dofiles}->{$a}->{order} <=> $stash->{dofiles}->{$b}->{order} } keys %{$stash->{dofiles}}) {
751             my $text = $stash->{dofiles}->{$f}->{order}.": $f [".$stash->{dofiles}->{$f}->{type}."]";
752             if ($stash->{dofiles}->{$f}->{origin} eq "cache") { $text .= " [FROM CACHE]"; }
753             if ($stash->{dofiles}->{$f}->{origin} eq "file") { $text .= " [FROM FILE]"; }
754             if ($stash->{dofiles}->{$f}->{cached}) { $text .= " [WAS CACHED]"; }
755             if ($stash->{dofiles}->{$f}->{last}) { $text .= " [LAST]"; }
756             if ($stash->{dofiles}->{$f}->{time}) { $text .= " ".int($stash->{dofiles}->{$f}->{time}*1000)."ms"; }
757             # NB: For {time} to be defined you need to add an extra line to your config - see below
758             push @files, $text;
759             }
760             # Do something with your @files.
761             }
762              
763             To switch on timing of your done-files, in your config.yml
764              
765             plugins:
766             DoFile:
767             timings: 1
768              
769             It's possible in the future the {dofiles} hashref will become an arrayref, as it's
770             possible to mess things up by redirecting a page to itself (if for example you need
771             to restart processing of passed data).
772              
773             =head1 EXAMPLES
774              
775             As noted, what's returned from a DoFile can contain anything. That gives you
776             the opportunity to do pretty much whatever you want with what's returned.
777              
778             =head1 AUTHOR
779              
780             Pero Moretti
781              
782             =head1 COPYRIGHT AND LICENSE
783              
784             This software is copyright (c) 2022 by Pero Moretti.
785              
786             This is free software; you can redistribute it and/or modify it under
787             the same terms as the Perl 5 programming language system itself.