X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=catagits%2FCatalyst-Runtime.git;a=blobdiff_plain;f=lib%2FCatalyst.pm;h=4cf485d0e99a0685744f7dab227f139c86aef751;hp=d93707fcd26f0c6c52b191687f125f150d1fedb0;hb=cc95842fedcac58b2dc12c6ce547e22d3170351c;hpb=8327e2e21fc9ea72c357e876f868001ad9712474 diff --git a/lib/Catalyst.pm b/lib/Catalyst.pm index d93707f..4cf485d 100644 --- a/lib/Catalyst.pm +++ b/lib/Catalyst.pm @@ -3,7 +3,6 @@ package Catalyst; use strict; use base 'Catalyst::Component'; use bytes; -use UNIVERSAL::require; use Catalyst::Exception; use Catalyst::Log; use Catalyst::Request; @@ -11,19 +10,26 @@ use Catalyst::Request::Upload; use Catalyst::Response; use Catalyst::Utils; use Catalyst::Controller; +use Devel::InnerPackage (); use File::stat; +use Module::Pluggable::Object (); use NEXT; -use Text::SimpleTable; -use Path::Class; +use Text::SimpleTable (); +use Path::Class::Dir (); +use Path::Class::File (); use Time::HiRes qw/gettimeofday tv_interval/; -use URI; -use Scalar::Util qw/weaken/; +use URI (); +use Scalar::Util qw/weaken blessed/; use Tree::Simple qw/use_weak_refs/; use Tree::Simple::Visitor::FindByUID; use attributes; +use utf8; +use Carp qw/croak/; + +BEGIN { require 5.008001; } __PACKAGE__->mk_accessors( - qw/counter request response state action stack namespace/ + qw/counter request response state action stack namespace stats/ ); attributes->import( __PACKAGE__, \&namespace, 'lvalue' ); @@ -44,11 +50,6 @@ our $START = time; our $RECURSION = 1000; our $DETACH = "catalyst_detach\n"; -require Module::Pluggable::Fast; - -# Helper script generation -our $CATALYST_SCRIPT_GEN = 27; - __PACKAGE__->mk_classdata($_) for qw/components arguments dispatcher engine log dispatcher_class engine_class context_class request_class response_class setup_finished/; @@ -58,7 +59,9 @@ __PACKAGE__->engine_class('Catalyst::Engine::CGI'); __PACKAGE__->request_class('Catalyst::Request'); __PACKAGE__->response_class('Catalyst::Response'); -our $VERSION = '5.64'; +# Remember to update this in Catalyst::Runtime as well! + +our $VERSION = '5.7006'; sub import { my ( $class, @arguments ) = @_; @@ -84,29 +87,32 @@ Catalyst - The Elegant MVC Web Application Framework =head1 SYNOPSIS - # use the helper to start a new application + # Install Catalyst::Devel for helpers and other development tools + # use the helper to create a new application catalyst.pl MyApp # add models, views, controllers - script/myapp_create.pl model Database DBIC dbi:SQLite:/path/to/db - script/myapp_create.pl view TT TT + script/myapp_create.pl model MyDatabase DBIC::Schema create=dynamic dbi:SQLite:/path/to/db + script/myapp_create.pl view MyTemplate TT script/myapp_create.pl controller Search # built in testserver -- use -r to restart automatically on changes + # --help to see all available options script/myapp_server.pl # command line testing interface script/myapp_test.pl /yada - ### in MyApp.pm + ### in lib/MyApp.pm use Catalyst qw/-Debug/; # include plugins here as well + ### In lib/MyApp/Controller/Root.pm (autocreated) sub foo : Global { # called for /foo, /foo/1, /foo/1/2, etc. my ( $self, $c, @args ) = @_; # args are qw/1 2/ for /foo/1/2 $c->stash->{template} = 'foo.tt'; # set the template # lookup something from db -- stash vars are passed to TT $c->stash->{data} = - MyApp::Model::Database::Foo->search( { country => $args[0] } ); + $c->model('Database::Foo')->search( { country => $args[0] } ); if ( $c->req->params->{bar} ) { # access GET or POST parameters $c->forward( 'bar' ); # process another action # do something else after forward returns @@ -124,7 +130,7 @@ Catalyst - The Elegant MVC Web Application Framework # called for all actions, from the top-most controller downwards sub auto : Private { my ( $self, $c ) = @_; - if ( !$c->user ) { + if ( !$c->user_exists ) { # Catalyst::Plugin::Authentication $c->res->redirect( '/login' ); # require login return 0; # abort request and go immediately to end() } @@ -153,21 +159,24 @@ Catalyst - The Elegant MVC Web Application Framework # called for /foo/bar/baz sub baz : Local { ... } - # first MyApp auto is called, then Foo auto, then this + # first Root auto is called, then Foo auto, then this sub auto : Private { ... } # powerful regular expression paths are also possible sub details : Regex('^product/(\w+)/details$') { my ( $self, $c ) = @_; # extract the (\w+) from the URI - my $product = $c->req->snippets->[0]; + my $product = $c->req->captures->[0]; } See L for additional information. =head1 DESCRIPTION -The key concept of Catalyst is DRY (Don't Repeat Yourself). +Catalyst is a modern framework for making web applications without the +pain usually associated with this process. This document is a reference +to the main Catalyst application. If you are a new user, we suggest you +start with L or L. See L for more documentation. @@ -178,19 +187,30 @@ C. use Catalyst qw/My::Module/; +If your plugin starts with a name other than C, you can +fully qualify the name by using a unary plus: + + use Catalyst qw/ + My::Module + +Fully::Qualified::Plugin::Name + /; + Special flags like C<-Debug> and C<-Engine> can also be specified as arguments when Catalyst is loaded: use Catalyst qw/-Debug My::Module/; The position of plugins and flags in the chain is important, because -they are loaded in exactly the order in which they appear. +they are loaded in the order in which they appear. The following flags are supported: =head2 -Debug -Enables debug output. +Enables debug output. You can also force this setting from the system +environment with CATALYST_DEBUG or _DEBUG. The environment +settings override the application, with _DEBUG having the highest +priority. =head2 -Engine @@ -203,7 +223,13 @@ C prefix of the engine name, i.e.: Forces Catalyst to use a specific home directory, e.g.: - use Catalyst qw[-Home=/usr/sri]; + use Catalyst qw[-Home=/usr/mst]; + +This can also be done in the shell environment by setting either the +C environment variable or C; where C +is replaced with the uppercased name of your application, any "::" in +the name will be replaced with underscores, e.g. MyApp::Web should use +MYAPP_WEB_HOME. If both variables are set, the MYAPP_HOME one will be used. =head2 -Log @@ -211,7 +237,7 @@ Specifies log level. =head1 METHODS -=head2 Information about the current request +=head2 INFORMATION ABOUT THE CURRENT REQUEST =head2 $c->action @@ -220,7 +246,7 @@ stringifies to the action name. See L. =head2 $c->namespace -Returns the namespace of the current action, i.e., the uri prefix +Returns the namespace of the current action, i.e., the URI prefix corresponding to the controller of the current action. For example: # in Controller::Foo::Bar @@ -230,29 +256,42 @@ corresponding to the controller of the current action. For example: =head2 $c->req -Returns the current L object. See -L. +Returns the current L object, giving access to +information about the current client request (including parameters, +cookies, HTTP headers, etc.). See L. -=head2 Processing and response to the current request +=head2 REQUEST FLOW HANDLING =head2 $c->forward( $action [, \@arguments ] ) =head2 $c->forward( $class, $method, [, \@arguments ] ) -Forwards processing to a private action. If you give a class name but no -method, C is called. You may also optionally pass arguments -in an arrayref. The action will receive the arguments in C<@_> and -C<$c-Ereq-Eargs>. Upon returning from the function, -C<$c-Ereq-Eargs> will be restored to the previous values. +Forwards processing to another action, by its private name. If you give a +class name but no method, C is called. You may also optionally +pass arguments in an arrayref. The action will receive the arguments in +C<@_> and C<< $c->req->args >>. Upon returning from the function, +C<< $c->req->args >> will be restored to the previous values. Any data Ced from the action forwarded to, will be returned by the call to forward. my $foodata = $c->forward('/foo'); $c->forward('index'); - $c->forward(qw/MyApp::Model::CDBI::Foo do_stuff/); + $c->forward(qw/MyApp::Model::DBIC::Foo do_stuff/); $c->forward('MyApp::View::TT'); +Note that forward implies an C<> around the call (actually +C does), thus de-fatalizing all 'dies' within the called +action. If you want C to propagate you need to do something like: + + $c->forward('foo'); + die $c->error if $c->error; + +Or make sure to always return true values from your actions and write +your code like this: + + $c->forward('foo') || return; + =cut sub forward { my $c = shift; $c->dispatcher->forward( $c, @_ ) } @@ -261,12 +300,54 @@ sub forward { my $c = shift; $c->dispatcher->forward( $c, @_ ) } =head2 $c->detach( $class, $method, [, \@arguments ] ) -The same as C, but doesn't return. +=head2 $c->detach() + +The same as C, but doesn't return to the previous action when +processing is finished. + +When called with no arguments it escapes the processing chain entirely. =cut sub detach { my $c = shift; $c->dispatcher->detach( $c, @_ ) } +=head2 $c->response + +=head2 $c->res + +Returns the current L object, see there for details. + +=head2 $c->stash + +Returns a hashref to the stash, which may be used to store data and pass +it between components during a request. You can also set hash keys by +passing arguments. The stash is automatically sent to the view. The +stash is cleared at the end of a request; it cannot be used for +persistent storage (for this you must use a session; see +L for a complete system integrated with +Catalyst). + + $c->stash->{foo} = $bar; + $c->stash( { moose => 'majestic', qux => 0 } ); + $c->stash( bar => 1, gorch => 2 ); # equivalent to passing a hashref + + # stash is automatically passed to the view for use in a template + $c->forward( 'MyApp::View::TT' ); + +=cut + +sub stash { + my $c = shift; + if (@_) { + my $stash = @_ > 1 ? {@_} : $_[0]; + croak('stash takes a hash or hashref') unless ref $stash; + foreach my $key ( keys %$stash ) { + $c->{stash}->{$key} = $stash->{$key}; + } + } + return $c->{stash}; +} + =head2 $c->error =head2 $c->error($error, ...) @@ -283,130 +364,142 @@ Add a new error. $c->error('Something bad happened'); -Clear errors. You probably don't want to clear the errors unless you are -implementing a custom error screen. - - $c->error(0); - =cut sub error { my $c = shift; if ( $_[0] ) { my $error = ref $_[0] eq 'ARRAY' ? $_[0] : [@_]; + croak @$error unless ref $c; push @{ $c->{error} }, @$error; } elsif ( defined $_[0] ) { $c->{error} = undef } return $c->{error} || []; } -=head2 $c->response -=head2 $c->res +=head2 $c->state -Returns the current L object. +Contains the return value of the last executed action. -=head2 $c->stash +=head2 $c->clear_errors -Returns a hashref to the stash, which may be used to store data and pass -it between components during a request. You can also set hash keys by -passing arguments. The stash is automatically sent to the view. The -stash is cleared at the end of a request; it cannot be used for -persistent storage. +Clear errors. You probably don't want to clear the errors unless you are +implementing a custom error screen. - $c->stash->{foo} = $bar; - $c->stash( { moose => 'majestic', qux => 0 } ); - $c->stash( bar => 1, gorch => 2 ); # equivalent to passing a hashref - - # stash is automatically passed to the view for use in a template - $c->forward( 'MyApp::V::TT' ); +This is equivalent to running + + $c->error(0); =cut -sub stash { +sub clear_errors { my $c = shift; - if (@_) { - my $stash = @_ > 1 ? {@_} : $_[0]; - while ( my ( $key, $val ) = each %$stash ) { - $c->{stash}->{$key} = $val; - } - } - return $c->{stash}; + $c->error(0); } -=head2 $c->state -Contains the return value of the last executed action. +# search via regex +sub _comp_search { + my ( $c, @names ) = @_; -=head2 Component Accessors + foreach my $name (@names) { + foreach my $component ( keys %{ $c->components } ) { + return $c->components->{$component} if $component =~ /$name/i; + } + } -=head2 $c->comp($name) + return undef; +} -=head2 $c->component($name) +# try explicit component names +sub _comp_explicit { + my ( $c, @names ) = @_; -Gets a component object by name. This method is no longer recommended, -unless you want to get a specific component by full -class. C<$c-Econtroller>, C<$c-Emodel>, and C<$c-Eview> -should be used instead. + foreach my $try (@names) { + return $c->components->{$try} if ( exists $c->components->{$try} ); + } -=cut + return undef; +} -sub component { - my $c = shift; +# like component, but try just these prefixes before regex searching, +# and do not try to return "sort keys %{ $c->components }" +sub _comp_prefixes { + my ( $c, $name, @prefixes ) = @_; - if (@_) { + my $appclass = ref $c || $c; - my $name = shift; + my @names = map { "${appclass}::${_}::${name}" } @prefixes; - my $appclass = ref $c || $c; + my $comp = $c->_comp_explicit(@names); + return $comp if defined($comp); + $comp = $c->_comp_search($name); + return $comp; +} - my @names = ( - $name, "${appclass}::${name}", - map { "${appclass}::${_}::${name}" } - qw/Model M Controller C View V/ - ); +# Find possible names for a prefix - foreach my $try (@names) { +sub _comp_names { + my ( $c, @prefixes ) = @_; - if ( exists $c->components->{$try} ) { + my $appclass = ref $c || $c; - my $comp = $c->components->{$try}; - if ( eval { $comp->can('ACCEPT_CONTEXT'); } ) { - return $comp->ACCEPT_CONTEXT($c); - } - else { return $comp } - } - } + my @pre = map { "${appclass}::${_}::" } @prefixes; - foreach my $component ( keys %{ $c->components } ) { - my $comp; - $comp = $c->components->{$component} if $component =~ /$name/i; - if ($comp) { - if ( ref $comp && $comp->can('ACCEPT_CONTEXT') ) { - return $comp->ACCEPT_CONTEXT($c); - } - else { return $comp } + my @names; + + COMPONENT: foreach my $comp ($c->component) { + foreach my $p (@pre) { + if ($comp =~ s/^$p//) { + push(@names, $comp); + next COMPONENT; } } - } - return sort keys %{ $c->components }; + return @names; } +# Return a component if only one matches. +sub _comp_singular { + my ( $c, @prefixes ) = @_; + + my $appclass = ref $c || $c; + + my ( $comp, $rest ) = + map { $c->_comp_search("^${appclass}::${_}::") } @prefixes; + return $comp unless $rest; +} + +# Filter a component before returning by calling ACCEPT_CONTEXT if available +sub _filter_component { + my ( $c, $comp, @args ) = @_; + if ( eval { $comp->can('ACCEPT_CONTEXT'); } ) { + return $comp->ACCEPT_CONTEXT( $c, @args ); + } + else { return $comp } +} + +=head2 COMPONENT ACCESSORS + =head2 $c->controller($name) Gets a L instance by name. $c->controller('Foo')->do_stuff; +If the name is omitted, will return the controller for the dispatched +action. + =cut sub controller { - my ( $c, $name ) = @_; - my $controller = $c->comp("Controller::$name"); - return $controller if defined $controller; - return $c->comp("C::$name"); + my ( $c, $name, @args ) = @_; + return $c->_filter_component( $c->_comp_prefixes( $name, qw/Controller C/ ), + @args ) + if ($name); + return $c->component( $c->action->class ); } =head2 $c->model($name) @@ -415,31 +508,135 @@ Gets a L instance by name. $c->model('Foo')->do_stuff; +If the name is omitted, it will look for + - a model object in $c->stash{current_model_instance}, then + - a model name in $c->stash->{current_model}, then + - a config setting 'default_model', or + - check if there is only one model, and return it if that's the case. + =cut sub model { - my ( $c, $name ) = @_; - my $model = $c->comp("Model::$name"); - return $model if defined $model; - return $c->comp("M::$name"); + my ( $c, $name, @args ) = @_; + return $c->_filter_component( $c->_comp_prefixes( $name, qw/Model M/ ), + @args ) + if $name; + if (ref $c) { + return $c->stash->{current_model_instance} + if $c->stash->{current_model_instance}; + return $c->model( $c->stash->{current_model} ) + if $c->stash->{current_model}; + return $c->model( $c->config->{default_model} ) + if $c->config->{default_model}; + } + return $c->_filter_component( $c->_comp_singular(qw/Model M/), @args ); + } +=head2 $c->controllers + +Returns the available names which can be passed to $c->controller + +=cut + +sub controllers { + my ( $c ) = @_; + return $c->_comp_names(qw/Controller C/); +} + + =head2 $c->view($name) Gets a L instance by name. $c->view('Foo')->do_stuff; +If the name is omitted, it will look for + - a view object in $c->stash{current_view_instance}, then + - a view name in $c->stash->{current_view}, then + - a config setting 'default_view', or + - check if there is only one view, and return it if that's the case. + =cut sub view { - my ( $c, $name ) = @_; - my $view = $c->comp("View::$name"); - return $view if defined $view; - return $c->comp("V::$name"); + my ( $c, $name, @args ) = @_; + return $c->_filter_component( $c->_comp_prefixes( $name, qw/View V/ ), + @args ) + if $name; + if (ref $c) { + return $c->stash->{current_view_instance} + if $c->stash->{current_view_instance}; + return $c->view( $c->stash->{current_view} ) + if $c->stash->{current_view}; + return $c->view( $c->config->{default_view} ) + if $c->config->{default_view}; + } + return $c->_filter_component( $c->_comp_singular(qw/View V/) ); +} + +=head2 $c->models + +Returns the available names which can be passed to $c->model + +=cut + +sub models { + my ( $c ) = @_; + return $c->_comp_names(qw/Model M/); } -=head2 Class data and helper classes + +=head2 $c->views + +Returns the available names which can be passed to $c->view + +=cut + +sub views { + my ( $c ) = @_; + return $c->_comp_names(qw/View V/); +} + +=head2 $c->comp($name) + +=head2 $c->component($name) + +Gets a component object by name. This method is not recommended, +unless you want to get a specific component by full +class. C<< $c->controller >>, C<< $c->model >>, and C<< $c->view >> +should be used instead. + +=cut + +sub component { + my $c = shift; + + if (@_) { + + my $name = shift; + + my $appclass = ref $c || $c; + + my @names = ( + $name, "${appclass}::${name}", + map { "${appclass}::${_}::${name}" } + qw/Model M Controller C View V/ + ); + + my $comp = $c->_comp_explicit(@names); + return $c->_filter_component( $comp, @_ ) if defined($comp); + + $comp = $c->_comp_search($name); + return $c->_filter_component( $comp, @_ ) if defined($comp); + } + + return sort keys %{ $c->components }; +} + + + +=head2 CLASS DATA AND HELPER CLASSES =head2 $c->config @@ -447,12 +644,14 @@ Returns or takes a hashref containing the application's configuration. __PACKAGE__->config( { db => 'dsn:SQLite:foo.db' } ); -You can also use a L config file like myapp.yml in your -applications home directory. +You can also use a C, C or C config file +like myapp.yml in your applications home directory. See +L. --- db: dsn:SQLite:foo.db + =cut sub config { @@ -464,10 +663,31 @@ sub config { $c->NEXT::config(@_); } +=head2 $c->log + +Returns the logging object instance. Unless it is already set, Catalyst +sets this up with a L object. To use your own log class, +set the logger with the C<< __PACKAGE__->log >> method prior to calling +C<< __PACKAGE__->setup >>. + + __PACKAGE__->log( MyLogger->new ); + __PACKAGE__->setup; + +And later: + + $c->log->info( 'Now logging with my own logger!' ); + +Your log class should implement the methods described in +L. + + =head2 $c->debug Overload to enable debug messages (same as -Debug option). +Note that this is a static method, not an accessor and should be overloaded +by declaring "sub debug { 1 }" in your MyApp.pm, not by calling $c->debug(1). + =cut sub debug { 0 } @@ -482,31 +702,13 @@ L. Returns the engine instance. Stringifies to the class name. See L. -=head2 $c->log - -Returns the logging object instance. Unless it is already set, Catalyst sets -this up with a L object. To use your own log class, set the -logger with the C<< __PACKAGE__->log >> method prior to calling -C<< __PACKAGE__->setup >>. - - __PACKAGE__->log( MyLogger->new ); - __PACKAGE__->setup; - -And later: - - $c->log->info( 'Now logging with my own logger!' ); - -Your log class should implement the methods described in the -L man page. - -=cut -=head2 Utility methods +=head2 UTILITY METHODS =head2 $c->path_to(@path) -Merges C<@path> with C<$c-Econfig-E{home}> and returns a -L object. +Merges C<@path> with C<< $c->config->{home} >> and returns a +L object. For example: @@ -516,9 +718,9 @@ For example: sub path_to { my ( $c, @path ) = @_; - my $path = dir( $c->config->{home}, @path ); + my $path = Path::Class::Dir->new( $c->config->{home}, @path ); if ( -d $path ) { return $path } - else { return file( $c->config->{home}, @path ) } + else { return Path::Class::File->new( $c->config->{home}, @path ) } } =head2 $c->plugin( $name, $class, @args ) @@ -534,12 +736,7 @@ loads and instantiates the given class. sub plugin { my ( $class, $name, $plugin, @args ) = @_; - $plugin->require; - - if ( my $error = $UNIVERSAL::require::ERROR ) { - Catalyst::Exception->throw( - message => qq/Couldn't load instant plugin "$plugin", "$error"/ ); - } + $class->_register_plugin( $plugin, 1 ); eval { $plugin->import }; $class->mk_classdata($name); @@ -571,6 +768,9 @@ Catalyst> line. sub setup { my ( $class, @arguments ) = @_; + $class->log->warn("Running setup twice is not a good idea.") + if ( $class->setup_finished ); + unless ( $class->isa('Catalyst') ) { Catalyst::Exception->throw( @@ -615,8 +815,9 @@ sub setup { } } - $class->log->warn( - <<"EOF") if ( $ENV{CATALYST_SCRIPT_GEN} && ( $ENV{CATALYST_SCRIPT_GEN} < $Catalyst::CATALYST_SCRIPT_GEN ) ); + eval { require Catalyst::Devel; }; + if( !$@ && $ENV{CATALYST_SCRIPT_GEN} && ( $ENV{CATALYST_SCRIPT_GEN} < $Catalyst::Devel::CATALYST_SCRIPT_GEN ) ) { + $class->log->warn(<<"EOF"); You are running an old script! Please update by running (this will overwrite existing files): @@ -624,23 +825,17 @@ You are running an old script! or (this will not overwrite existing files): catalyst.pl -scripts $class -EOF +EOF + } + if ( $class->debug ) { - - my @plugins = (); - - { - no strict 'refs'; - @plugins = - map { $_ . ' ' . ( $_->VERSION || '' ) } - grep { /^Catalyst::Plugin/ } @{"$class\::ISA"}; - } + my @plugins = map { "$_ " . ( $_->VERSION || '' ) } $class->registered_plugins; if (@plugins) { - my $t = Text::SimpleTable->new(76); + my $t = Text::SimpleTable->new(74); $t->row($_) for @plugins; - $class->log->debug( "Loaded plugins:\n" . $t->draw ); + $class->log->debug( "Loaded plugins:\n" . $t->draw . "\n" ); } my $dispatcher = $class->dispatcher; @@ -670,12 +865,12 @@ EOF $class->setup_components; if ( $class->debug ) { - my $t = Text::SimpleTable->new( [ 65, 'Class' ], [ 8, 'Type' ] ); + my $t = Text::SimpleTable->new( [ 63, 'Class' ], [ 8, 'Type' ] ); for my $comp ( sort keys %{ $class->components } ) { my $type = ref $class->components->{$comp} ? 'instance' : 'class'; $t->row( $comp, $type ); } - $class->log->debug( "Loaded components:\n" . $t->draw ) + $class->log->debug( "Loaded components:\n" . $t->draw . "\n" ) if ( keys %{ $class->components } ); } @@ -693,12 +888,20 @@ EOF $class->setup_finished(1); } -=head2 $c->uri_for( $path, [ @args ] ) +=head2 $c->uri_for( $path, @args?, \%query_values? ) + +Merges path with C<< $c->request->base >> for absolute URIs and with +C<< $c->namespace >> for relative URIs, then returns a normalized L +object. If any args are passed, they are added at the end of the path. +If the last argument to C is a hash reference, it is assumed to +contain GET parameter key/value pairs, which will be appended to the URI +in standard fashion. -Merges path with C<$c-Erequest-Ebase> for absolute uri's and -with C<$c-Enamespace> for relative uri's, then returns a -normalized L object. If any args are passed, they are added at the -end of the path. +Instead of C<$path>, you can also optionally pass a C<$action> object +which will be resolved to a path using +C<< $c->dispatcher->uri_for_action >>; if the first element of +C<@args> is an arrayref it is treated as a list of captures to be passed +to C. =cut @@ -708,19 +911,38 @@ sub uri_for { my $basepath = $base->path; $basepath =~ s/\/$//; $basepath .= '/'; - my $namespace = $c->namespace; + my $namespace = $c->namespace || ''; + + if ( Scalar::Util::blessed($path) ) { # action object + my $captures = ( scalar @args && ref $args[0] eq 'ARRAY' + ? shift(@args) + : [] ); + $path = $c->dispatcher->uri_for_action($path, $captures); + return undef unless defined($path); + $path = '/' if $path eq ''; + } # massage namespace, empty if absolute path - $namespace =~ s/^\///; + $namespace =~ s/^\/// if $namespace; $namespace .= '/' if $namespace; $path ||= ''; $namespace = '' if $path =~ /^\//; $path =~ s/^\///; + $path =~ s/\?/%3F/g; - my $params = (scalar @args && ref $args[$#args] eq 'HASH' ? pop @args : {}); + my $params = + ( scalar @args && ref $args[$#args] eq 'HASH' ? pop @args : {} ); + for my $value ( values %$params ) { + next unless defined $value; + for ( ref $value eq 'ARRAY' ? @$value : $value ) { + $_ = "$_"; + utf8::encode( $_ ); + } + }; + # join args with '/', or a blank string - my $args = ( scalar @args ? '/' . join( '/', @args ) : '' ); + my $args = ( scalar @args ? '/' . join( '/', map {s/\?/%3F/g; $_} @args ) : '' ); $args =~ s/^\/// unless $path; my $res = URI->new_abs( URI->new_abs( "$path$args", "$basepath$namespace" ), $base ) @@ -763,7 +985,6 @@ sub welcome_message { text-align: left; background-color: #ccc; border: 1px solid #aaa; - -moz-border-radius: 10px; } p, h1, h2 { margin-left: 20px; @@ -793,7 +1014,6 @@ sub welcome_message { margin: 10px; background-color: #fff; border: 1px solid #aaa; - -moz-border-radius: 10px; } h1 { font-size: 0.9em; @@ -826,7 +1046,7 @@ sub welcome_message {

Catalyst Logo

-

Welcome to the wonderful world of Catalyst. +

Welcome to the world of Catalyst. This MVC framework will make web development something you had never expected it to be: Fun, rewarding, and quick.

@@ -834,10 +1054,14 @@ sub welcome_message {

That really depends on what you want to do. We do, however, provide you with a few starting points.

If you want to jump right into web development with Catalyst - you might want to check out the documentation.

-
perldoc Catalyst::Manual::Intro
-perldoc Catalyst::Manual::Tutorial
-perldoc Catalyst::Manual
+ you might want want to start with a tutorial.

+
perldoc Catalyst::Manual::Tutorial
+
+

Afterwards you can go on to check out a more complete look at our features.

+
+perldoc Catalyst::Manual::Intro
+
+

What to do next?

Next it's time to write an actual application. Use the helper scripts to generate controllers, @@ -917,7 +1141,10 @@ that will be dumped on the error page in debug mode. sub dump_these { my $c = shift; - [ Request => $c->req ], [ Response => $c->res ], [ Stash => $c->stash ],; + [ Request => $c->req ], + [ Response => $c->res ], + [ Stash => $c->stash ], + [ Config => $c->config ]; } =head2 $c->engine_class @@ -936,120 +1163,132 @@ sub execute { $class = $c->component($class) || $class; $c->state(0); - if ( $c->debug ) { + if ( $c->depth >= $RECURSION ) { my $action = "$code"; - $action = "/$action" unless $action =~ /\-\>/; - $c->counter->{"$code"}++; + $action = "/$action" unless $action =~ /->/; + my $error = qq/Deep recursion detected calling "$action"/; + $c->log->error($error); + $c->error($error); + $c->state(0); + return $c->state; + } + + my $stats_info = $c->_stats_start_execute( $code ) if $c->debug; - if ( $c->counter->{"$code"} > $RECURSION ) { - my $error = qq/Deep recursion detected in "$action"/; - $c->log->error($error); + push( @{ $c->stack }, $code ); + + eval { $c->state( &$code( $class, $c, @{ $c->req->args } ) || 0 ) }; + + $c->_stats_finish_execute( $stats_info ) if $c->debug and $stats_info; + + my $last = pop( @{ $c->stack } ); + + if ( my $error = $@ ) { + if ( !ref($error) and $error eq $DETACH ) { die $DETACH if $c->depth > 1 } + else { + unless ( ref $error ) { + no warnings 'uninitialized'; + chomp $error; + my $class = $last->class; + my $name = $last->name; + $error = qq/Caught exception in $class->$name "$error"/; + } $c->error($error); $c->state(0); - return $c->state; } + } + return $c->state; +} - # determine if the call was the result of a forward - # this is done by walking up the call stack and looking for a calling - # sub of Catalyst::forward before the eval - my $callsub = q{}; - for my $index ( 1 .. 10 ) { - last - if ( ( caller($index) )[0] eq 'Catalyst' - && ( caller($index) )[3] eq '(eval)' ); - - if ( ( caller($index) )[3] =~ /forward$/ ) { - $callsub = ( caller($index) )[3]; - $action = "-> $action"; - last; - } - } +sub _stats_start_execute { + my ( $c, $code ) = @_; - my $node = Tree::Simple->new( - { - action => $action, - elapsed => undef, # to be filled in later - } - ); - $node->setUID( "$code" . $c->counter->{"$code"} ); + return if ( ( $code->name =~ /^_.*/ ) + && ( !$c->config->{show_internal_actions} ) ); - unless ( ( $code->name =~ /^_.*/ ) - && ( !$c->config->{show_internal_actions} ) ) - { + $c->counter->{"$code"}++; - # is this a root-level call or a forwarded call? - if ( $callsub =~ /forward$/ ) { - - # forward, locate the caller - if ( my $parent = $c->stack->[-1] ) { - my $visitor = Tree::Simple::Visitor::FindByUID->new; - $visitor->searchForUID( - "$parent" . $c->counter->{"$parent"} ); - $c->{stats}->accept($visitor); - if ( my $result = $visitor->getResult ) { - $result->addChild($node); - } - } - else { + my $action = "$code"; + $action = "/$action" unless $action =~ /->/; - # forward with no caller may come from a plugin - $c->{stats}->addChild($node); - } - } - else { + # determine if the call was the result of a forward + # this is done by walking up the call stack and looking for a calling + # sub of Catalyst::forward before the eval + my $callsub = q{}; + for my $index ( 2 .. 11 ) { + last + if ( ( caller($index) )[0] eq 'Catalyst' + && ( caller($index) )[3] eq '(eval)' ); - # root-level call - $c->{stats}->addChild($node); - } + if ( ( caller($index) )[3] =~ /forward$/ ) { + $callsub = ( caller($index) )[3]; + $action = "-> $action"; + last; } } - push( @{ $c->stack }, $code ); - my $elapsed = 0; - my $start = 0; - $start = [gettimeofday] if $c->debug; - eval { $c->state( &$code( $class, $c, @{ $c->req->args } ) || 0 ) }; - $elapsed = tv_interval($start) if $c->debug; - - if ( $c->debug ) { - unless ( ( $code->name =~ /^_.*/ ) - && ( !$c->config->{show_internal_actions} ) ) + my $node = Tree::Simple->new( { + action => $action, + elapsed => undef, # to be filled in later + comment => "", + } + ); + $node->setUID( "$code" . $c->counter->{"$code"} ); - # FindByUID uses an internal die, so we save the existing error - my $error = $@; + # is this a root-level call or a forwarded call? + if ( $callsub =~ /forward$/ ) { - # locate the node in the tree and update the elapsed time + # forward, locate the caller + if ( my $parent = $c->stack->[-1] ) { my $visitor = Tree::Simple::Visitor::FindByUID->new; - $visitor->searchForUID( "$code" . $c->counter->{"$code"} ); - $c->{stats}->accept($visitor); + $visitor->searchForUID( + "$parent" . $c->counter->{"$parent"} ); + $c->stats->accept($visitor); if ( my $result = $visitor->getResult ) { - my $value = $result->getNodeValue; - $value->{elapsed} = sprintf( '%fs', $elapsed ); - $result->setNodeValue($value); + $result->addChild($node); } + } + else { - # restore error - $@ = $error || undef; + # forward with no caller may come from a plugin + $c->stats->addChild($node); } } - my $last = ${ $c->stack }[-1]; - pop( @{ $c->stack } ); + else { - if ( my $error = $@ ) { - if ( $error eq $DETACH ) { die $DETACH if $c->depth > 1 } - else { - unless ( ref $error ) { - chomp $error; - my $class = $last->class; - my $name = $last->name; - $error = qq/Caught exception in $class->$name "$error"/; - } - $c->error($error); - $c->state(0); - } + # root-level call + $c->stats->addChild($node); } - return $c->state; + + return { + start => [gettimeofday], + node => $node, + }; +} + +sub _stats_finish_execute { + my ( $c, $info ) = @_; + my $elapsed = tv_interval $info->{start}; + my $value = $info->{node}->getNodeValue; + $value->{elapsed} = sprintf( '%fs', $elapsed ); +} + +=head2 $c->_localize_fields( sub { }, \%keys ); + +=cut + +sub _localize_fields { + my ( $c, $localized, $code ) = ( @_ ); + + my $request = delete $localized->{request} || {}; + my $response = delete $localized->{response} || {}; + + local @{ $c }{ keys %$localized } = values %$localized; + local @{ $c->request }{ keys %$request } = values %$request; + local @{ $c->response }{ keys %$response } = values %$response; + + $code->(); } =head2 $c->finalize @@ -1065,21 +1304,46 @@ sub finalize { $c->log->error($error); } - $c->finalize_uploads; - - # Error - if ( $#{ $c->error } >= 0 ) { - $c->finalize_error; + # Allow engine to handle finalize flow (for POE) + if ( $c->engine->can('finalize') ) { + $c->engine->finalize($c); } + else { - $c->finalize_headers; + $c->finalize_uploads; - # HEAD request - if ( $c->request->method eq 'HEAD' ) { - $c->response->body(''); + # Error + if ( $#{ $c->error } >= 0 ) { + $c->finalize_error; + } + + $c->finalize_headers; + + # HEAD request + if ( $c->request->method eq 'HEAD' ) { + $c->response->body(''); + } + + $c->finalize_body; } + + if ($c->debug) { + my $elapsed = sprintf '%f', tv_interval($c->stats->getNodeValue); + my $av = sprintf '%.3f', ( $elapsed == 0 ? '??' : ( 1 / $elapsed ) ); + + my $t = Text::SimpleTable->new( [ 62, 'Action' ], [ 9, 'Time' ] ); + $c->stats->traverse( + sub { + my $action = shift; + my $stat = $action->getNodeValue; + $t->row( ( q{ } x $action->getDepth ) . $stat->{action} . $stat->{comment}, + $stat->{elapsed} || '??' ); + } + ); - $c->finalize_body; + $c->log->info( + "Request took ${elapsed}s ($av/s)\n" . $t->draw . "\n" ); + } return $c->response->status; } @@ -1130,7 +1394,8 @@ sub finalize_headers { if ( $c->response->body && !$c->response->content_length ) { # get the length from a filehandle - if ( ref $c->response->body && $c->response->body->can('read') ) { + if ( blessed( $c->response->body ) && $c->response->body->can('read') ) + { if ( my $stat = stat $c->response->body ) { $c->response->content_length( $stat->size ); } @@ -1194,7 +1459,7 @@ namespaces. sub get_actions { my $c = shift; $c->dispatcher->get_actions( $c, @_ ) } -=head2 handle_request( $class, @arguments ) +=head2 $c->handle_request( $class, @arguments ) Called to handle each HTTP request. @@ -1206,38 +1471,16 @@ sub handle_request { # Always expect worst case! my $status = -1; eval { - my $stats = ( $class->debug ) ? Tree::Simple->new: q{}; - - my $handler = sub { - my $c = $class->prepare(@arguments); - $c->{stats} = $stats; - $c->dispatch; - return $c->finalize; - }; - - if ( $class->debug ) { - my $start = [gettimeofday]; - $status = &$handler; - my $elapsed = tv_interval $start; - $elapsed = sprintf '%f', $elapsed; - my $av = sprintf '%.3f', - ( $elapsed == 0 ? '??' : ( 1 / $elapsed ) ); - my $t = Text::SimpleTable->new( [ 64, 'Action' ], [ 9, 'Time' ] ); - - $stats->traverse( - sub { - my $action = shift; - my $stat = $action->getNodeValue; - $t->row( ( q{ } x $action->getDepth ) . $stat->{action}, - $stat->{elapsed} || '??' ); - } - ); - - $class->log->info( - "Request took ${elapsed}s ($av/s)\n" . $t->draw ); + if ($class->debug) { + my $secs = time - $START || 1; + my $av = sprintf '%.3f', $COUNT / $secs; + my $time = localtime time; + $class->log->info("*** Request $COUNT ($av/s) [$$] [$time] ***"); } - else { $status = &$handler } + my $c = $class->prepare(@arguments); + $c->dispatch; + $status = $c->finalize; }; if ( my $error = $@ ) { @@ -1274,7 +1517,7 @@ sub prepare { parameters => {}, query_parameters => {}, secure => 0, - snippets => [], + captures => [], uploads => {} } ), @@ -1291,33 +1534,35 @@ sub prepare { } ); + if ( $c->debug ) { + $c->stats(Tree::Simple->new([gettimeofday])); + $c->res->headers->header( 'X-Catalyst' => $Catalyst::VERSION ); + } + # For on-demand data $c->request->{_context} = $c; $c->response->{_context} = $c; weaken( $c->request->{_context} ); weaken( $c->response->{_context} ); - if ( $c->debug ) { - my $secs = time - $START || 1; - my $av = sprintf '%.3f', $COUNT / $secs; - $c->log->debug('**********************************'); - $c->log->debug("* Request $COUNT ($av/s) [$$]"); - $c->log->debug('**********************************'); - $c->res->headers->header( 'X-Catalyst' => $Catalyst::VERSION ); + # Allow engine to direct the prepare flow (for POE) + if ( $c->engine->can('prepare') ) { + $c->engine->prepare( $c, @arguments ); + } + else { + $c->prepare_request(@arguments); + $c->prepare_connection; + $c->prepare_query_parameters; + $c->prepare_headers; + $c->prepare_cookies; + $c->prepare_path; + + # On-demand parsing + $c->prepare_body unless $c->config->{parse_on_demand}; } - - $c->prepare_request(@arguments); - $c->prepare_connection; - $c->prepare_query_parameters; - $c->prepare_headers; - $c->prepare_cookies; - $c->prepare_path; - - # On-demand parsing - $c->prepare_body unless $c->config->{parse_on_demand}; my $method = $c->req->method || ''; - my $path = $c->req->path || ''; + my $path = $c->req->path || '/'; my $address = $c->req->address || ''; $c->log->debug(qq/"$method" request for "$path" from "$address"/) @@ -1330,7 +1575,7 @@ sub prepare { =head2 $c->prepare_action -Prepares action. +Prepares action. See L. =cut @@ -1354,7 +1599,7 @@ sub prepare_body { $c->prepare_uploads; if ( $c->debug && keys %{ $c->req->body_parameters } ) { - my $t = Text::SimpleTable->new( [ 37, 'Key' ], [ 36, 'Value' ] ); + my $t = Text::SimpleTable->new( [ 35, 'Parameter' ], [ 36, 'Value' ] ); for my $key ( sort keys %{ $c->req->body_parameters } ) { my $param = $c->req->body_parameters->{$key}; my $value = defined($param) ? $param : ''; @@ -1369,6 +1614,8 @@ sub prepare_body { Prepares a chunk of data before sending it to L. +See L. + =cut sub prepare_body_chunk { @@ -1446,7 +1693,7 @@ sub prepare_query_parameters { $c->engine->prepare_query_parameters( $c, @_ ); if ( $c->debug && keys %{ $c->request->query_parameters } ) { - my $t = Text::SimpleTable->new( [ 37, 'Key' ], [ 36, 'Value' ] ); + my $t = Text::SimpleTable->new( [ 35, 'Parameter' ], [ 36, 'Value' ] ); for my $key ( sort keys %{ $c->req->query_parameters } ) { my $param = $c->req->query_parameters->{$key}; my $value = defined($param) ? $param : ''; @@ -1486,8 +1733,8 @@ sub prepare_uploads { if ( $c->debug && keys %{ $c->request->uploads } ) { my $t = Text::SimpleTable->new( - [ 12, 'Key' ], - [ 28, 'Filename' ], + [ 12, 'Parameter' ], + [ 26, 'Filename' ], [ 18, 'Type' ], [ 9, 'Size' ] ); @@ -1523,7 +1770,7 @@ Reads a chunk of data from the request body. This method is designed to be used in a while loop, reading C<$maxlength> bytes on every call. C<$maxlength> defaults to the size of the request if not specified. -You have to set Cconfig-E{parse_on_demand}> to use this +You have to set C<< MyApp->config->{parse_on_demand} >> to use this directly. =cut @@ -1556,64 +1803,74 @@ sub setup_actions { my $c = shift; $c->dispatcher->setup_actions( $c, @_ ) } =head2 $c->setup_components -Sets up components. +Sets up components. Specify a C config option to pass +additional options directly to L. To add additional +search paths, specify a key named C as an array +reference. Items in the array beginning with C<::> will have the +application class name prepended to them. =cut sub setup_components { my $class = shift; - my $callback = sub { - my ( $component, $context ) = @_; - - unless ( $component->can('COMPONENT') ) { - return $component; + my @paths = qw( ::Controller ::C ::Model ::M ::View ::V ); + my $config = $class->config->{ setup_components }; + my $extra = delete $config->{ search_extra } || []; + + push @paths, @$extra; + + my $locator = Module::Pluggable::Object->new( + search_path => [ map { s/^(?=::)/$class/; $_; } @paths ], + %$config + ); + + for my $component ( sort { length $a <=> length $b } $locator->plugins ) { + Catalyst::Utils::ensure_class_loaded( $component, { ignore_loaded => 1 } ); + + my $module = $class->setup_component( $component ); + my %modules = ( + $component => $module, + map { + $_ => $class->setup_component( $_ ) + } Devel::InnerPackage::list_packages( $component ) + ); + + for my $key ( keys %modules ) { + $class->components->{ $key } = $modules{ $key }; } + } +} - my $suffix = Catalyst::Utils::class2classsuffix($component); - my $config = $class->config->{$suffix} || {}; - - my $instance; - - eval { $instance = $component->COMPONENT( $context, $config ); }; +=head2 $c->setup_component - if ( my $error = $@ ) { +=cut - chomp $error; +sub setup_component { + my( $class, $component ) = @_; - Catalyst::Exception->throw( message => - qq/Couldn't instantiate component "$component", "$error"/ ); - } + unless ( $component->can( 'COMPONENT' ) ) { + return $component; + } - Catalyst::Exception->throw( message => -qq/Couldn't instantiate component "$component", "COMPONENT() didn't return a object"/ - ) - unless ref $instance; - return $instance; - }; + my $suffix = Catalyst::Utils::class2classsuffix( $component ); + my $config = $class->config->{ $suffix } || {}; - eval "package $class;\n" . q!Module::Pluggable::Fast->import( - name => '_catalyst_components', - search => [ - "$class\::Controller", "$class\::C", - "$class\::Model", "$class\::M", - "$class\::View", "$class\::V" - ], - callback => $callback - ); - !; + my $instance = eval { $component->COMPONENT( $class, $config ); }; if ( my $error = $@ ) { - chomp $error; - Catalyst::Exception->throw( - message => qq/Couldn't load components "$error"/ ); + message => qq/Couldn't instantiate component "$component", "$error"/ + ); } - for my $component ( $class->_catalyst_components($class) ) { - $class->components->{ ref $component || $component } = $component; - } + Catalyst::Exception->throw( + message => + qq/Couldn't instantiate component "$component", "COMPONENT() didn't return an object-like value"/ + ) unless eval { $instance->can( 'can' ) }; + + return $instance; } =head2 $c->setup_dispatcher @@ -1642,11 +1899,8 @@ sub setup_dispatcher { $dispatcher = $class->dispatcher_class; } - $dispatcher->require; - - if ($@) { - Catalyst::Exception->throw( - message => qq/Couldn't load dispatcher "$dispatcher", "$@"/ ); + unless (Class::Inspector->loaded($dispatcher)) { + require Class::Inspector->filename($dispatcher); } # dispatcher instance @@ -1737,12 +1991,8 @@ sub setup_engine { $engine = $class->engine_class; } - $engine->require; - - if ($@) { - Catalyst::Exception->throw( message => -qq/Couldn't load engine "$engine" (maybe you forgot to install it?), "$@"/ - ); + unless (Class::Inspector->loaded($engine)) { + require Class::Inspector->filename($engine); } # check for old engines that are no longer compatible @@ -1795,6 +2045,7 @@ sub setup_home { } if ( $ENV{ uc($class) . '_HOME' } ) { + $class =~ s/::/_/g; $home = $ENV{ uc($class) . '_HOME' }; } @@ -1804,7 +2055,7 @@ sub setup_home { if ($home) { $class->config->{home} ||= $home; - $class->config->{root} ||= dir($home)->subdir('root'); + $class->config->{root} ||= Path::Class::Dir->new($home)->subdir('root'); } } @@ -1841,31 +2092,67 @@ Sets up plugins. =cut -sub setup_plugins { - my ( $class, $plugins ) = @_; +=head2 $c->registered_plugins - $plugins ||= []; - for my $plugin ( reverse @$plugins ) { +Returns a sorted list of the plugins which have either been stated in the +import list or which have been added via C<< MyApp->plugin(@args); >>. - $plugin = "Catalyst::Plugin::$plugin"; +If passed a given plugin name, it will report a boolean value indicating +whether or not that plugin is loaded. A fully qualified name is required if +the plugin name does not begin with C. - $plugin->require; + if ($c->registered_plugins('Some::Plugin')) { + ... + } - if ($@) { - Catalyst::Exception->throw( - message => qq/Couldn't load plugin "$plugin", "$@"/ ); +=cut + +{ + + sub registered_plugins { + my $proto = shift; + return sort keys %{ $proto->_plugins } unless @_; + my $plugin = shift; + return 1 if exists $proto->_plugins->{$plugin}; + return exists $proto->_plugins->{"Catalyst::Plugin::$plugin"}; + } + + sub _register_plugin { + my ( $proto, $plugin, $instant ) = @_; + my $class = ref $proto || $proto; + + unless (Class::Inspector->loaded($plugin)) { + require Class::Inspector->filename($plugin); } - { + $proto->_plugins->{$plugin} = 1; + unless ($instant) { no strict 'refs'; unshift @{"$class\::ISA"}, $plugin; } + return $class; + } + + sub setup_plugins { + my ( $class, $plugins ) = @_; + + $class->_plugins( {} ) unless $class->_plugins; + $plugins ||= []; + for my $plugin ( reverse @$plugins ) { + + unless ( $plugin =~ s/\A\+// ) { + $plugin = "Catalyst::Plugin::$plugin"; + } + + $class->_register_plugin($plugin); + } } } =head2 $c->stack -Returns the stack. +Returns an arrayref of the internal execution stack (actions that are +currently executing). =head2 $c->write( $data ) @@ -1953,9 +2240,9 @@ If you do not wish to use the proxy support at all, you may set: =head1 THREAD SAFETY -Catalyst has been tested under Apache 2's threading mpm_worker, mpm_winnt, -and the standalone forking HTTP server on Windows. We believe the Catalyst -core to be thread-safe. +Catalyst has been tested under Apache 2's threading C, +C, and the standalone forking HTTP server on Windows. We +believe the Catalyst core to be thread-safe. If you plan to operate in a threaded environment, remember that all other modules you are using must also be thread-safe. Some modules, most notably @@ -2018,6 +2305,8 @@ Autrijus Tang Brian Cassidy +Carl Franks + Christian Hansen Christopher Hicks