example "delayed" component setup
John Napiorkowski [Thu, 16 Apr 2015 23:18:32 +0000 (18:18 -0500)]
Changes
lib/Catalyst.pm
lib/Catalyst/Component.pm
lib/Catalyst/Component/DelayedInstance.pm [new file with mode: 0644]
lib/Catalyst/Dispatcher.pm
lib/Catalyst/Upgrading.pod
lib/Catalyst/Utils.pm
t/configured_comps.t

diff --git a/Changes b/Changes
index 7da6712..8fc0fbb 100644 (file)
--- a/Changes
+++ b/Changes
     traits for these core Catalyst classes without needing to create subclasses. So
     in general any request or response trait on CPAN that used 'CatalystX::RoleApplicator'
     should now just work with this core feature.
+  - NEW FEATURE: Core concepts from 'CatalystX::ComponentsFromConfig'.  You can now
+    setup components directly from configuration.  This could save you some effort and
+    creating 'empty' base classes in your Model/View and Controller directories.  This
+    feature is currently limited in that you can only configure components that are
+    'true' Catalyst components (but you may use Catalyst::Model::Adaptor to proxy
+    stand alone classes...).
   - Only create a stats object if you are using stats.  This is a minor performance
     optimization, but there's a small chance it is a breaking change, so please
     report any stats related issues.
+  - Added a developer mode warning if you call a component with arguments that does not
+    expect arguments (for example calling $c->model('Foo', 1,2,3,4) where Myapp::Model::Foo
+    does not ACCEPT_CONTEXT.   Only components that ACCEPT_CONTEXT do anything with
+    passed arguments in $c->controller/view/model.
 
 5.90089_001 - 2015-03-26
   - New development branch synched with 5.90085.
index 27b421e..0e50180 100644 (file)
@@ -713,10 +713,16 @@ sub _comp_names {
 sub _filter_component {
     my ( $c, $comp, @args ) = @_;
 
+    if(ref $comp eq 'CODE') {
+      $comp = $comp->();
+    }
+
     if ( eval { $comp->can('ACCEPT_CONTEXT'); } ) {
         return $comp->ACCEPT_CONTEXT( $c, @args );
     }
 
+    $c->log->warn("You called component '${\$comp->catalyst_component_name}' with arguments [@args], but this component does not ACCEPT_CONTEXT, so args are ignored.") if scalar(@args) && $c->debug;
+
     return $comp;
 }
 
@@ -763,7 +769,8 @@ Gets a L<Catalyst::Model> instance by name.
 
     $c->model('Foo')->do_stuff;
 
-Any extra arguments are directly passed to ACCEPT_CONTEXT.
+Any extra arguments are directly passed to ACCEPT_CONTEXT, if the model
+defines ACCEPT_CONTEXT.  If it does not, the args are discarded.
 
 If the name is omitted, it will look for
  - a model object in $c->stash->{current_model_instance}, then
@@ -2832,23 +2839,40 @@ sub setup_components {
 
     for my $component (@comps) {
         my $instance = $class->components->{ $component } = $class->setup_component($component);
-        my @expanded_components = $instance->can('expand_modules')
-            ? $instance->expand_modules( $component, $config )
-            : $class->expand_component_module( $component, $config );
-        for my $component (@expanded_components) {
-            next if $comps{$component};
-            $class->components->{ $component } = $class->setup_component($component);
-        }
     }
 
-    # Inject a component or wrap a stand alone class in an adaptor
-    #my @configured_comps = grep { not($class->component($_)||'') }
-    # grep { /^(Model)::|(View)::|(Controller::)/ }
-    #   keys %{$class->config ||+{}};
+    # Inject a component or wrap a stand alone class in an adaptor. This makes a list
+    # of named components in the configuration that are not actually existing (not a
+    # real file).
+    my @configured_comps = grep { not($class->components->{$_}||'') }
+      grep { /^(Model)::|(View)::|(Controller::)/ }
+        keys %{$class->config ||+{}};
+
+    foreach my $configured_comp(@configured_comps) {
+      my $component_class = exists $class->config->{$configured_comp}->{from_component} ? 
+        delete $class->config->{$configured_comp}->{from_component} : '';
+
+      if($component_class) {
+        my @roles = @{ exists $class->config->{$configured_comp}->{roles} ?
+          delete $class->config->{$configured_comp}->{roles} : [] };
+
+        my %args = %{ exists $class->config->{$configured_comp}->{args} ? 
+          delete $class->config->{$configured_comp}->{args} : +{} };
+
+        $class->config->{$configured_comp} = \%args;
+        Catalyst::Utils::inject_component(
+          into => $class,
+          component => $component_class,
+          (scalar(@roles) ? (traits => \@roles) : ()),
+          as => $configured_comp);
+      }
+    }
+
+    # All components are registered, now we need to 'init' them.
+    foreach my $component_name (keys %{$class->components||{}}) {
+      $class->components->{$component_name} = $class->components->{$component_name}->();
+    }
 
-    #foreach my $configured_comp(@configured_comps) {
-      #warn $configured_comp;
-      #}
 }
 
 =head2 $c->locate_components( $setup_component_config )
@@ -2899,6 +2923,7 @@ sub expand_component_module {
 sub setup_component {
     my( $class, $component ) = @_;
 
+return sub {
     unless ( $component->can( 'COMPONENT' ) ) {
         return $component;
     }
@@ -2910,14 +2935,15 @@ sub setup_component {
     # for the debug screen, as $component is already the key name.
     local $config->{catalyst_component_name} = $component;
 
-    my $instance = eval { $component->COMPONENT( $class, $config ); };
-
-    if ( my $error = $@ ) {
-        chomp $error;
-        Catalyst::Exception->throw(
-            message => qq/Couldn't instantiate component "$component", "$error"/
-        );
-    }
+    my $instance = eval {
+      $component->COMPONENT( $class, $config );
+    } || do {
+      my $error = $@;
+      chomp $error;
+      Catalyst::Exception->throw(
+        message => qq/Couldn't instantiate component "$component", "$error"/
+      );
+    };
 
     unless (blessed $instance) {
         my $metaclass = Moose::Util::find_meta($component);
@@ -2929,7 +2955,18 @@ sub setup_component {
             qq/Couldn't instantiate component "$component", COMPONENT() method (from $component_method_from) didn't return an object-like value (value was $value)./
         );
     }
-    return $instance;
+
+my @expanded_components = $instance->can('expand_modules')
+  ? $instance->expand_modules( $component, $config )
+  : $class->expand_component_module( $component, $config );
+for my $component (@expanded_components) {
+  next if $class->components->{ $component };
+  $class->components->{ $component } = $class->setup_component($component);
+}
+
+    return $instance; 
+}
+
 }
 
 =head2 $c->setup_dispatcher
index 13c9323..0952b76 100644 (file)
@@ -202,6 +202,19 @@ something like this:
       return $class->new($app, $args);
   }
 
+B<NOTE:> Generally when L<Catalyst> starts, it initializes all the components
+and passes the hashref present in any configutation information to the
+COMPONET method.  For example
+
+    MyApp->config(
+      'Model::Foo' => {
+        bar => 'baz',
+      });
+
+You would expect COMPONENT to be called like this ->COMPONENT( 'MyApp', +{ bar=>'baz'});
+
+This would happen ONCE during setup.
+
 =head2 $c->config
 
 =head2 $c->config($hashref)
@@ -251,6 +264,31 @@ would cause your MyApp::Model::Foo instance's ACCEPT_CONTEXT to be called with
 ($c, 'bar', 'baz')) and the return value of this method is returned to the
 calling code in the application rather than the component itself.
 
+B<NOTE:> All classes that are L<Catalyst::Component>s will have a COMPONENT
+method, but classes that are intended to be factories or generators will
+have ACCEPT_CONTEXT.  If you have initialization arguments (such as from
+configuration) that you wish to expose to the ACCEPT_CONTEXT you should
+proxy them in the factory instance.  For example:
+
+    MyApp::Model::FooFactory;
+
+    use Moose;
+    extends 'Catalyst::Model';
+
+    has type => (is=>'ro', required=>1);
+
+    sub ACCEPT_CONTEXT {
+      my ($self, $c, @args) = @_;
+      return bless { args=>\@args }, $self->type;
+    }
+
+    MyApp::Model::Foo->meta->make_immutable;
+    MyApp::Model::Foo->config( type => 'Type1' );
+
+And in a controller:
+
+    my $type = $c->model('FooFactory', 1,2,3,4): # $type->isa('Type1')
+
 =head1 SEE ALSO
 
 L<Catalyst>, L<Catalyst::Model>, L<Catalyst::View>, L<Catalyst::Controller>.
diff --git a/lib/Catalyst/Component/DelayedInstance.pm b/lib/Catalyst/Component/DelayedInstance.pm
new file mode 100644 (file)
index 0000000..06096e7
--- /dev/null
@@ -0,0 +1,94 @@
+package Catalyst::Component::DelayedInstance;
+
+use Moose::Role;
+
+around 'COMPONENT', sub {
+  my ($orig, $class, $app, $conf) = @_;
+  my $method = $class->can('build_delayed_instance') ?
+    'build_delayed_instance' : 'COMPONENT';
+
+  return bless sub { my $c = shift; $class->$method($app, $conf) }, $class;
+};
+
+our $SINGLE;
+
+sub ACCEPT_CONTEXT {
+  my ($self, $c, @args) = @_;
+  $c->log->warn("Component ${\$self->catalyst_component_name} cannot be called with arguments")
+    if $c->debug and scalar(@args) > 0;
+
+  return $SINGLE ||= $self->();
+}
+
+sub AUTOLOAD {
+  my ($self, @args) = @_;
+  my $method = our $AUTOLOAD;
+  $method =~ s/.*:://;
+
+  warn $method;
+  use Devel::Dwarn;
+  Dwarn \@args;
+  
+  return ($SINGLE ||= $self->())->$method(@args);
+}
+
+1;
+
+=head1 NAME
+
+Catalyst::Component::DelayedInstance - Moose Role for components which setup 
+
+=head1 SYNOPSIS
+
+    package MyApp::Model::Foo;
+
+    use Moose;
+    extends 'Catalyst::Model';
+    with 'Catalyst::Component::DelayedInstance';
+
+    sub build_per_application_instance {
+      my ($class, $app, $config) = @_;
+
+      $config->{bar} = $app->model("Baz");
+      return $class->new($config);
+    }    
+
+=head1 DESCRIPTION
+
+Sometimes you want an application scoped component that nevertheless needs other
+application components as part of its setup.  In the past this was not reliable
+since Application scoped components are setup in linear order.  You could not
+call $app->model in a COMPONENT method and expect 'Foo' to be there.  This role
+defers creating the application scoped instance until after your application is
+fully setup.  This means you can now assume your other application scoped components
+(components that do COMPONENT but not ACCEPT_CONTEXT) are available as dependencies.
+
+Please note this means that your instance is not created until the first time its
+called in a request.  As a result any errors with configuration will not show up
+until later in runtime.  So there is a larger burden on your testing to make sure
+your application startup and runtime is accurate.  Also note that even though your
+instance creation is deferred to request time, the request context is NOT given,
+but the application is (this means that you cannot depend on components that do
+ACCEPT_CONTEXT, since you don't have one...).
+
+=head1 ATTRIBUTES
+
+=head1 METHODS
+
+=head2 ACCEPT_CONTEXT
+
+=head2 AUTOLOAD
+
+=head1 SEE ALSO
+
+L<Catalyst::Component>,
+
+=head1 AUTHORS
+
+See L<Catalyst>.
+
+=head1 COPYRIGHT
+
+See L<Catalyst>.
+
+=cut
index 12040b2..4b9fa8e 100644 (file)
@@ -613,7 +613,8 @@ sub setup_actions {
       $self->_load_dispatch_types( @{ $self->preload_dispatch_types } );
     @{ $self->_registered_dispatch_types }{@classes} = (1) x @classes;
 
-    foreach my $comp ( values %{ $c->components } ) {
+    foreach my $comp_key ( keys %{ $c->components } ) {
+      my $comp = $c->component($comp_key);
         $comp->register_actions($c) if $comp->can('register_actions');
     }
 
index 75a74f7..6012f98 100644 (file)
@@ -49,6 +49,38 @@ and 'stats_class_traits', so you use like this (note this value is an ArrayRef)
 traits for Engine, since that class does a lot less nowadays, and dispatcher.  If you
 used those and can share a use case, we'd be likely to support them.
 
+Lastly, we have some of the feature from L<CatalystX::ComponentsFromConfig> in
+core.  This should mostly work the same way in core, except for now the
+core version does not create an automatic base wrapper class for your configured
+components (it requires these to be catalyst components and injects them directly.
+So if you make heavy use of custom base classes in L<CatalystX::ComponentsFromConfig>
+you might need a bit of work to use the core version (although there is no reason
+to stop using L<CatalystX::ComponentsFromConfig> since it should continue to work
+fine and we'd consider issues with it to be bugs).  Here's one way to map from
+L<CatalystX::ComponentsFromConfig> to core:
+
+In L<CatalystX::ComponentsFromConfig>:
+
+    MyApp->config(
+      'Model::MyClass' => {
+          class => 
+
+      });
+
+<Model::MyClass>
+ class My::Class
+ <args>
+  some param
+ </args>
+</Model::MyClass>
+
+Also we added a new develop console mode only warning when you call a component
+with arguments that don't expect or do anything meaningful with those args.  Its
+possible if you are logging debug mode in production (please don't...) this 
+could add verbosity to those logs if you also happen to be calling for components
+and passing pointless arguments.  We added this warning to help people not make this
+error and to better understand the component resolution flow.
+
 =head1 Upgrading to Catalyst 5.90085
 
 In this version of Catalyst we made a small change to Chained Dispatching so
index db89063..847a1c3 100644 (file)
@@ -579,9 +579,9 @@ sub inject_component {
     };
 
     $_setup_component->( $into, $component_package );
-    for my $inner_component_package ( Devel::InnerPackage::list_packages( $component_package ) ) {
-        $_setup_component->( $into, $inner_component_package );
-    }
+    #  for my $inner_component_package ( Devel::InnerPackage::list_packages( $component_package ) ) {
+    #       $_setup_component->( $into, $inner_component_package );
+    #   }
 }
 
 =head1 PSGI Helpers
index 84a5a8e..1cc0d1a 100644 (file)
@@ -4,6 +4,15 @@ use HTTP::Request::Common;
 use Test::More;
 
 {
+  package Local::Model::Foo;
+
+  use Moose;
+  extends 'Catalyst::Model';
+
+  has a => (is=>'ro', required=>1);
+
+  sub foo { shift->a . 'foo' }
+
   package Local::Controller::Errors;
 
   use Moose;
@@ -13,12 +22,22 @@ use Test::More;
 
   has ['a', 'b'] => (is=>'ro', required=>1);
 
-  sub not_found :Local { pop->res->from_psgi_response(404, [], ['Not Found']) }
+  sub not_found :Local { pop->res->from_psgi_response([404, [], ['Not Found']]) }
 
   package MyApp::Model::User;
   $INC{'MyApp/Model/User.pm'} = __FILE__;
 
-  use base 'Catalyst::Model';
+  use Moose;
+  extends 'Catalyst::Model';
+
+  has 'zoo' => (is=>'ro', required=>1, isa=>'Object');
+
+  around 'COMPONENT', sub {
+    my ($orig, $class, $app, $config) = @_;
+    $config->{zoo} = $app->model('Zoo');
+
+    return $class->$orig($app, $config);
+  };
 
   our %users = (
     1 => { name => 'john', age => 46 },
@@ -44,6 +63,9 @@ use Test::More;
   sub user :Local Args(1) {
     my ($self, $c, $int) = @_;
     my $user = $c->model("User")->find($int);
+
+     $c->model("User")->zoo->a;
+    
     $c->res->body("name: $user->{name}, age: $user->{age}");
   }
 
@@ -59,8 +81,18 @@ use Test::More;
 
   MyApp->config({
     'Controller::Err' => {
-      component => 'Local::Controller::Errors'
-    }
+      from_component => 'Local::Controller::Errors',
+      args => { a=> 100, b => 200, namespace =>'error' },
+    },
+    'Model::Zoo' => {
+      from_component => 'Local::Model::Foo',
+      args => {a=>2},
+    },
+    'Model::Foo' => {
+      from_component => 'Local::Model::Foo',
+      args => { a=> 100 },
+    },
+
   });
 
   MyApp->setup;
@@ -73,4 +105,9 @@ use Catalyst::Test 'MyApp';
   is $res->content, 'name: john, age: 46';
 }
 
+{
+  my $res = request '/error/not_found';
+  is $res->content, 'Not Found';
+}
+
 done_testing;