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.
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;
}
$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
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 )
sub setup_component {
my( $class, $component ) = @_;
+return sub {
unless ( $component->can( 'COMPONENT' ) ) {
return $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);
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
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)
($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>.
--- /dev/null
+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
$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');
}
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
};
$_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
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;
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 },
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}");
}
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;
is $res->content, 'name: john, age: 46';
}
+{
+ my $res = request '/error/not_found';
+ is $res->content, 'Not Found';
+}
+
done_testing;