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::Request::Upload;
18 use Catalyst::Response;
20 require Module::Pluggable::Fast;
22 $Data::Dumper::Terse = 1;
24 __PACKAGE__->mk_classdata($_) for qw/actions components tree/;
25 __PACKAGE__->mk_accessors(qw/request response state/);
28 { plain => {}, private => {}, regex => {}, compiled => [], reverse => {} }
30 __PACKAGE__->tree( Tree::Simple->new( 0, Tree::Simple->ROOT ) );
39 memoize('_class2prefix');
43 Catalyst::Engine - The Catalyst Engine
55 =item $c->benchmark($coderef)
57 Takes a coderef with arguments and returns elapsed time as float.
59 my ( $elapsed, $status ) = $c->benchmark( sub { return 1 } );
60 $c->log->info( sprintf "Processing took %f seconds", $elapsed );
67 my $time = [gettimeofday];
68 my @return = &$code(@_);
69 my $elapsed = tv_interval $time;
70 return wantarray ? ( $elapsed, @return ) : $elapsed;
75 =item $c->component($name)
77 Get a component object by name.
79 $c->comp('MyApp::Model::MyModel')->do_stuff;
81 Regex search for a component.
83 $c->comp('mymodel')->do_stuff;
88 my ( $c, $name ) = @_;
89 if ( my $component = $c->components->{$name} ) {
93 for my $component ( keys %{ $c->components } ) {
94 return $c->components->{$component} if $component =~ /$name/i;
101 Dispatch request to actions.
107 my $action = $c->req->action;
109 $namespace = ( join( '/', @{ $c->req->args } ) || '/' )
110 if $action eq 'default';
111 unless ($namespace) {
112 if ( my $result = $c->get_action($action) ) {
113 $namespace = _class2prefix( $result->[0]->[0]->[0] );
116 my $default = $action eq 'default' ? $namespace : undef;
117 my $results = $c->get_action( $action, $default );
123 if ( my $begin = @{ $c->get_action( 'begin', $namespace ) }[-1] ) {
124 $c->execute( @{ $begin->[0] } );
125 return if scalar @{ $c->error };
128 # Execute the auto chain
129 for my $auto ( @{ $c->get_action( 'auto', $namespace ) } ) {
130 $c->execute( @{ $auto->[0] } );
131 return if scalar @{ $c->error };
132 last unless $c->state;
135 # Execute the action or last default
136 if ( ( my $action = $c->req->action ) && $c->state ) {
137 if ( my $result = @{ $c->get_action( $action, $default ) }[-1] ) {
138 $c->execute( @{ $result->[0] } );
143 if ( my $end = @{ $c->get_action( 'end', $namespace ) }[-1] ) {
144 $c->execute( @{ $end->[0] } );
145 return if scalar @{ $c->error };
149 my $path = $c->req->path;
151 ? qq/Unknown resource "$path"/
152 : "No default action defined";
153 $c->log->error($error) if $c->debug;
160 =item $c->error($error, ...)
162 =item $c->error($arrayref)
164 Returns an arrayref containing error messages.
166 my @error = @{ $c->error };
170 $c->error('Something bad happened');
176 my $error = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
177 push @{ $c->{error} }, @$error;
181 =item $c->execute($class, $coderef)
183 Execute a coderef in given class and catch exceptions.
184 Errors are available via $c->error.
189 my ( $c, $class, $code ) = @_;
190 $class = $c->comp($class) || $class;
192 my $callsub = ( caller(1) )[3];
196 my $action = $c->actions->{reverse}->{"$code"};
197 $action = "/$action" unless $action =~ /\-\>/;
198 $action = "-> $action" if $callsub =~ /forward$/;
199 my ( $elapsed, @state ) =
200 $c->benchmark( $code, $class, $c, @{ $c->req->args } );
201 push @{ $c->{stats} }, [ $action, sprintf( '%fs', $elapsed ) ];
204 else { $c->state( &$code( $class, $c, @{ $c->req->args } ) ) }
206 if ( my $error = $@ ) {
208 unless ( ref $error ) {
210 $error = qq/Caught exception "$error"/;
213 $c->log->error($error);
229 $c->finalize_cookies;
231 if ( my $location = $c->response->redirect ) {
232 $c->log->debug(qq/Redirecting to "$location"/) if $c->debug;
233 $c->response->header( Location => $location );
234 $c->response->status(302) if $c->response->status !~ /^3\d\d$/;
237 if ( $#{ $c->error } >= 0 ) {
241 if ( !$c->response->output && $c->response->status !~ /^(1|3)\d\d$/ ) {
245 if ( $c->response->output && !$c->response->content_length ) {
246 use bytes; # play safe with a utf8 aware perl
247 $c->response->content_length( length $c->response->output );
250 my $status = $c->finalize_headers;
255 =item $c->finalize_cookies
261 sub finalize_cookies {
264 while ( my ( $name, $cookie ) = each %{ $c->response->cookies } ) {
265 my $cookie = CGI::Cookie->new(
267 -value => $cookie->{value},
268 -expires => $cookie->{expires},
269 -domain => $cookie->{domain},
270 -path => $cookie->{path},
271 -secure => $cookie->{secure} || 0
274 $c->res->headers->push_header( 'Set-Cookie' => $cookie->as_string );
278 =item $c->finalize_error
287 $c->res->headers->content_type('text/html');
288 my $name = $c->config->{name} || 'Catalyst Application';
290 my ( $title, $error, $infos );
292 $error = join '<br/>', @{ $c->error };
293 $error ||= 'No output';
294 $title = $name = "$name on Catalyst $Catalyst::VERSION";
295 my $req = encode_entities Dumper $c->req;
296 my $res = encode_entities Dumper $c->res;
297 my $stash = encode_entities Dumper $c->stash;
300 <b><u>Request</u></b><br/>
302 <b><u>Response</u></b><br/>
304 <b><u>Stash</u></b><br/>
313 (en) Please come back later
314 (de) Bitte versuchen sie es spaeter nocheinmal
315 (nl) Gelieve te komen later terug
316 (no) Vennligst prov igjen senere
317 (fr) Veuillez revenir plus tard
318 (es) Vuelto por favor mas adelante
319 (pt) Voltado por favor mais tarde
320 (it) Ritornato prego piĆ¹ successivamente
325 $c->res->output( <<"" );
328 <title>$title</title>
329 <style type="text/css">
331 font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
332 Tahoma, Arial, helvetica, sans-serif;
334 background-color: #eee;
339 background-color: #ccc;
340 border: 1px solid #aaa;
343 -moz-border-radius: 10px;
346 background-color: #977;
347 border: 1px solid #755;
351 -moz-border-radius: 10px;
354 background-color: #797;
355 border: 1px solid #575;
359 -moz-border-radius: 10px;
362 background-color: #779;
363 border: 1px solid #557;
366 -moz-border-radius: 10px;
372 <div class="error">$error</div>
373 <div class="infos">$infos</div>
374 <div class="name">$name</div>
381 =item $c->finalize_headers
387 sub finalize_headers { }
389 =item $c->finalize_output
395 sub finalize_output { }
397 =item $c->forward($command)
399 Forward processing to a private action or a method from a class.
400 If you define a class without method it will default to process().
403 $c->forward('index');
404 $c->forward(qw/MyApp::Model::CDBI::Foo do_stuff/);
405 $c->forward('MyApp::View::TT');
413 $c->log->debug('Nothing to forward to') if $c->debug;
416 my $caller = caller(0);
418 if ( $command =~ /^\// ) {
419 $command =~ /^(.*)\/(\w+)$/;
420 $namespace = $1 || '/';
423 else { $namespace = _class2prefix($caller) || '/' }
424 my $results = $c->get_action( $command, $namespace );
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 )
452 Get an action in a given namespace.
457 my ( $c, $action, $namespace ) = @_;
458 return [] unless $action;
461 $namespace = '' if $namespace eq '/';
462 my $parent = $c->tree;
464 my %allowed = ( begin => 1, auto => 1, default => 1, end => 1 );
465 if ( $allowed{$action} ) {
466 my $result = $c->actions->{private}->{ $parent->getUID }->{$action};
467 push @results, [$result] if $result;
468 my $visitor = Tree::Simple::Visitor::FindByPath->new;
469 for my $part ( split '/', $namespace ) {
470 $visitor->setSearchPath($part);
471 $parent->accept($visitor);
472 my $child = $visitor->getResult;
473 my $uid = $child->getUID if $child;
474 my $match = $c->actions->{private}->{$uid}->{$action} if $uid;
475 push @results, [$match] if $match;
476 $parent = $child if $child;
481 my $visitor = Tree::Simple::Visitor::FindByPath->new;
482 $visitor->setSearchPath( split '/', $namespace );
483 $parent->accept($visitor);
484 my $child = $visitor->getResult;
485 my $uid = $child->getUID if $child;
486 my $match = $c->actions->{private}->{$uid}->{$action}
488 push @results, [$match] if $match;
492 $c->actions->{private}->{ $parent->getUID }->{$action};
493 push @results, [$result] if $result;
498 elsif ( my $p = $c->actions->{plain}->{$action} ) { return [ [$p] ] }
499 elsif ( my $r = $c->actions->{regex}->{$action} ) { return [ [$r] ] }
501 for my $i ( 0 .. $#{ $c->actions->{compiled} } ) {
502 my $name = $c->actions->{compiled}->[$i]->[0];
503 my $regex = $c->actions->{compiled}->[$i]->[1];
504 if ( $action =~ $regex ) {
506 for my $i ( 1 .. 9 ) {
509 push @snippets, ${$i};
511 return [ [ $c->actions->{regex}->{$name}, $name, \@snippets ] ];
518 =item $c->handler( $class, $r )
525 my ( $class, $engine ) = @_;
527 # Always expect worst case!
532 my $c = $class->prepare($engine);
533 $c->{stats} = \@stats;
537 if ( $class->debug ) {
539 ( $elapsed, $status ) = $class->benchmark($handler);
540 $elapsed = sprintf '%f', $elapsed;
541 my $av = sprintf '%.3f', 1 / $elapsed;
542 my $t = Text::ASCIITable->new;
543 $t->setCols( 'Action', 'Time' );
544 $t->setColWidth( 'Action', 64, 1 );
545 $t->setColWidth( 'Time', 9, 1 );
547 for my $stat (@stats) {
548 $t->addRow( wrap( $stat->[0], 64 ), wrap( $stat->[1], 9 ) );
550 $class->log->info( "Request took $elapsed" . "s ($av/s)",
553 else { $status = &$handler }
555 if ( my $error = $@ ) {
557 $class->log->error(qq/Caught exception in engine "$error"/);
563 =item $c->prepare($r)
565 Turns the engine-specific request( Apache, CGI ... )
566 into a Catalyst context .
571 my ( $class, $r ) = @_;
573 request => Catalyst::Request->new(
577 headers => HTTP::Headers->new,
583 response => Catalyst::Response->new(
584 { cookies => {}, headers => HTTP::Headers->new, status => 200 }
590 my $secs = time - $START || 1;
591 my $av = sprintf '%.3f', $COUNT / $secs;
592 $c->log->debug('**********************************');
593 $c->log->debug("* Request $COUNT ($av/s) [$$]");
594 $c->log->debug('**********************************');
595 $c->res->headers->header( 'X-Catalyst' => $Catalyst::VERSION );
597 $c->prepare_request($r);
601 $c->prepare_connection;
602 my $method = $c->req->method || '';
603 my $path = $c->req->path || '';
604 my $hostname = $c->req->hostname || '';
605 my $address = $c->req->address || '';
606 $c->log->debug(qq/"$method" request for "$path" from $hostname($address)/)
609 $c->prepare_parameters;
611 if ( $c->debug && keys %{ $c->req->params } ) {
612 my $t = Text::ASCIITable->new;
613 $t->setCols( 'Key', 'Value' );
614 $t->setColWidth( 'Key', 37, 1 );
615 $t->setColWidth( 'Value', 36, 1 );
616 for my $key ( keys %{ $c->req->params } ) {
617 my $value = $c->req->params->{$key} || '';
618 $t->addRow( wrap( $key, 37 ), wrap( $value, 36 ) );
620 $c->log->debug( 'Parameters are', $t->draw );
626 =item $c->prepare_action
634 my $path = $c->req->path;
635 my @path = split /\//, $c->req->path;
636 $c->req->args( \my @args );
638 $path = join '/', @path;
639 if ( my $result = ${ $c->get_action($path) }[0] ) {
643 my $match = $result->[1];
644 my @snippets = @{ $result->[2] };
646 qq/Requested action is "$path" and matched "$match"/)
649 'Snippets are "' . join( ' ', @snippets ) . '"' )
650 if ( $c->debug && @snippets );
651 $c->req->action($match);
652 $c->req->snippets( \@snippets );
655 $c->req->action($path);
656 $c->log->debug(qq/Requested action is "$path"/) if $c->debug;
658 $c->req->match($path);
661 unshift @args, pop @path;
663 unless ( $c->req->action ) {
664 $c->req->action('default');
667 $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
668 if ( $c->debug && @args );
671 =item $c->prepare_connection
677 sub prepare_connection { }
679 =item $c->prepare_cookies
685 sub prepare_cookies {
688 if ( my $header = $c->request->header('Cookie') ) {
689 $c->req->cookies( { CGI::Cookie->parse($header) } );
693 =item $c->prepare_headers
699 sub prepare_headers { }
701 =item $c->prepare_parameters
707 sub prepare_parameters { }
709 =item $c->prepare_path
711 Prepare path and base.
717 =item $c->prepare_request
719 Prepare the engine request.
723 sub prepare_request { }
725 =item $c->prepare_uploads
731 sub prepare_uploads { }
745 Returns a C<Catalyst::Request> object.
753 Returns a C<Catalyst::Response> object.
757 =item $c->set_action( $action, $code, $namespace, $attrs )
759 Set an action in a given namespace.
764 my ( $c, $method, $code, $namespace, $attrs ) = @_;
766 my $prefix = _class2prefix($namespace) || '';
769 for my $attr ( @{$attrs} ) {
770 if ( $attr =~ /^(Local|Relative)$/ ) { $flags{local}++ }
771 elsif ( $attr =~ /^(Global|Absolute)$/ ) { $flags{global}++ }
772 elsif ( $attr =~ /^Path\((.+)\)$/i ) { $flags{path} = $1 }
773 elsif ( $attr =~ /^Private$/i ) { $flags{private}++ }
774 elsif ( $attr =~ /^(Regex|Regexp)\((.+)\)$/i ) { $flags{regex} = $2 }
777 return unless keys %flags;
779 my $parent = $c->tree;
780 my $visitor = Tree::Simple::Visitor::FindByPath->new;
781 for my $part ( split '/', $prefix ) {
782 $visitor->setSearchPath($part);
783 $parent->accept($visitor);
784 my $child = $visitor->getResult;
786 $child = $parent->addChild( Tree::Simple->new($part) );
787 $visitor->setSearchPath($part);
788 $parent->accept($visitor);
789 $child = $visitor->getResult;
793 my $uid = $parent->getUID;
794 $c->actions->{private}->{$uid}->{$method} = [ $namespace, $code ];
795 my $forward = $prefix ? "$prefix/$method" : $method;
797 if ( $flags{path} ) {
798 $flags{path} =~ s/^\w+//;
799 $flags{path} =~ s/\w+$//;
800 if ( $flags{path} =~ /^'(.*)'$/ ) { $flags{path} = $1 }
801 if ( $flags{path} =~ /^"(.*)"$/ ) { $flags{path} = $1 }
803 if ( $flags{regex} ) {
804 $flags{regex} =~ s/^\w+//;
805 $flags{regex} =~ s/\w+$//;
806 if ( $flags{regex} =~ /^'(.*)'$/ ) { $flags{regex} = $1 }
807 if ( $flags{regex} =~ /^"(.*)"$/ ) { $flags{regex} = $1 }
810 my $reverse = $prefix ? "$prefix/$method" : $method;
812 if ( $flags{local} || $flags{global} || $flags{path} ) {
813 my $path = $flags{path} || $method;
815 if ( $path =~ /^\/(.+)/ ) {
819 $absolute = 1 if $flags{global};
820 my $name = $absolute ? $path : $prefix ? "$prefix/$path" : $path;
821 $c->actions->{plain}->{$name} = [ $namespace, $code ];
823 if ( my $regex = $flags{regex} ) {
824 push @{ $c->actions->{compiled} }, [ $regex, qr#$regex# ];
825 $c->actions->{regex}->{$regex} = [ $namespace, $code ];
828 $c->actions->{reverse}->{"$code"} = $reverse;
841 $self->setup_components;
842 if ( $self->debug ) {
843 my $name = $self->config->{name} || 'Application';
844 $self->log->info("$name powered by Catalyst $Catalyst::VERSION");
848 =item $class->setup_actions($component)
850 Setup actions for a component.
855 my ( $self, $comp ) = @_;
856 $comp = ref $comp || $comp;
857 for my $action ( @{ $comp->_cache } ) {
858 my ( $code, $attrs ) = @{$action};
861 my @cache = ( $comp, @{"$comp\::ISA"} );
863 while ( my $namespace = shift @cache ) {
864 $namespaces{$namespace}++;
865 for my $isa ( @{"$comp\::ISA"} ) {
866 next if $namespaces{$isa};
871 for my $namespace ( keys %namespaces ) {
872 for my $sym ( values %{ $namespace . '::' } ) {
873 if ( *{$sym}{CODE} && *{$sym}{CODE} == $code ) {
874 $name = *{$sym}{NAME};
875 $self->set_action( $name, $code, $comp, $attrs );
883 =item $class->setup_components
889 sub setup_components {
893 my $class = ref $self || $self;
896 import Module::Pluggable::Fast
897 name => '_components',
899 '$class\::Controller', '$class\::C',
900 '$class\::Model', '$class\::M',
901 '$class\::View', '$class\::V'
904 if ( my $error = $@ ) {
907 qq/Couldn't initialize "Module::Pluggable::Fast", "$error"/);
909 $self->setup_actions($self);
910 $self->components( {} );
911 for my $comp ( $self->_components($self) ) {
912 $self->components->{ ref $comp } = $comp;
913 $self->setup_actions($comp);
915 my $t = Text::ASCIITable->new( { hide_HeadRow => 1, hide_HeadLine => 1 } );
916 $t->setCols('Class');
917 $t->setColWidth( 'Class', 75, 1 );
918 $t->addRow( wrap( $_, 75 ) ) for keys %{ $self->components };
919 $self->log->debug( 'Loaded components', $t->draw )
920 if ( @{ $t->{tbl_rows} } && $self->debug );
921 my $actions = $self->actions;
922 my $privates = Text::ASCIITable->new;
923 $privates->setCols( 'Private', 'Class', 'Code' );
924 $privates->setColWidth( 'Private', 28, 1 );
925 $privates->setColWidth( 'Class', 28, 1 );
926 $privates->setColWidth( 'Code', 14, 1 );
928 my ( $walker, $parent, $prefix ) = @_;
929 $prefix .= $parent->getNodeValue || '';
930 $prefix .= '/' unless $prefix =~ /\/$/;
931 my $uid = $parent->getUID;
932 for my $action ( keys %{ $actions->{private}->{$uid} } ) {
933 my ( $class, $code ) = @{ $actions->{private}->{$uid}->{$action} };
935 wrap( "$prefix$action", 28 ),
940 $walker->( $walker, $_, $prefix ) for $parent->getAllChildren;
942 $walker->( $walker, $self->tree, '' );
943 $self->log->debug( 'Loaded private actions', $privates->draw )
944 if ( @{ $privates->{tbl_rows} } && $self->debug );
945 my $publics = Text::ASCIITable->new;
946 $publics->setCols( 'Public', 'Private' );
947 $publics->setColWidth( 'Public', 37, 1 );
948 $publics->setColWidth( 'Private', 36, 1 );
950 for my $plain ( sort keys %{ $actions->{plain} } ) {
951 my ( $class, $code ) = @{ $actions->{plain}->{$plain} };
952 $publics->addRow( wrap( "/$plain", 37 ),
953 wrap( $self->actions->{reverse}->{$code} || $code, 36 ) );
955 $self->log->debug( 'Loaded public actions', $publics->draw )
956 if ( @{ $publics->{tbl_rows} } && $self->debug );
957 my $regexes = Text::ASCIITable->new;
958 $regexes->setCols( 'Regex', 'Private' );
959 $regexes->setColWidth( 'Regex', 37, 1 );
960 $regexes->setColWidth( 'Private', 36, 1 );
961 for my $regex ( sort keys %{ $actions->{regex} } ) {
962 my ( $class, $code ) = @{ $actions->{regex}->{$regex} };
963 $regexes->addRow( wrap( $regex, 37 ),
964 wrap( $self->actions->{reverse}->{$class} || $class, 36 ) );
966 $self->log->debug( 'Loaded regex actions', $regexes->draw )
967 if ( @{ $regexes->{tbl_rows} } && $self->debug );
972 Contains the return value of the last executed action.
976 Returns a hashref containing all your data.
978 $c->stash->{foo} ||= 'yada';
979 print $c->stash->{foo};
986 my $stash = $_[1] ? {@_} : $_[0];
987 while ( my ( $key, $val ) = each %$stash ) {
988 $self->{stash}->{$key} = $val;
991 return $self->{stash};
995 my ( $class, $name ) = @_;
996 my $prefix = _class2prefix($class);
997 $name = "$prefix/$name" if $prefix;
1002 my $class = shift || '';
1004 if ( $class =~ /^.*::([MVC]|Model|View|Controller)?::(.*)$/ ) {
1006 $prefix =~ s/\:\:/\//g;
1015 Sebastian Riedel, C<sri@cpan.org>
1019 This program is free software, you can redistribute it and/or modify it under
1020 the same terms as Perl itself.