Updated helper for Root class
[catagits/Catalyst-Runtime.git] / lib / Catalyst.pm
index 7310d71..981b20f 100644 (file)
@@ -1,7 +1,7 @@
 package Catalyst;
 
 use strict;
-use base 'Catalyst::Base';
+use base 'Catalyst::Component';
 use bytes;
 use UNIVERSAL::require;
 use Catalyst::Exception;
@@ -10,6 +10,7 @@ use Catalyst::Request;
 use Catalyst::Request::Upload;
 use Catalyst::Response;
 use Catalyst::Utils;
+use Catalyst::Controller;
 use File::stat;
 use NEXT;
 use Text::SimpleTable;
@@ -17,6 +18,8 @@ use Path::Class;
 use Time::HiRes qw/gettimeofday tv_interval/;
 use URI;
 use Scalar::Util qw/weaken/;
+use Tree::Simple qw/use_weak_refs/;
+use Tree::Simple::Visitor::FindByUID;
 use attributes;
 
 __PACKAGE__->mk_accessors(
@@ -44,18 +47,18 @@ our $DETACH    = "catalyst_detach\n";
 require Module::Pluggable::Fast;
 
 # Helper script generation
-our $CATALYST_SCRIPT_GEN = 25;
+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/;
+  engine_class context_class request_class response_class setup_finished/;
 
 __PACKAGE__->dispatcher_class('Catalyst::Dispatcher');
 __PACKAGE__->engine_class('Catalyst::Engine::CGI');
 __PACKAGE__->request_class('Catalyst::Request');
 __PACKAGE__->response_class('Catalyst::Response');
 
-our $VERSION = '5.62';
+our $VERSION = '5.66';
 
 sub import {
     my ( $class, @arguments ) = @_;
@@ -68,7 +71,7 @@ sub import {
 
     unless ( $caller->isa('Catalyst') ) {
         no strict 'refs';
-        push @{"$caller\::ISA"}, $class;
+        push @{"$caller\::ISA"}, $class, 'Catalyst::Controller';
     }
 
     $caller->arguments( [@arguments] );
@@ -175,6 +178,14 @@ C<My::Module>.
 
     use Catalyst qw/My::Module/;
 
+If your plugin starts with a name other than C<Catalyst::Plugin::>, 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:
 
@@ -242,7 +253,10 @@ in an arrayref. The action will receive the arguments in C<@_> and
 C<$c-E<gt>req-E<gt>args>. Upon returning from the function,
 C<$c-E<gt>req-E<gt>args> will be restored to the previous values.
 
-    $c->forward('/foo');
+Any data C<return>ed 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('MyApp::View::TT');
@@ -364,13 +378,23 @@ sub component {
 
             if ( exists $c->components->{$try} ) {
 
-                return $c->components->{$try};
+                my $comp = $c->components->{$try};
+                if ( eval { $comp->can('ACCEPT_CONTEXT'); } ) {
+                    return $comp->ACCEPT_CONTEXT($c);
+                }
+                else { return $comp }
             }
         }
 
         foreach my $component ( keys %{ $c->components } ) {
-
-            return $c->components->{$component} if $component =~ /$name/i;
+            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 }
+            }
         }
 
     }
@@ -429,7 +453,24 @@ sub view {
 
 Returns or takes a hashref containing the application's configuration.
 
-    __PACKAGE__->config({ db => 'dsn:SQLite:foo.db' });
+    __PACKAGE__->config( { db => 'dsn:SQLite:foo.db' } );
+
+You can also use a L<YAML> config file like myapp.yml in your
+applications home directory.
+
+    ---
+    db: dsn:SQLite:foo.db
+
+=cut
+
+sub config {
+    my $c = shift;
+
+    $c->log->warn("Setting config after setup has been run is not a good idea.")
+      if ( @_ and $c->setup_finished );
+
+    $c->NEXT::config(@_);
+}
 
 =head2 $c->debug
 
@@ -451,10 +492,16 @@ L<Catalyst::Engine>.
 
 =head2 $c->log
 
-Returns the logging object instance. Unless it is already set, Catalyst
-sets this up with a L<Catalyst::Log> object. To use your own log class:
+Returns the logging object instance. Unless it is already set, Catalyst sets
+this up with a L<Catalyst::Log> 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( MyLogger->new );
     $c->log->info( 'Now logging with my own logger!' );
 
 Your log class should implement the methods described in the
@@ -495,12 +542,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);
@@ -559,11 +601,12 @@ sub setup {
         }
     }
 
+    $class->setup_home( delete $flags->{home} );
+
     $class->setup_log( delete $flags->{log} );
     $class->setup_plugins( delete $flags->{plugins} );
     $class->setup_dispatcher( delete $flags->{dispatcher} );
     $class->setup_engine( delete $flags->{engine} );
-    $class->setup_home( delete $flags->{home} );
 
     for my $flag ( sort keys %{$flags} ) {
 
@@ -592,9 +635,9 @@ EOF
 
         {
             no strict 'refs';
-            @plugins = 
-                map  { $_ . ' ' . ( $_->VERSION || '' ) }
-                grep { /^Catalyst::Plugin/ } @{"$class\::ISA"};
+            @plugins =
+              map { $_ . ' ' . ( $_->VERSION || '' ) }
+              grep { /^Catalyst::Plugin/ } @{"$class\::ISA"};
         }
 
         if (@plugins) {
@@ -649,6 +692,8 @@ EOF
         $class->log->info("$name powered by Catalyst $Catalyst::VERSION");
     }
     $class->log->_flush() if $class->log->can('_flush');
+
+    $class->setup_finished(1);
 }
 
 =head2 $c->uri_for( $path, [ @args ] )
@@ -656,7 +701,9 @@ EOF
 Merges path with C<$c-E<gt>request-E<gt>base> for absolute uri's and
 with C<$c-E<gt>namespace> for relative uri's, then returns a
 normalized L<URI> object. If any args are passed, they are added at the
-end of the path.
+end of the path.  If the last argument to uri_for is a hash reference,
+it is assumed to contain GET parameter key/value pairs, which will be
+appended to the URI in standard fashion.
 
 =cut
 
@@ -675,12 +722,16 @@ sub uri_for {
     $namespace = '' if $path =~ /^\//;
     $path =~ s/^\///;
 
+    my $params =
+      ( scalar @args && ref $args[$#args] eq 'HASH' ? pop @args : {} );
+
     # join args with '/', or a blank string
     my $args = ( scalar @args ? '/' . join( '/', @args ) : '' );
     $args =~ s/^\/// unless $path;
     my $res =
       URI->new_abs( URI->new_abs( "$path$args", "$basepath$namespace" ), $base )
       ->canonical;
+    $res->query_form(%$params);
     $res;
 }
 
@@ -784,23 +835,24 @@ sub welcome_message {
                  <p>Welcome to the wonderful world of Catalyst.
                     This <a href="http://en.wikipedia.org/wiki/MVC">MVC</a>
                     framework will make web development something you had
-                    never expected it to be: Fun, rewarding and quick.</p>
+                    never expected it to be: Fun, rewarding, and quick.</p>
                  <h2>What to do now?</h2>
                  <p>That really depends  on what <b>you</b> want to do.
                     We do, however, provide you with a few starting points.</p>
                  <p>If you want to jump right into web development with Catalyst
                     you might want to check out the documentation.</p>
                  <pre><code>perldoc <a href="http://cpansearch.perl.org/dist/Catalyst/lib/Catalyst/Manual/Intro.pod">Catalyst::Manual::Intro</a>
+perldoc <a href="http://cpansearch.perl.org/dist/Catalyst/lib/Catalyst/Manual/Tutorial.pod">Catalyst::Manual::Tutorial</a></code>
 perldoc <a href="http://cpansearch.perl.org/dist/Catalyst/lib/Catalyst/Manual.pod">Catalyst::Manual</a></code></pre>
                  <h2>What to do next?</h2>
                  <p>Next it's time to write an actual application. Use the
                     helper scripts to generate <a href="http://cpansearch.perl.org/search?query=Catalyst%3A%3AController%3A%3A&amp;mode=all">controllers</a>,
-                    <a href="http://cpansearch.perl.org/search?query=Catalyst%3A%3AModel%3A%3A&amp;mode=all">models</a> and
-                    <a href="http://cpansearch.perl.org/search?query=Catalyst%3A%3AView%3A%3A&amp;mode=all">views</a>,
+                    <a href="http://cpansearch.perl.org/search?query=Catalyst%3A%3AModel%3A%3A&amp;mode=all">models</a>, and
+                    <a href="http://cpansearch.perl.org/search?query=Catalyst%3A%3AView%3A%3A&amp;mode=all">views</a>;
                     they can save you a lot of work.</p>
                     <pre><code>script/${prefix}_create.pl -help</code></pre>
                     <p>Also, be sure to check out the vast and growing
-                    collection of <a href="http://cpansearch.perl.org/search?query=Catalyst%3A%3APlugin%3A%3A&amp;mode=all">plugins for Catalyst on CPAN</a>,
+                    collection of <a href="http://cpansearch.perl.org/search?query=Catalyst%3A%3APlugin%3A%3A&amp;mode=all">plugins for Catalyst on CPAN</a>;
                     you are likely to find what you need there.
                     </p>
 
@@ -887,17 +939,11 @@ via $c->error.
 
 sub execute {
     my ( $c, $class, $code ) = @_;
-    $class = $c->components->{$class} || $class;
+    $class = $c->component($class) || $class;
     $c->state(0);
 
-    my $callsub =
-        ( caller(0) )[0]->isa('Catalyst::Action')
-      ? ( caller(2) )[3]
-      : ( caller(1) )[3];
-
-    my $action = '';
     if ( $c->debug ) {
-        $action = "$code";
+        my $action = "$code";
         $action = "/$action" unless $action =~ /\-\>/;
         $c->counter->{"$code"}++;
 
@@ -909,8 +955,61 @@ sub execute {
             return $c->state;
         }
 
-        $action = "-> $action" if $callsub =~ /forward$/;
+        # 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;
+            }
+        }
+
+        my $node = Tree::Simple->new(
+            {
+                action  => $action,
+                elapsed => undef,     # to be filled in later
+            }
+        );
+        $node->setUID( "$code" . $c->counter->{"$code"} );
+
+        unless ( ( $code->name =~ /^_.*/ )
+            && ( !$c->config->{show_internal_actions} ) )
+        {
+
+            # 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 {
+
+                    # forward with no caller may come from a plugin
+                    $c->{stats}->addChild($node);
+                }
+            }
+            else {
+
+                # root-level call
+                $c->{stats}->addChild($node);
+            }
+        }
     }
+
     push( @{ $c->stack }, $code );
     my $elapsed = 0;
     my $start   = 0;
@@ -922,14 +1021,28 @@ sub execute {
         unless ( ( $code->name =~ /^_.*/ )
             && ( !$c->config->{show_internal_actions} ) )
         {
-            push @{ $c->{stats} }, [ $action, sprintf( '%fs', $elapsed ) ];
+
+            # FindByUID uses an internal die, so we save the existing error
+            my $error = $@;
+
+            # locate the node in the tree and update the elapsed time
+            my $visitor = Tree::Simple::Visitor::FindByUID->new;
+            $visitor->searchForUID( "$code" . $c->counter->{"$code"} );
+            $c->{stats}->accept($visitor);
+            if ( my $result = $visitor->getResult ) {
+                my $value = $result->getNodeValue;
+                $value->{elapsed} = sprintf( '%fs', $elapsed );
+                $result->setNodeValue($value);
+            }
+
+            # restore error
+            $@ = $error || undef;
         }
     }
     my $last = ${ $c->stack }[-1];
     pop( @{ $c->stack } );
 
     if ( my $error = $@ ) {
-
         if ( $error eq $DETACH ) { die $DETACH if $c->depth > 1 }
         else {
             unless ( ref $error ) {
@@ -1099,11 +1212,11 @@ sub handle_request {
     # Always expect worst case!
     my $status = -1;
     eval {
-        my @stats = ();
+        my $stats = ( $class->debug ) ? Tree::Simple->new: q{};
 
         my $handler = sub {
             my $c = $class->prepare(@arguments);
-            $c->{stats} = \@stats;
+            $c->{stats} = $stats;
             $c->dispatch;
             return $c->finalize;
         };
@@ -1117,7 +1230,15 @@ sub handle_request {
               ( $elapsed == 0 ? '??' : ( 1 / $elapsed ) );
             my $t = Text::SimpleTable->new( [ 64, 'Action' ], [ 9, 'Time' ] );
 
-            for my $stat (@stats) { $t->row( $stat->[0], $stat->[1] ) }
+            $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 );
         }
@@ -1451,7 +1572,7 @@ sub setup_components {
     my $callback = sub {
         my ( $component, $context ) = @_;
 
-        unless ( $component->isa('Catalyst::Component') ) {
+        unless ( $component->can('COMPONENT') ) {
             return $component;
         }
 
@@ -1460,7 +1581,7 @@ sub setup_components {
 
         my $instance;
 
-        eval { $instance = $component->new( $context, $config ); };
+        eval { $instance = $component->COMPONENT( $context, $config ); };
 
         if ( my $error = $@ ) {
 
@@ -1471,7 +1592,7 @@ sub setup_components {
         }
 
         Catalyst::Exception->throw( message =>
-qq/Couldn't instantiate component "$component", "new() didn't return a object"/
+qq/Couldn't instantiate component "$component", "COMPONENT() didn't return a object"/
           )
           unless ref $instance;
         return $instance;
@@ -1576,19 +1697,19 @@ sub setup_engine {
         if ( $software eq 'mod_perl' ) {
 
             if ( !$engine ) {
-                
+
                 if ( $version >= 1.99922 ) {
                     $engine = 'Catalyst::Engine::Apache2::MP20';
                 }
-    
+
                 elsif ( $version >= 1.9901 ) {
                     $engine = 'Catalyst::Engine::Apache2::MP19';
                 }
-    
+
                 elsif ( $version >= 1.24 ) {
                     $engine = 'Catalyst::Engine::Apache::MP13';
                 }
-    
+
                 else {
                     Catalyst::Exception->throw( message =>
                           qq/Unsupported mod_perl version: $ENV{MOD_PERL}/ );
@@ -1726,25 +1847,64 @@ Sets up plugins.
 
 =cut
 
-sub setup_plugins {
-    my ( $class, $plugins ) = @_;
+=head2 $c->registered_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); >>.
+
+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<Catalyst::Plugin::>.
+
+ if ($c->registered_plugins('Some::Plugin')) {
+     ...
+ }
+
+=cut
+
+{
+    my %PLUGINS;
 
-    $plugins ||= [];
-    for my $plugin ( reverse @$plugins ) {
+    sub registered_plugins {
+        my $proto = shift;
+        return sort keys %PLUGINS unless @_;
+        my $plugin = shift;
+        return 1 if exists $PLUGINS{$plugin};
+        return exists $PLUGINS{"Catalyst::Plugin::$plugin"};
+    }
 
-        $plugin = "Catalyst::Plugin::$plugin";
+    sub _register_plugin {
+        my ( $proto, $plugin, $instant ) = @_;
+        my $class = ref $proto || $proto;
 
         $plugin->require;
 
-        if ($@) {
+        if ( my $error = $@ ) {
+            my $type = $instant ? "instant " : '';
             Catalyst::Exception->throw(
-                message => qq/Couldn't load plugin "$plugin", "$@"/ );
+                message => qq/Couldn't load ${type}plugin "$plugin", $error/ );
         }
 
-        {
+        $PLUGINS{$plugin} = 1;
+        unless ($instant) {
             no strict 'refs';
             unshift @{"$class\::ISA"}, $plugin;
         }
+        return $class;
+    }
+
+    sub setup_plugins {
+        my ( $class, $plugins ) = @_;
+
+        $plugins ||= [];
+        for my $plugin ( reverse @$plugins ) {
+
+            unless ( $plugin =~ s/\A\+// ) {
+                $plugin = "Catalyst::Plugin::$plugin";
+            }
+
+            $class->_register_plugin($plugin);
+        }
     }
 }