improved cgi engine docs
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Engine.pm
CommitLineData
fc7ec1d9 1package Catalyst::Engine;
2
3use strict;
4use base qw/Class::Data::Inheritable Class::Accessor::Fast/;
5use UNIVERSAL::require;
fc7ec1d9 6use Data::Dumper;
7use HTML::Entities;
8use HTTP::Headers;
9use Time::HiRes qw/gettimeofday tv_interval/;
10use Catalyst::Request;
11use Catalyst::Response;
12
13require Module::Pluggable::Fast;
14
15$Data::Dumper::Terse = 1;
16
17__PACKAGE__->mk_classdata($_) for qw/actions components/;
18__PACKAGE__->mk_accessors(qw/request response/);
19
20__PACKAGE__->actions(
21 { plain => {}, regex => {}, compiled => {}, reverse => {} } );
22
23*comp = \&component;
24*req = \&request;
25*res = \&response;
26
27our $COUNT = 1;
28our $START = time;
29
30=head1 NAME
31
32Catalyst::Engine - The Catalyst Engine
33
34=head1 SYNOPSIS
35
36See L<Catalyst>.
37
38=head1 DESCRIPTION
39
40=head2 METHODS
41
42=head3 action
43
44Add one or more actions.
45
46 $c->action( '!foo' => sub { $_[1]->res->output('Foo!') } );
47
48Get an action's class and coderef.
49
50 my ($class, $code) = @{ $c->action('foo') };
51
52Get a list of available actions.
53
54 my @actions = $c->action;
55
56It also automatically calls setup() if needed.
57
58See L<Catalyst::Manual::Intro> for more informations about actions.
59
60=cut
61
62sub action {
63 my $self = shift;
64 $self->setup unless $self->components;
65 $self->actions( {} ) unless $self->actions;
66 my $action;
67 $_[1] ? ( $action = {@_} ) : ( $action = shift );
68 if ( ref $action eq 'HASH' ) {
69 while ( my ( $name, $code ) = each %$action ) {
7833fdfc 70 my $class = caller(0);
fc7ec1d9 71 if ( $name =~ /^\/(.*)\/$/ ) {
72 my $regex = $1;
73 $self->actions->{compiled}->{qr/$regex/} = $name;
74 $self->actions->{regex}->{$name} = [ $class, $code ];
75 }
76 elsif ( $name =~ /^\?(.*)$/ ) {
77 $name = $1;
7833fdfc 78 $name = _prefix( $class, $name );
fc7ec1d9 79 $self->actions->{plain}->{$name} = [ $class, $code ];
80 }
81 elsif ( $name =~ /^\!\?(.*)$/ ) {
82 $name = $1;
7833fdfc 83 $name = _prefix( $class, $name );
fc7ec1d9 84 $name = "\!$name";
85 $self->actions->{plain}->{$name} = [ $class, $code ];
86 }
87 else { $self->actions->{plain}->{$name} = [ $class, $code ] }
88 $self->actions->{reverse}->{"$code"} = $name;
7833fdfc 89 $self->log->debug(qq/"$class" defined "$name" as "$code"/)
fc7ec1d9 90 if $self->debug;
91 }
92 }
93 elsif ($action) {
94 if ( my $p = $self->actions->{plain}->{$action} ) { return [$p] }
95 elsif ( my $r = $self->actions->{regex}->{$action} ) { return [$r] }
96 else {
97 while ( my ( $regex, $name ) =
98 each %{ $self->actions->{compiled} } )
99 {
100 if ( $action =~ $regex ) {
101 my @snippets;
102 for my $i ( 1 .. 9 ) {
103 no strict 'refs';
104 last unless ${$i};
105 push @snippets, ${$i};
106 }
7e5adedd 107 return [ $self->actions->{regex}->{$name},
108 $name, \@snippets ];
fc7ec1d9 109 }
110 }
111 }
112 return 0;
113 }
114 else {
115 return (
116 keys %{ $self->actions->{plain} },
117 keys %{ $self->actions->{regex} }
118 );
119 }
120}
121
122=head3 benchmark
123
124Takes a coderef with arguments and returns elapsed time as float.
125
126 my ( $elapsed, $status ) = $c->benchmark( sub { return 1 } );
127 $c->log->info( sprintf "Processing took %f seconds", $elapsed );
128
129=cut
130
131sub benchmark {
132 my $c = shift;
133 my $code = shift;
134 my $time = [gettimeofday];
135 my @return = &$code(@_);
136 my $elapsed = tv_interval $time;
137 return wantarray ? ( $elapsed, @return ) : $elapsed;
138}
139
140=head3 component (comp)
141
142Get a component object by name.
143
144 $c->comp('MyApp::Model::MyModel')->do_stuff;
145
146Regex search for a component.
147
148 $c->comp('mymodel')->do_stuff;
149
150=cut
151
152sub component {
153 my ( $c, $name ) = @_;
154 if ( my $component = $c->components->{$name} ) {
155 return $component;
156 }
157 else {
158 for my $component ( keys %{ $c->components } ) {
159 return $c->components->{$component} if $component =~ /$name/i;
160 }
161 }
162}
163
164=head3 errors
165
166Returns an arrayref containing errors messages.
167
168 my @errors = @{ $c->errors };
169
170Add a new error.
171
172 $c->errors('Something bad happened');
173
174=cut
175
176sub errors {
177 my $c = shift;
178 my $errors = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
179 push @{ $c->{errors} }, @$errors;
180 return $c->{errors};
181}
182
183=head3 finalize
184
185Finalize request.
186
187=cut
188
189sub finalize {
190 my $c = shift;
191 if ( !$c->res->output || $#{ $c->errors } >= 0 ) {
192 $c->res->headers->content_type('text/html');
193 my $name = $c->config->{name} || 'Catalyst Application';
194 my ( $title, $errors, $infos );
195 if ( $c->debug ) {
196 $errors = join '<br/>', @{ $c->errors };
197 $errors ||= 'No output';
198 $title = $name = "$name on Catalyst $Catalyst::VERSION";
199 my $req = encode_entities Dumper $c->req;
200 my $res = encode_entities Dumper $c->res;
201 my $stash = encode_entities Dumper $c->stash;
202 $infos = <<"";
203<br/>
204<b><u>Request</u></b><br/>
205<pre>$req</pre>
206<b><u>Response</u></b><br/>
207<pre>$res</pre>
208<b><u>Stash</u></b><br/>
209<pre>$stash</pre>
210
211 }
212 else {
213 $title = $name;
214 $errors = '';
215 $infos = <<"";
216<pre>
217(en) Please come back later
218(de) Bitte versuchen sie es spaeter nocheinmal
219(nl) Gelieve te komen later terug
220(no) Vennligst prov igjen senere
221(fr) Veuillez revenir plus tard
222(es) Vuelto por favor mas adelante
223(pt) Voltado por favor mais tarde
224(it) Ritornato prego più successivamente
225</pre>
226
227 $name = '';
228 }
229 $c->res->{output} = <<"";
230<html>
231 <head>
232 <title>$title</title>
233 <style type="text/css">
234 body {
235 font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
236 Tahoma, Arial, helvetica, sans-serif;
237 color: #ddd;
238 background-color: #eee;
239 margin: 0px;
240 padding: 0px;
241 }
242 div.box {
243 background-color: #ccc;
244 border: 1px solid #aaa;
245 padding: 4px;
246 margin: 10px;
247 -moz-border-radius: 10px;
248 }
249 div.errors {
250 background-color: #977;
251 border: 1px solid #755;
252 padding: 8px;
253 margin: 4px;
254 margin-bottom: 10px;
255 -moz-border-radius: 10px;
256 }
257 div.infos {
258 background-color: #797;
259 border: 1px solid #575;
260 padding: 8px;
261 margin: 4px;
262 margin-bottom: 10px;
263 -moz-border-radius: 10px;
264 }
265 div.name {
266 background-color: #779;
267 border: 1px solid #557;
268 padding: 8px;
269 margin: 4px;
270 -moz-border-radius: 10px;
271 }
272 </style>
273 </head>
274 <body>
275 <div class="box">
276 <div class="errors">$errors</div>
277 <div class="infos">$infos</div>
278 <div class="name">$name</div>
279 </div>
280 </body>
281</html>
282
283 }
284 if ( my $location = $c->res->redirect ) {
285 $c->log->debug(qq/Redirecting to "$location"/) if $c->debug;
286 $c->res->headers->header( Location => $location );
287 $c->res->status(302);
288 }
289 $c->res->headers->content_length( length $c->res->output );
290 my $status = $c->finalize_headers;
291 $c->finalize_output;
292 return $status;
293}
294
295=head3 finalize_headers
296
297Finalize headers.
298
299=cut
300
301sub finalize_headers { }
302
303=head3 finalize_output
304
305Finalize output.
306
307=cut
308
309sub finalize_output { }
310
311=head3 forward
312
313Forward processing to a private/public action or a method from a class.
314If you define a class without method it will default to process().
315
316 $c->forward('!foo');
317 $c->forward('index.html');
318 $c->forward(qw/MyApp::Model::CDBI::Foo do_stuff/);
319 $c->forward('MyApp::View::TT');
320
321=cut
322
323sub forward {
324 my $c = shift;
325 my $command = shift;
326 unless ($command) {
327 $c->log->debug('Nothing to forward to') if $c->debug;
328 return 0;
329 }
330 if ( $command =~ /^\?(.*)$/ ) {
331 $command = $1;
332 my $caller = caller(0);
333 $command = _prefix( $caller, $command );
334 }
335 elsif ( $command =~ /^\!\?(.*)$/ ) {
336 $command = $1;
337 my $caller = caller(0);
338 $command = _prefix( $caller, $command );
339 $command = "\!$command";
340 }
7242d068 341 elsif ( $command =~ /^\!(.*)$/ ) {
342 my $try = $1;
343 my $caller = caller(0);
344 my $prefix = _class2prefix($caller);
345 $try = "!$prefix/$command";
346 $command = $try if $c->actions->{plain}->{$try};
347 }
fc7ec1d9 348 my ( $class, $code );
349 if ( my $action = $c->action($command) ) {
7e5adedd 350 if ( $action->[2] ) {
351 $c->log->debug(qq/Couldn't forward "$command" to regex action/)
352 if $c->debug;
353 return 0;
354 }
fc7ec1d9 355 ( $class, $code ) = @{ $action->[0] };
356 }
357 else {
358 $class = $command;
359 if ( $class =~ /[^\w\:]/ ) {
360 $c->log->debug(qq/Couldn't forward to "$class"/) if $c->debug;
361 return 0;
362 }
363 my $method = shift || 'process';
364 if ( $code = $class->can($method) ) {
365 $c->actions->{reverse}->{"$code"} = "$class->$method";
366 }
367 else {
368 $c->log->debug(qq/Couldn't forward to "$class->$method"/)
369 if $c->debug;
370 return 0;
371 }
372 }
373 $class = $c->components->{$class} || $class;
374 return $c->process( $class, $code );
375}
376
377=head3 handler
378
379Handles the request.
380
381=cut
382
383sub handler {
384 my ( $class, $r ) = @_;
385
386 # Always expect worst case!
387 my $status = -1;
388 eval {
389 my $handler = sub {
390 my $c = $class->prepare($r);
7833fdfc 391 if ( my $action = $c->action( $c->req->action ) ) {
fc7ec1d9 392 my ( $begin, $end );
7833fdfc 393 my $class = ${ $action->[0] }[0];
394 my $prefix = _class2prefix($class);
395 if ($prefix) {
fc7ec1d9 396 if ( $c->actions->{plain}->{"\!$prefix/begin"} ) {
397 $begin = "\!$prefix/begin";
398 }
399 elsif ( $c->actions->{plain}->{'!begin'} ) {
400 $begin = '!begin';
401 }
402 if ( $c->actions->{plain}->{"\!$prefix/end"} ) {
403 $end = "\!$prefix/end";
404 }
405 elsif ( $c->actions->{plain}->{'!end'} ) { $end = '!end' }
406 }
7833fdfc 407 else {
408 if ( $c->actions->{plain}->{'!begin'} ) {
409 $begin = '!begin';
410 }
411 if ( $c->actions->{plain}->{'!end'} ) { $end = '!end' }
412 }
fc7ec1d9 413 $c->forward($begin) if $begin;
414 $c->forward( $c->req->action ) if $c->req->action;
415 $c->forward($end) if $end;
416 }
417 else {
418 my $action = $c->req->path;
419 my $error = $action
420 ? qq/Unknown resource "$action"/
7833fdfc 421 : "No default action defined";
fc7ec1d9 422 $c->log->error($error) if $c->debug;
423 $c->errors($error);
424 }
425 return $c->finalize;
426 };
427 if ( $class->debug ) {
428 my $elapsed;
429 ( $elapsed, $status ) = $class->benchmark($handler);
430 $elapsed = sprintf '%f', $elapsed;
431 my $av = sprintf '%.3f', 1 / $elapsed;
432 $class->log->info( "Request took $elapsed" . "s ($av/s)" );
433 }
434 else { $status = &$handler }
435 };
436 if ( my $error = $@ ) {
437 chomp $error;
438 $class->log->error(qq/Caught exception in engine "$error"/);
439 }
440 $COUNT++;
441 return $status;
442}
443
444=head3 prepare
445
446Turns the request (Apache, CGI...) into a Catalyst context.
447
448=cut
449
450sub prepare {
451 my ( $class, $r ) = @_;
452 my $c = bless {
453 request => Catalyst::Request->new(
454 {
455 arguments => [],
456 cookies => {},
457 headers => HTTP::Headers->new,
458 parameters => {},
459 snippets => [],
460 uploads => {}
461 }
462 ),
463 response => Catalyst::Response->new(
464 { cookies => {}, headers => HTTP::Headers->new, status => 200 }
465 ),
466 stash => {}
467 }, $class;
468 if ( $c->debug ) {
469 my $secs = time - $START || 1;
470 my $av = sprintf '%.3f', $COUNT / $secs;
471 $c->log->debug('********************************');
472 $c->log->debug("* Request $COUNT ($av/s) [$$]");
473 $c->log->debug('********************************');
474 $c->res->headers->header( 'X-Catalyst' => $Catalyst::VERSION );
475 }
476 $c->prepare_request($r);
477 $c->prepare_path;
478 my $path = $c->request->path;
479 $c->log->debug(qq/Requested path "$path"/) if $c->debug;
480 $c->prepare_cookies;
481 $c->prepare_headers;
482 $c->prepare_action;
483 $c->prepare_parameters;
484 $c->prepare_uploads;
485 return $c;
486}
487
488=head3 prepare_action
489
490Prepare action.
491
492=cut
493
494sub prepare_action {
495 my $c = shift;
496 my $path = $c->req->path;
497 my @path = split /\//, $c->req->path;
498 $c->req->args( \my @args );
499 while (@path) {
7833fdfc 500 $path = join '/', @path;
fc7ec1d9 501 if ( my $result = $c->action($path) ) {
502
503 # It's a regex
504 if ($#$result) {
7e5adedd 505 my $match = $result->[1];
506 my @snippets = @{ $result->[2] };
fc7ec1d9 507 $c->log->debug(qq/Requested action "$path" matched "$match"/)
508 if $c->debug;
509 $c->log->debug(
510 'Snippets are "' . join( ' ', @snippets ) . '"' )
511 if ( $c->debug && @snippets );
512 $c->req->action($match);
513 $c->req->snippets( \@snippets );
514 }
515 else {
516 $c->req->action($path);
517 $c->log->debug(qq/Requested action "$path"/) if $c->debug;
518 }
519 $c->req->match($path);
520 $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
521 if ( $c->debug && @args );
522 last;
523 }
524 unshift @args, pop @path;
525 }
526 unless ( $c->req->action ) {
527 my $prefix = $c->req->args->[0];
528 if ( $prefix && $c->actions->{plain}->{"\!$prefix/default"} ) {
529 $c->req->match('');
530 $c->req->action("\!$prefix/default");
531 $c->log->debug('Using prefixed default action') if $c->debug;
532 }
533 elsif ( $c->actions->{plain}->{'!default'} ) {
534 $c->req->match('');
535 $c->req->action('!default');
536 $c->log->debug('Using default action') if $c->debug;
537 }
538 }
539}
540
541=head3 prepare_cookies;
542
543Prepare cookies.
544
545=cut
546
547sub prepare_cookies { }
548
549=head3 prepare_headers
550
551Prepare headers.
552
553=cut
554
555sub prepare_headers { }
556
557=head3 prepare_parameters
558
559Prepare parameters.
560
561=cut
562
563sub prepare_parameters { }
564
565=head3 prepare_path
566
567Prepare path and base.
568
569=cut
570
571sub prepare_path { }
572
573=head3 prepare_request
574
575Prepare the engine request.
576
577=cut
578
579sub prepare_request { }
580
581=head3 prepare_uploads
582
583Prepare uploads.
584
585=cut
586
587sub prepare_uploads { }
588
589=head3 process
590
591Process a coderef in given class and catch exceptions.
592Errors are available via $c->errors.
593
594=cut
595
596sub process {
597 my ( $c, $class, $code ) = @_;
598 my $status;
599 eval {
600 if ( $c->debug )
601 {
602 my $action = $c->actions->{reverse}->{"$code"} || "$code";
603 my $elapsed;
604 ( $elapsed, $status ) =
605 $c->benchmark( $code, $class, $c, @{ $c->req->args } );
606 $c->log->info( sprintf qq/Processing "$action" took %fs/, $elapsed )
607 if $c->debug;
608 }
609 else { $status = &$code( $class, $c, @{ $c->req->args } ) }
610 };
611 if ( my $error = $@ ) {
612 chomp $error;
613 $error = qq/Caught exception "$error"/;
614 $c->log->error($error);
615 $c->errors($error) if $c->debug;
616 return 0;
617 }
618 return $status;
619}
620
621=head3 remove_action
622
623Remove an action.
624
625 $c->remove_action('!foo');
626
627=cut
628
629sub remove_action {
630 my ( $self, $action ) = @_;
631 if ( delete $self->actions->{regex}->{$action} ) {
632 while ( my ( $regex, $name ) = each %{ $self->actions->{compiled} } ) {
633 if ( $name eq $action ) {
634 delete $self->actions->{compiled}->{$regex};
635 last;
636 }
637 }
638 }
639 else {
640 delete $self->actions->{plain}->{$action};
641 }
642}
643
644=head3 request (req)
645
646Returns a C<Catalyst::Request> object.
647
648 my $req = $c->req;
649
650=head3 response (res)
651
652Returns a C<Catalyst::Response> object.
653
654 my $res = $c->res;
655
656=head3 setup
657
658Setup.
659
660 MyApp->setup;
661
662=cut
663
664sub setup {
665 my $self = shift;
666 $self->setup_components;
667 if ( $self->debug ) {
668 my $name = $self->config->{name} || 'Application';
669 $self->log->info("$name powered by Catalyst $Catalyst::VERSION");
670 }
671}
672
673=head3 setup_components
674
675Setup components.
676
677=cut
678
679sub setup_components {
680 my $self = shift;
681
682 # Components
683 my $class = ref $self || $self;
684 eval <<"";
685 package $class;
686 import Module::Pluggable::Fast
687 name => '_components',
688 search => [
689 '$class\::Controller', '$class\::C',
690 '$class\::Model', '$class\::M',
691 '$class\::View', '$class\::V'
692 ];
693
694 if ( my $error = $@ ) {
695 chomp $error;
696 $self->log->error(
697 qq/Couldn't initialize "Module::Pluggable::Fast", "$error"/);
698 }
699 $self->components( {} );
700 for my $component ( $self->_components($self) ) {
701 $self->components->{ ref $component } = $component;
702 }
703 $self->log->debug( 'Initialized components "'
704 . join( ' ', keys %{ $self->components } )
705 . '"' )
706 if $self->debug;
707}
708
709=head3 stash
710
711Returns a hashref containing all your data.
712
713 $c->stash->{foo} ||= 'yada';
714 print $c->stash->{foo};
715
716=cut
717
718sub stash {
719 my $self = shift;
720 if ( $_[0] ) {
721 my $stash = $_[1] ? {@_} : $_[0];
722 while ( my ( $key, $val ) = each %$stash ) {
723 $self->{stash}->{$key} = $val;
724 }
725 }
726 return $self->{stash};
727}
728
729sub _prefix {
730 my ( $class, $name ) = @_;
7833fdfc 731 my $prefix = _class2prefix($class);
732 $name = "$prefix/$name" if $prefix;
733 return $name;
734}
735
736sub _class2prefix {
737 my $class = shift;
fc7ec1d9 738 $class =~ /^.*::[(M)(Model)(V)(View)(C)(Controller)]+::(.*)$/;
739 my $prefix = lc $1 || '';
740 $prefix =~ s/\:\:/_/g;
7833fdfc 741 return $prefix;
fc7ec1d9 742}
743
744=head1 AUTHOR
745
746Sebastian Riedel, C<sri@cpan.org>
747
748=head1 COPYRIGHT
749
750This program is free software, you can redistribute it and/or modify it under
751the same terms as Perl itself.
752
753=cut
754
7551;