X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FCatalyst.pm;h=83366f52edbe7e89e0f48aa8c36e708d0f51e7ca;hb=c49b3de44e34e737e058fc6837719dd9d437edbd;hp=6f1848a4db3502333a02f5e0e66faea5cf8f423e;hpb=85d9fce671016c9040775c8b4458cf9c72ec2208;p=catagits%2FCatalyst-Runtime.git
diff --git a/lib/Catalyst.pm b/lib/Catalyst.pm
index 6f1848a..83366f5 100644
--- a/lib/Catalyst.pm
+++ b/lib/Catalyst.pm
@@ -34,8 +34,6 @@ __PACKAGE__->mk_accessors(
qw/counter request response state action stack namespace stats/
);
-attributes->import( __PACKAGE__, \&namespace, 'lvalue' );
-
sub depth { scalar @{ shift->stack || [] }; }
# Laziness++
@@ -54,16 +52,18 @@ our $DETACH = "catalyst_detach\n";
__PACKAGE__->mk_classdata($_)
for qw/components arguments dispatcher engine log dispatcher_class
- engine_class context_class request_class response_class setup_finished/;
+ engine_class context_class request_class response_class stats_class
+ setup_finished/;
__PACKAGE__->dispatcher_class('Catalyst::Dispatcher');
__PACKAGE__->engine_class('Catalyst::Engine::CGI');
__PACKAGE__->request_class('Catalyst::Request');
__PACKAGE__->response_class('Catalyst::Response');
+__PACKAGE__->stats_class('Catalyst::Stats');
# Remember to update this in Catalyst::Runtime as well!
-our $VERSION = '5.7008';
+our $VERSION = '5.7014';
sub import {
my ( $class, @arguments ) = @_;
@@ -240,6 +240,17 @@ MYAPP_WEB_HOME. If both variables are set, the MYAPP_HOME one will be used.
Specifies log level.
+=head2 -Stats
+
+Enables statistics collection and reporting. You can also force this setting
+from the system environment with CATALYST_STATS or _STATS. The
+environment settings override the application, with _STATS having the
+highest priority.
+
+e.g.
+
+ use Catalyst qw/-Stats=1/
+
=head1 METHODS
=head2 INFORMATION ABOUT THE CURRENT REQUEST
@@ -403,87 +414,60 @@ sub clear_errors {
$c->error(0);
}
+# search components given a name and some prefixes
+sub _comp_search_prefixes {
+ my ( $c, $name, @prefixes ) = @_;
+ my $appclass = ref $c || $c;
+ my $filter = "^${appclass}::(" . join( '|', @prefixes ) . ')::';
-# search via regex
-sub _comp_search {
- my ( $c, @names ) = @_;
-
- foreach my $name (@names) {
- foreach my $component ( keys %{ $c->components } ) {
- return $c->components->{$component} if $component =~ /$name/i;
- }
- }
-
- return undef;
-}
+ # map the original component name to the sub part that we will search against
+ my %eligible = map { my $n = $_; $n =~ s{^$appclass\::[^:]+::}{}; $_ => $n; }
+ grep { /$filter/ } keys %{ $c->components };
-# try explicit component names
-sub _comp_explicit {
- my ( $c, @names ) = @_;
+ # undef for a name will return all
+ return keys %eligible if !defined $name;
- foreach my $try (@names) {
- return $c->components->{$try} if ( exists $c->components->{$try} );
- }
+ my $query = ref $name ? $name : qr/^$name$/i;
+ my @result = grep { $eligible{$_} =~ m{$query} } keys %eligible;
- return undef;
-}
+ return map { $c->components->{ $_ } } @result if @result;
-# 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 we were given a regexp to search against, we're done.
+ return if ref $name;
- my $appclass = ref $c || $c;
+ # regexp fallback
+ $query = qr/$name/i;
+ @result = grep { $eligible{ $_ } =~ m{$query} } keys %eligible;
- my @names = map { "${appclass}::${_}::${name}" } @prefixes;
+ # don't warn if we didn't find any results, it just might not exist
+ if( @result ) {
+ $c->log->warn( 'Relying on the regexp fallback behavior for component resolution' );
+ $c->log->warn( 'is unreliable and unsafe. You have been warned' );
+ }
- my $comp = $c->_comp_explicit(@names);
- return $comp if defined($comp);
- $comp = $c->_comp_search($name);
- return $comp;
+ return @result;
}
# Find possible names for a prefix
-
sub _comp_names {
my ( $c, @prefixes ) = @_;
-
my $appclass = ref $c || $c;
- my @pre = map { "${appclass}::${_}::" } @prefixes;
-
- my @names;
-
- COMPONENT: foreach my $comp ($c->component) {
- foreach my $p (@pre) {
- if ($comp =~ s/^$p//) {
- push(@names, $comp);
- next COMPONENT;
- }
- }
- }
+ my $filter = "^${appclass}::(" . join( '|', @prefixes ) . ')::';
+ my @names = map { s{$filter}{}; $_; } $c->_comp_search_prefixes( undef, @prefixes );
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 }
+
+ return $comp;
}
=head2 COMPONENT ACCESSORS
@@ -501,9 +485,13 @@ action.
sub controller {
my ( $c, $name, @args ) = @_;
- return $c->_filter_component( $c->_comp_prefixes( $name, qw/Controller C/ ),
- @args )
- if ($name);
+
+ if( $name ) {
+ my @result = $c->_comp_search_prefixes( $name, qw/Controller C/ );
+ return map { $c->_filter_component( $_, @args ) } @result if ref $name;
+ return $c->_filter_component( $result[ 0 ], @args );
+ }
+
return $c->component( $c->action->class );
}
@@ -525,9 +513,13 @@ If the name is omitted, it will look for
sub model {
my ( $c, $name, @args ) = @_;
- return $c->_filter_component( $c->_comp_prefixes( $name, qw/Model M/ ),
- @args )
- if $name;
+
+ if( $name ) {
+ my @result = $c->_comp_search_prefixes( $name, qw/Model M/ );
+ return map { $c->_filter_component( $_, @args ) } @result if ref $name;
+ return $c->_filter_component( $result[ 0 ], @args );
+ }
+
if (ref $c) {
return $c->stash->{current_model_instance}
if $c->stash->{current_model_instance};
@@ -536,19 +528,18 @@ sub model {
}
return $c->model( $c->config->{default_model} )
if $c->config->{default_model};
- return $c->_filter_component( $c->_comp_singular(qw/Model M/) );
-
-}
-
-=head2 $c->controllers
-Returns the available names which can be passed to $c->controller
+ my( $comp, $rest ) = $c->_comp_search_prefixes( undef, qw/Model M/);
-=cut
+ if( $rest ) {
+ $c->log->warn( 'Calling $c->model() will return a random model unless you specify one of:' );
+ $c->log->warn( '* $c->config->{default_model} # the name of the default model to use' );
+ $c->log->warn( '* $c->stash->{current_model} # the name of the model to use for this request' );
+ $c->log->warn( '* $c->stash->{current_model_instance} # the instance of the model to use for this request' );
+ $c->log->warn( 'NB: in version 5.80, the "random" behavior will not work at all.' );
+ }
-sub controllers {
- my ( $c ) = @_;
- return $c->_comp_names(qw/Controller C/);
+ return $c->_filter_component( $comp );
}
@@ -570,9 +561,13 @@ If the name is omitted, it will look for
sub view {
my ( $c, $name, @args ) = @_;
- return $c->_filter_component( $c->_comp_prefixes( $name, qw/View V/ ),
- @args )
- if $name;
+
+ if( $name ) {
+ my @result = $c->_comp_search_prefixes( $name, qw/View V/ );
+ return map { $c->_filter_component( $_, @args ) } @result if ref $name;
+ return $c->_filter_component( $result[ 0 ], @args );
+ }
+
if (ref $c) {
return $c->stash->{current_view_instance}
if $c->stash->{current_view_instance};
@@ -581,7 +576,29 @@ sub view {
}
return $c->view( $c->config->{default_view} )
if $c->config->{default_view};
- return $c->_filter_component( $c->_comp_singular(qw/View V/) );
+
+ my( $comp, $rest ) = $c->_comp_search_prefixes( undef, qw/View V/);
+
+ if( $rest ) {
+ $c->log->warn( 'Calling $c->view() will return a random view unless you specify one of:' );
+ $c->log->warn( '* $c->config->{default_view} # the name of the default view to use' );
+ $c->log->warn( '* $c->stash->{current_view} # the name of the view to use for this request' );
+ $c->log->warn( '* $c->stash->{current_view_instance} # the instance of the view to use for this request' );
+ $c->log->warn( 'NB: in version 5.80, the "random" behavior will not work at all.' );
+ }
+
+ return $c->_filter_component( $comp );
+}
+
+=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->models
@@ -619,32 +636,43 @@ should be used instead.
=cut
sub component {
- my $c = shift;
+ my ( $c, $name, @args ) = @_;
- if (@_) {
+ if( $name ) {
+ my $comps = $c->components;
- my $name = shift;
+ if( !ref $name ) {
+ # is it the exact name?
+ return $comps->{ $name } if exists $comps->{ $name };
- my $appclass = ref $c || $c;
+ # perhaps we just omitted "MyApp"?
+ my $composed = ( ref $c || $c ) . "::${name}";
+ return $comps->{ $composed } if exists $comps->{ $composed };
- my @names = (
- $name, "${appclass}::${name}",
- map { "${appclass}::${_}::${name}" }
- qw/Model M Controller C View V/
- );
+ # search all of the models, views and controllers
+ my( $comp ) = $c->_comp_search_prefixes( $name, qw/Model M Controller C View V/ );
+ return $c->_filter_component( $comp, @args ) if $comp;
+ }
+
+ # This is here so $c->comp( '::M::' ) works
+ my $query = ref $name ? $name : qr{$name}i;
- my $comp = $c->_comp_explicit(@names);
- return $c->_filter_component( $comp, @_ ) if defined($comp);
+ my @result = grep { m{$query} } keys %{ $c->components };
+ return @result if ref $name;
- $comp = $c->_comp_search($name);
- return $c->_filter_component( $comp, @_ ) if defined($comp);
+ if( $result[ 0 ] ) {
+ $c->log->warn( 'Relying on the regexp fallback behavior for component resolution' );
+ $c->log->warn( 'is unreliable and unsafe. You have been warned' );
+ return $result[ 0 ];
+ }
+
+ # I would expect to return an empty list here, but that breaks back-compat
}
+ # fallback
return sort keys %{ $c->components };
}
-
-
=head2 CLASS DATA AND HELPER CLASSES
=head2 $c->config
@@ -813,6 +841,7 @@ sub setup {
$class->setup_plugins( delete $flags->{plugins} );
$class->setup_dispatcher( delete $flags->{dispatcher} );
$class->setup_engine( delete $flags->{engine} );
+ $class->setup_stats( delete $flags->{stats} );
for my $flag ( sort keys %{$flags} ) {
@@ -906,6 +935,9 @@ 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.
+Note that uri_for is destructive to the passed hashref. Subsequent calls
+with the same hashref may have unintended results.
+
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
@@ -957,14 +989,14 @@ sub uri_for {
if (my @keys = keys %$params) {
# somewhat lifted from URI::_query's query_form
$query = '?'.join('&', map {
+ my $val = $params->{$_};
s/([;\/?:@&=+,\$\[\]%])/$URI::Escape::escapes{$1}/go;
s/ /+/g;
my $key = $_;
- my $val = $params->{$_};
$val = '' unless defined $val;
(map {
$_ = "$_";
- utf8::encode( $_ );
+ utf8::encode( $_ ) if utf8::is_utf8($_);
# using the URI::Escape pattern here so utf8 chars survive
s/([^A-Za-z0-9\-_.!~*'() ])/$URI::Escape::escapes{$1}/go;
s/ /+/g;
@@ -1079,7 +1111,7 @@ 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 want to start with a tutorial.
+ you might 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.
@@ -1198,13 +1230,13 @@ sub execute {
return $c->state;
}
- my $stats_info = $c->_stats_start_execute( $code ) if $c->debug;
+ my $stats_info = $c->_stats_start_execute( $code ) if $c->use_stats;
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;
+ $c->_stats_finish_execute( $stats_info ) if $c->use_stats and $stats_info;
my $last = pop( @{ $c->stack } );
@@ -1252,51 +1284,43 @@ sub _stats_start_execute {
}
}
- my $node = Tree::Simple->new(
- {
- action => $action,
- elapsed => undef, # to be filled in later
- comment => "",
- }
- );
- $node->setUID( "$code" . $c->counter->{"$code"} );
+ my $uid = "$code" . $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);
- }
+ $c->stats->profile(
+ begin => $action,
+ parent => "$parent" . $c->counter->{"$parent"},
+ uid => $uid,
+ );
}
else {
# forward with no caller may come from a plugin
- $c->stats->addChild($node);
+ $c->stats->profile(
+ begin => $action,
+ uid => $uid,
+ );
}
}
else {
-
+
# root-level call
- $c->stats->addChild($node);
+ $c->stats->profile(
+ begin => $action,
+ uid => $uid,
+ );
}
+ return $action;
- 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 );
+ $c->stats->profile( end => $info );
}
=head2 $c->_localize_fields( sub { }, \%keys );
@@ -1352,22 +1376,11 @@ sub finalize {
$c->finalize_body;
}
- if ($c->debug) {
- my $elapsed = sprintf '%f', tv_interval($c->stats->getNodeValue);
+ if ($c->use_stats) {
+ my $elapsed = sprintf '%f', $c->stats->elapsed;
my $av = $elapsed == 0 ? '??' : sprintf '%.3f', 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->log->info(
- "Request took ${elapsed}s ($av/s)\n" . $t->draw . "\n" );
+ "Request took ${elapsed}s ($av/s)\n" . $c->stats->report . "\n" );
}
return $c->response->status;
@@ -1437,6 +1450,7 @@ sub finalize_headers {
}
}
else {
+ # everything should be bytes at this point, but just in case
$c->response->content_length( bytes::length( $c->response->body ) );
}
}
@@ -1567,8 +1581,8 @@ sub prepare {
}
);
+ $c->stats($class->stats_class->new)->enable($c->use_stats);
if ( $c->debug ) {
- $c->stats(Tree::Simple->new([gettimeofday]));
$c->res->headers->header( 'X-Catalyst' => $Catalyst::VERSION );
}
@@ -1590,12 +1604,19 @@ sub prepare {
$c->prepare_cookies;
$c->prepare_path;
- # On-demand parsing
- $c->prepare_body unless $c->config->{parse_on_demand};
+ # Prepare the body for reading, either by prepare_body
+ # or the user, if they are using $c->read
+ $c->prepare_read;
+
+ # Parse the body unless the user wants it on-demand
+ unless ( $c->config->{parse_on_demand} ) {
+ $c->prepare_body;
+ }
}
my $method = $c->req->method || '';
- my $path = $c->req->path || '/';
+ my $path = $c->req->path;
+ $path = '/' unless length $path;
my $address = $c->req->address || '';
$c->log->debug(qq/"$method" request for "$path" from "$address"/)
@@ -1806,6 +1827,10 @@ C<$maxlength> defaults to the size of the request if not specified.
You have to set C<< MyApp->config->{parse_on_demand} >> to use this
directly.
+Warning: If you use read(), Catalyst will not process the body,
+so you will not be able to access POST parameters or file uploads via
+$c->request. You must handle all body parsing yourself.
+
=cut
sub read { my $c = shift; return $c->engine->read( $c, @_ ) }
@@ -1862,6 +1887,11 @@ sub setup_components {
my %comps = map { $_ => 1 } @comps;
for my $component ( @comps ) {
+
+ # We pass ignore_loaded here so that overlay files for (e.g.)
+ # Model::DBI::Schema sub-classes are loaded - if it's in @comps
+ # we know M::P::O found a file on disk so this is safe
+
Catalyst::Utils::ensure_class_loaded( $component, { ignore_loaded => 1 } );
my $module = $class->setup_component( $component );
@@ -2110,6 +2140,26 @@ Sets up plugins.
=cut
+=head2 $c->setup_stats
+
+Sets up timing statistics class.
+
+=cut
+
+sub setup_stats {
+ my ( $class, $stats ) = @_;
+
+ Catalyst::Utils::ensure_class_loaded($class->stats_class);
+
+ my $env = Catalyst::Utils::env_value( $class, 'STATS' );
+ if ( defined($env) ? $env : ($stats || $class->debug ) ) {
+ no strict 'refs';
+ *{"$class\::use_stats"} = sub { 1 };
+ $class->log->debug('Statistics enabled');
+ }
+}
+
+
=head2 $c->registered_plugins
Returns a sorted list of the plugins which have either been stated in the
@@ -2139,7 +2189,10 @@ the plugin name does not begin with C.
my ( $proto, $plugin, $instant ) = @_;
my $class = ref $proto || $proto;
- Catalyst::Utils::ensure_class_loaded( $plugin, { ignore_loaded => 1 } );
+ # no ignore_loaded here, the plugin may already have been
+ # defined in memory and we don't want to error on "no file" if so
+
+ Catalyst::Utils::ensure_class_loaded( $plugin );
$proto->_plugins->{$plugin} = 1;
unless ($instant) {
@@ -2170,6 +2223,24 @@ the plugin name does not begin with C.
Returns an arrayref of the internal execution stack (actions that are
currently executing).
+=head2 $c->stats_class
+
+Returns or sets the stats (timing statistics) class.
+
+=head2 $c->use_stats
+
+Returns 1 when stats collection is enabled. Stats collection is enabled
+when the -Stats options is set, debug is on or when the _STATS
+environment variable is set.
+
+Note that this is a static method, not an accessor and should be overloaded
+by declaring "sub use_stats { 1 }" in your MyApp.pm, not by calling $c->use_stats(1).
+
+=cut
+
+sub use_stats { 0 }
+
+
=head2 $c->write( $data )
Writes $data to the output stream. When using this method directly, you
@@ -2217,8 +2288,8 @@ This causes C to map to C.
=head1 ON-DEMAND PARSER
The request body is usually parsed at the beginning of a request,
-but if you want to handle input yourself or speed things up a bit,
-you can enable on-demand parsing with a config parameter.
+but if you want to handle input yourself, you can enable on-demand
+parsing with a config parameter.
MyApp->config->{parse_on_demand} = 1;
@@ -2363,6 +2434,8 @@ Sam Vilain
Sascha Kiefer
+Sebastian Willert
+
Tatsuhiko Miyagawa
Ulf Edvinsson