1 package Catalyst::Engine;
4 use base qw/Class::Data::Inheritable Class::Accessor::Fast/;
5 use UNIVERSAL::require;
11 use Time::HiRes qw/gettimeofday tv_interval/;
13 use Text::ASCIITable::Wrap 'wrap';
15 use Tree::Simple::Visitor::FindByPath;
16 use Catalyst::Request;
17 use Catalyst::Response;
19 require Module::Pluggable::Fast;
21 $Data::Dumper::Terse = 1;
23 __PACKAGE__->mk_classdata($_) for qw/actions components tree/;
24 __PACKAGE__->mk_accessors(qw/request response state/);
27 { plain => {}, private => {}, regex => {}, compiled => [], reverse => {} }
29 __PACKAGE__->tree( Tree::Simple->new( 0, Tree::Simple->ROOT ) );
38 memoize('_class2prefix');
42 Catalyst::Engine - The Catalyst Engine
54 =item $c->benchmark($coderef)
56 Takes a coderef with arguments and returns elapsed time as float.
58 my ( $elapsed, $status ) = $c->benchmark( sub { return 1 } );
59 $c->log->info( sprintf "Processing took %f seconds", $elapsed );
66 my $time = [gettimeofday];
67 my @return = &$code(@_);
68 my $elapsed = tv_interval $time;
69 return wantarray ? ( $elapsed, @return ) : $elapsed;
74 =item $c->component($name)
76 Get a component object by name.
78 $c->comp('MyApp::Model::MyModel')->do_stuff;
80 Regex search for a component.
82 $c->comp('mymodel')->do_stuff;
87 my ( $c, $name ) = @_;
88 if ( my $component = $c->components->{$name} ) {
92 for my $component ( keys %{ $c->components } ) {
93 return $c->components->{$component} if $component =~ /$name/i;
100 Dispatch request to actions.
106 my $action = $c->req->action;
108 $namespace = ( join( '/', @{ $c->req->args } ) || '/' )
109 if $action eq 'default';
110 unless ($namespace) {
111 if ( my $result = $c->get_action($action) ) {
112 $namespace = _class2prefix( $result->[0]->[0]->[0] );
115 my $default = $action eq 'default' ? $namespace : undef;
116 my $results = $c->get_action( $action, $default );
122 if ( my $begin = @{ $c->get_action( 'begin', $namespace ) }[-1] ) {
123 $c->execute( @{ $begin->[0] } );
124 return if scalar @{ $c->error };
127 # Execute the auto chain
128 for my $auto ( @{ $c->get_action( 'auto', $namespace ) } ) {
129 $c->execute( @{ $auto->[0] } );
130 return if scalar @{ $c->error };
131 last unless $c->state;
134 # Execute the action or last default
135 if ( ( my $action = $c->req->action ) && $c->state ) {
136 if ( my $result = @{ $c->get_action( $action, $default ) }[-1] ) {
137 $c->execute( @{ $result->[0] } );
142 if ( my $end = @{ $c->get_action( 'end', $namespace ) }[-1] ) {
143 $c->execute( @{ $end->[0] } );
144 return if scalar @{ $c->error };
148 my $path = $c->req->path;
150 ? qq/Unknown resource "$path"/
151 : "No default action defined";
152 $c->log->error($error) if $c->debug;
159 =item $c->error($error, ...)
161 =item $c->error($arrayref)
163 Returns an arrayref containing error messages.
165 my @error = @{ $c->error };
169 $c->error('Something bad happened');
175 my $error = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
176 push @{ $c->{error} }, @$error;
180 =item $c->execute($class, $coderef)
182 Execute a coderef in given class and catch exceptions.
183 Errors are available via $c->error.
188 my ( $c, $class, $code ) = @_;
189 $class = $c->comp($class) || $class;
191 my $callsub = ( caller(1) )[3];
195 my $action = $c->actions->{reverse}->{"$code"};
196 $action = "/$action" unless $action =~ /\-\>/;
197 $action = "-> $action" if $callsub =~ /forward$/;
198 my ( $elapsed, @state ) =
199 $c->benchmark( $code, $class, $c, @{ $c->req->args } );
200 push @{ $c->{stats} }, [ $action, sprintf( '%fs', $elapsed ) ];
203 else { $c->state( &$code( $class, $c, @{ $c->req->args } ) ) }
205 if ( my $error = $@ ) {
207 unless ( ref $error ) {
209 $error = qq/Caught exception "$error"/;
212 $c->log->error($error);
228 $c->finalize_cookies;
230 if ( my $location = $c->response->redirect ) {
231 $c->log->debug(qq/Redirecting to "$location"/) if $c->debug;
232 $c->response->header( Location => $location );
233 $c->response->status(302) if $c->response->status !~ /3\d\d$/;
236 if ( $#{ $c->error } >= 0 ) {
240 if ( !$c->response->output && $c->response->status !~ /^(1|3)\d\d$/ ) {
244 if ( $c->response->output && !$c->response->content_length ) {
245 use bytes; # play safe with a utf8 aware perl
246 $c->response->content_length( length $c->response->output );
249 my $status = $c->finalize_headers;
254 =item $c->finalize_cookies
260 sub finalize_cookies {
263 while ( my ( $name, $cookie ) = each %{ $c->response->cookies } ) {
264 my $cookie = CGI::Cookie->new(
266 -value => $cookie->{value},
267 -expires => $cookie->{expires},
268 -domain => $cookie->{domain},
269 -path => $cookie->{path},
270 -secure => $cookie->{secure} || 0
273 $c->res->headers->push_header( 'Set-Cookie' => $cookie->as_string );
277 =item $c->finalize_error
286 $c->res->headers->content_type('text/html');
287 my $name = $c->config->{name} || 'Catalyst Application';
289 my ( $title, $error, $infos );
291 $error = join '<br/>', @{ $c->error };
292 $error ||= 'No output';
293 $title = $name = "$name on Catalyst $Catalyst::VERSION";
294 my $req = encode_entities Dumper $c->req;
295 my $res = encode_entities Dumper $c->res;
296 my $stash = encode_entities Dumper $c->stash;
299 <b><u>Request</u></b><br/>
301 <b><u>Response</u></b><br/>
303 <b><u>Stash</u></b><br/>
312 (en) Please come back later
313 (de) Bitte versuchen sie es spaeter nocheinmal
314 (nl) Gelieve te komen later terug
315 (no) Vennligst prov igjen senere
316 (fr) Veuillez revenir plus tard
317 (es) Vuelto por favor mas adelante
318 (pt) Voltado por favor mais tarde
319 (it) Ritornato prego piĆ¹ successivamente
324 $c->res->output( <<"" );
327 <title>$title</title>
328 <style type="text/css">
330 font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
331 Tahoma, Arial, helvetica, sans-serif;
333 background-color: #eee;
338 background-color: #ccc;
339 border: 1px solid #aaa;
342 -moz-border-radius: 10px;
345 background-color: #977;
346 border: 1px solid #755;
350 -moz-border-radius: 10px;
353 background-color: #797;
354 border: 1px solid #575;
358 -moz-border-radius: 10px;
361 background-color: #779;
362 border: 1px solid #557;
365 -moz-border-radius: 10px;
371 <div class="error">$error</div>
372 <div class="infos">$infos</div>
373 <div class="name">$name</div>
380 =item $c->finalize_headers
386 sub finalize_headers { }
388 =item $c->finalize_output
394 sub finalize_output { }
396 =item $c->forward($command)
398 Forward processing to a private action or a method from a class.
399 If you define a class without method it will default to process().
402 $c->forward('index');
403 $c->forward(qw/MyApp::Model::CDBI::Foo do_stuff/);
404 $c->forward('MyApp::View::TT');
412 $c->log->debug('Nothing to forward to') if $c->debug;
415 my $caller = caller(0);
416 my $global = $command =~ /^\// ? 0 : 1;
418 if ( $command =~ /^\// ) {
419 $command =~ /^(.*)\/(\w+)$/;
420 $namespace = $1 || '/';
423 else { $namespace = _class2prefix($caller) || '/' }
424 my $results = $c->get_action( $command, $namespace, $global );
425 unless ( @{$results} ) {
426 my $class = $command;
427 if ( $class =~ /[^\w\:]/ ) {
428 $c->log->debug(qq/Couldn't forward to "$class"/) if $c->debug;
431 my $method = shift || 'process';
432 if ( my $code = $class->can($method) ) {
433 $c->actions->{reverse}->{"$code"} = "$class->$method";
434 $results = [ [ [ $class, $code ] ] ];
437 $c->log->debug(qq/Couldn't forward to "$class->$method"/)
442 for my $result ( @{$results} ) {
443 $c->execute( @{ $result->[0] } );
444 return if scalar @{ $c->error };
445 last unless $c->state;
450 =item $c->get_action( $action, $namespace, $global )
452 Get an action in a given namespace.
457 my ( $c, $action, $namespace, $global ) = @_;
458 return [] unless $action;
463 for my $uid ( keys %{ $c->actions->{private} } ) {
464 if ( my $result = $c->actions->{private}->{$uid}->{$action} ) {
465 push @results, [$result];
471 $namespace = '' if $namespace eq '/';
472 my $parent = $c->tree;
474 my $result = $c->actions->{private}->{ $parent->getUID }->{$action};
475 push @results, [$result] if $result;
476 my $visitor = Tree::Simple::Visitor::FindByPath->new;
477 for my $part ( split '/', $namespace ) {
478 $visitor->setSearchPath($part);
479 $parent->accept($visitor);
480 my $child = $visitor->getResult;
481 my $uid = $child->getUID if $child;
482 my $match = $c->actions->{private}->{$uid}->{$action} if $uid;
483 push @results, [$match] if $match;
484 $parent = $child if $child;
489 elsif ( my $p = $c->actions->{plain}->{$action} ) { return [ [$p] ] }
490 elsif ( my $r = $c->actions->{regex}->{$action} ) { return [ [$r] ] }
492 for my $i ( 0 .. $#{ $c->actions->{compiled} } ) {
493 my $name = $c->actions->{compiled}->[$i]->[0];
494 my $regex = $c->actions->{compiled}->[$i]->[1];
495 if ( $action =~ $regex ) {
497 for my $i ( 1 .. 9 ) {
500 push @snippets, ${$i};
502 return [ [ $c->actions->{regex}->{$name}, $name, \@snippets ] ];
509 =item $c->handler( $class, $r )
516 my ( $class, $engine ) = @_;
518 # Always expect worst case!
523 my $c = $class->prepare($engine);
524 $c->{stats} = \@stats;
528 if ( $class->debug ) {
530 ( $elapsed, $status ) = $class->benchmark($handler);
531 $elapsed = sprintf '%f', $elapsed;
532 my $av = sprintf '%.3f', 1 / $elapsed;
533 my $t = Text::ASCIITable->new;
534 $t->setCols( 'Action', 'Time' );
535 $t->setColWidth( 'Action', 64, 1 );
536 $t->setColWidth( 'Time', 9, 1 );
538 for my $stat (@stats) {
539 $t->addRow( wrap( $stat->[0], 64 ), wrap( $stat->[1], 9 ) );
541 $class->log->info( "Request took $elapsed" . "s ($av/s)",
544 else { $status = &$handler }
546 if ( my $error = $@ ) {
548 $class->log->error(qq/Caught exception in engine "$error"/);
554 =item $c->prepare($r)
556 Turns the engine-specific request( Apache, CGI ... )
557 into a Catalyst context .
562 my ( $class, $r ) = @_;
564 request => Catalyst::Request->new(
568 headers => HTTP::Headers->new,
574 response => Catalyst::Response->new(
575 { cookies => {}, headers => HTTP::Headers->new, status => 200 }
581 my $secs = time - $START || 1;
582 my $av = sprintf '%.3f', $COUNT / $secs;
583 $c->log->debug('**********************************');
584 $c->log->debug("* Request $COUNT ($av/s) [$$]");
585 $c->log->debug('**********************************');
586 $c->res->headers->header( 'X-Catalyst' => $Catalyst::VERSION );
588 $c->prepare_request($r);
592 $c->prepare_connection;
593 my $method = $c->req->method || '';
594 my $path = $c->req->path || '';
595 my $hostname = $c->req->hostname || '';
596 my $address = $c->req->address || '';
597 $c->log->debug(qq/"$method" request for "$path" from $hostname($address)/)
600 $c->prepare_parameters;
602 if ( $c->debug && keys %{ $c->req->params } ) {
603 my $t = Text::ASCIITable->new;
604 $t->setCols( 'Key', 'Value' );
605 $t->setColWidth( 'Key', 37, 1 );
606 $t->setColWidth( 'Value', 36, 1 );
607 for my $key ( keys %{ $c->req->params } ) {
608 my $value = $c->req->params->{$key} || '';
609 $t->addRow( wrap( $key, 37 ), wrap( $value, 36 ) );
611 $c->log->debug( 'Parameters are', $t->draw );
617 =item $c->prepare_action
625 my $path = $c->req->path;
626 my @path = split /\//, $c->req->path;
627 $c->req->args( \my @args );
629 $path = join '/', @path;
630 if ( my $result = ${ $c->get_action($path) }[0] ) {
634 my $match = $result->[1];
635 my @snippets = @{ $result->[2] };
637 qq/Requested action is "$path" and matched "$match"/)
640 'Snippets are "' . join( ' ', @snippets ) . '"' )
641 if ( $c->debug && @snippets );
642 $c->req->action($match);
643 $c->req->snippets( \@snippets );
646 $c->req->action($path);
647 $c->log->debug(qq/Requested action is "$path"/) if $c->debug;
649 $c->req->match($path);
652 unshift @args, pop @path;
654 unless ( $c->req->action ) {
655 $c->req->action('default');
658 $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
659 if ( $c->debug && @args );
662 =item $c->prepare_connection
668 sub prepare_connection { }
670 =item $c->prepare_cookies
676 sub prepare_cookies {
679 if ( my $header = $c->request->header('Cookie') ) {
680 $c->req->cookies( { CGI::Cookie->parse($header) } );
684 =item $c->prepare_headers
690 sub prepare_headers { }
692 =item $c->prepare_parameters
698 sub prepare_parameters { }
700 =item $c->prepare_path
702 Prepare path and base.
708 =item $c->prepare_request
710 Prepare the engine request.
714 sub prepare_request { }
716 =item $c->prepare_uploads
722 sub prepare_uploads { }
736 Returns a C<Catalyst::Request> object.
744 Returns a C<Catalyst::Response> object.
748 =item $c->set_action( $action, $code, $namespace, $attrs )
750 Set an action in a given namespace.
755 my ( $c, $method, $code, $namespace, $attrs ) = @_;
757 my $prefix = _class2prefix($namespace) || '';
760 for my $attr ( @{$attrs} ) {
761 if ( $attr =~ /^(Local|Relative)$/ ) { $flags{local}++ }
762 elsif ( $attr =~ /^(Global|Absolute)$/ ) { $flags{global}++ }
763 elsif ( $attr =~ /^Path\((.+)\)$/i ) { $flags{path} = $1 }
764 elsif ( $attr =~ /^Private$/i ) { $flags{private}++ }
765 elsif ( $attr =~ /^(Regex|Regexp)\((.+)\)$/i ) { $flags{regex} = $2 }
768 return unless keys %flags;
770 my $parent = $c->tree;
771 my $visitor = Tree::Simple::Visitor::FindByPath->new;
772 for my $part ( split '/', $prefix ) {
773 $visitor->setSearchPath($part);
774 $parent->accept($visitor);
775 my $child = $visitor->getResult;
777 $child = $parent->addChild( Tree::Simple->new($part) );
778 $visitor->setSearchPath($part);
779 $parent->accept($visitor);
780 $child = $visitor->getResult;
784 my $uid = $parent->getUID;
785 $c->actions->{private}->{$uid}->{$method} = [ $namespace, $code ];
786 my $forward = $prefix ? "$prefix/$method" : $method;
788 if ( $flags{path} ) {
789 $flags{path} =~ s/^\w+//;
790 $flags{path} =~ s/\w+$//;
791 if ( $flags{path} =~ /^'(.*)'$/ ) { $flags{path} = $1 }
792 if ( $flags{path} =~ /^"(.*)"$/ ) { $flags{path} = $1 }
794 if ( $flags{regex} ) {
795 $flags{regex} =~ s/^\w+//;
796 $flags{regex} =~ s/\w+$//;
797 if ( $flags{regex} =~ /^'(.*)'$/ ) { $flags{regex} = $1 }
798 if ( $flags{regex} =~ /^"(.*)"$/ ) { $flags{regex} = $1 }
801 my $reverse = $prefix ? "$prefix/$method" : $method;
803 if ( $flags{local} || $flags{global} || $flags{path} ) {
804 my $path = $flags{path} || $method;
806 if ( $path =~ /^\/(.+)/ ) {
810 $absolute = 1 if $flags{global};
811 my $name = $absolute ? $path : $prefix ? "$prefix/$path" : $path;
812 $c->actions->{plain}->{$name} = [ $namespace, $code ];
814 if ( my $regex = $flags{regex} ) {
815 push @{ $c->actions->{compiled} }, [ $regex, qr#$regex# ];
816 $c->actions->{regex}->{$regex} = [ $namespace, $code ];
819 $c->actions->{reverse}->{"$code"} = $reverse;
832 $self->setup_components;
833 if ( $self->debug ) {
834 my $name = $self->config->{name} || 'Application';
835 $self->log->info("$name powered by Catalyst $Catalyst::VERSION");
839 =item $class->setup_actions($component)
841 Setup actions for a component.
846 my ( $self, $comp ) = @_;
847 $comp = ref $comp || $comp;
848 for my $action ( @{ $comp->_cache } ) {
849 my ( $code, $attrs ) = @{$action};
852 my @cache = ( $comp, @{"$comp\::ISA"} );
854 while ( my $namespace = shift @cache ) {
855 $namespaces{$namespace}++;
856 for my $isa ( @{"$comp\::ISA"} ) {
857 next if $namespaces{$isa};
862 for my $namespace ( keys %namespaces ) {
863 for my $sym ( values %{ $namespace . '::' } ) {
864 if ( *{$sym}{CODE} && *{$sym}{CODE} == $code ) {
865 $name = *{$sym}{NAME};
866 $self->set_action( $name, $code, $comp, $attrs );
874 =item $class->setup_components
880 sub setup_components {
884 my $class = ref $self || $self;
887 import Module::Pluggable::Fast
888 name => '_components',
890 '$class\::Controller', '$class\::C',
891 '$class\::Model', '$class\::M',
892 '$class\::View', '$class\::V'
895 if ( my $error = $@ ) {
898 qq/Couldn't initialize "Module::Pluggable::Fast", "$error"/);
900 $self->setup_actions($self);
901 $self->components( {} );
902 for my $comp ( $self->_components($self) ) {
903 $self->components->{ ref $comp } = $comp;
904 $self->setup_actions($comp);
906 my $t = Text::ASCIITable->new( { hide_HeadRow => 1, hide_HeadLine => 1 } );
907 $t->setCols('Class');
908 $t->setColWidth( 'Class', 75, 1 );
909 $t->addRow( wrap( $_, 75 ) ) for keys %{ $self->components };
910 $self->log->debug( 'Loaded components', $t->draw )
911 if ( @{ $t->{tbl_rows} } && $self->debug );
912 my $actions = $self->actions;
913 my $privates = Text::ASCIITable->new;
914 $privates->setCols( 'Private', 'Class', 'Code' );
915 $privates->setColWidth( 'Private', 28, 1 );
916 $privates->setColWidth( 'Class', 28, 1 );
917 $privates->setColWidth( 'Code', 14, 1 );
919 my ( $walker, $parent, $prefix ) = @_;
920 $prefix .= $parent->getNodeValue || '';
921 $prefix .= '/' unless $prefix =~ /\/$/;
922 my $uid = $parent->getUID;
923 for my $action ( keys %{ $actions->{private}->{$uid} } ) {
924 my ( $class, $code ) = @{ $actions->{private}->{$uid}->{$action} };
926 wrap( "$prefix$action", 28 ),
931 $walker->( $walker, $_, $prefix ) for $parent->getAllChildren;
933 $walker->( $walker, $self->tree, '' );
934 $self->log->debug( 'Loaded private actions', $privates->draw )
935 if ( @{ $privates->{tbl_rows} } && $self->debug );
936 my $publics = Text::ASCIITable->new;
937 $publics->setCols( 'Public', 'Private' );
938 $publics->setColWidth( 'Public', 37, 1 );
939 $publics->setColWidth( 'Private', 36, 1 );
941 for my $plain ( sort keys %{ $actions->{plain} } ) {
942 my ( $class, $code ) = @{ $actions->{plain}->{$plain} };
943 $publics->addRow( wrap( "/$plain", 37 ),
944 wrap( $self->actions->{reverse}->{$code} || $code, 36 ) );
946 $self->log->debug( 'Loaded public actions', $publics->draw )
947 if ( @{ $publics->{tbl_rows} } && $self->debug );
948 my $regexes = Text::ASCIITable->new;
949 $regexes->setCols( 'Regex', 'Private' );
950 $regexes->setColWidth( 'Regex', 37, 1 );
951 $regexes->setColWidth( 'Private', 36, 1 );
952 for my $regex ( sort keys %{ $actions->{regex} } ) {
953 my ( $class, $code ) = @{ $actions->{regex}->{$regex} };
954 $regexes->addRow( wrap( $regex, 37 ),
955 wrap( $self->actions->{reverse}->{$class} || $class, 36 ) );
957 $self->log->debug( 'Loaded regex actions', $regexes->draw )
958 if ( @{ $regexes->{tbl_rows} } && $self->debug );
963 Contains the return value of the last executed action.
967 Returns a hashref containing all your data.
969 $c->stash->{foo} ||= 'yada';
970 print $c->stash->{foo};
977 my $stash = $_[1] ? {@_} : $_[0];
978 while ( my ( $key, $val ) = each %$stash ) {
979 $self->{stash}->{$key} = $val;
982 return $self->{stash};
986 my ( $class, $name ) = @_;
987 my $prefix = _class2prefix($class);
988 $name = "$prefix/$name" if $prefix;
993 my $class = shift || '';
995 if ( $class =~ /^.*::([MVC]|Model|View|Controller)?::(.*)$/ ) {
997 $prefix =~ s/\:\:/\//g;
1006 Sebastian Riedel, C<sri@cpan.org>
1010 This program is free software, you can redistribute it and/or modify it under
1011 the same terms as Perl itself.