NAME
Catalyst::Plugin::InjectionHelpers - Enhance Catalyst Component Injection
SYNOPSIS
Use the plugin in your application class:
package MyApp;
use Catalyst 'InjectionHelpers';
MyApp->config(
'Model::SingletonA' => {
-inject => {
from_class=>'MyApp::Singleton',
adaptor=>'Application',
roles=>['MyApp::Role::Foo'],
method=>'new',
},
aaa => 100,
},
'Model::SingletonB' => {
-inject => {
from_class=>'MyApp::Singleton',
adaptor=>'Application',
method=>sub {
my ($adaptor_instance, $from_class, $app, %args) = @_;
return $from_class->new(aaa=>$args{arg});
},
arg => 300,
},
);
MyApp->setup;
Alternatively you can use the 'inject_components' class method:
package MyApp;
use Catalyst 'InjectionHelpers';
MyApp->inject_components(
'Model::SingletonA' => {
from_class=>'MyApp::Singleton',
adaptor=>'Application',
roles=>['MyApp::Role::Foo'],
method=>'new',
},
'Model::SingletonB' => {
from_class=>'MyApp::Singleton',
adaptor=>'Application',
method=>sub {
my ($adaptor_instance, $from_class, $app, %args) = @_;
return $class->new(aaa=>$args{arg});
},
},
);
MyApp->config(
'Model::SingletonA' => { aaa=>100 },
'Model::SingletonB' => { arg=>300 },
);
MyApp->setup;
The first method is a better choice if you need to alter how your injections work based on configuration that is controlled per environment.
DESCRIPTION
NOTE Starting with VERSION
0.012 there is a breaking change in the number of arguments that the method
and from_code
callbacks get. If you need to keep backwards compatibility you should set the version flag to 1:
MyApp->config(
'Plugin::InjectionHelpers' => { version => 1 },
## Additional configuration as needed
);
This plugin enhances the build in component injection features of Catalyst (since v5.90090) to make it easy to bring non Catalyst::Component classes into your application. You may consider using this for what you often used Catalyst::Model::Adaptor in the past for (although there is no reason to stop using that if you are doing so, its not a 'broken' approach, but for the very simple cases this might suffice and allow you to reduce the number of nearly empty 'boilerplate' classes in your application.)
You should be familiar with how component injection works in newer versions of Catalyst (v5.90090+).
It also experimentally supports a mechanism for dependency injection (that is the ability to set other componements as initialization arguments, similar to how you might see this work with inversion of control frameworks such as Bread::Board.) Author has no plan to move this past experimental status; he is merely publishing code that he's used on jobs where the code worked for the exact cases he was using it for the purposes of easing long term maintainance on those projects. If you like this feature and would like to see it stablized it will be on you to help the author validate it; its not impossible more changes and pontentially breaking changes will be needed to make that happen, and its also not impossible that changes to core Catalyst would be needed as well. Reports from users in the wild greatly appreciated.
USAGE
MyApp->config(
$model_name => +{
-inject => +{ %injection_args },
\%configuration_args;
or
MyApp->inject_components($model_name => \%injection_args);
MyApp->config($model_name => \%configuration_args);
Where $model_name
is the name of the component as it is in your Catalyst application (ie 'Model::User', 'View::HTML', 'Controller::Static') and %injection_args
are key /values as described below:
from_class
This is the full namespace of the class you are adapting to use as a Catalyst component. Example 'MyApp::Class'.
from_code
This is a codereference that generates your component instance. Used when you don't have a class you wish to adapt (handy for prototyping or small components).
MyApp->inject_components(
'Model::Foo' => {
from_code => sub {
my ($app_ctx, %args) = @_;
return $XX;
},
adaptor => 'Factory',
},
);
$app_ctx
is either the application class or Catalyst context, depending on the scope of your component.
If you use this you should not define the 'method' key or the 'roles' key (below).
roles
A list of Moose::Roless that will be composed into the 'from_class' prior to creating an instance of that class. Useful if you apply roles for debugging or testing in certain environments.
method
Either a string or a coderef. If left empty this defaults to 'new'.
The name of the method used to create the adapted class instance. Generally this is 'new'. If you have complex instantiation requirements you may instead use a coderef. If so, your coderef will receive three arguments. The first is the name of the from_class. The second is either the application or context, depending on the type adaptor. The third is a hash of arguments which merges the global configuration for the named component along with any arguments passed in the request for the component (this only makes sense for non application scoped models, btw).
Example:
MyApp->inject_components(
'Model::Foo' => {
from_class => 'Foo',
method => sub {
my ($from_class, $app_or_ctx, %args) = @_;
},
adaptor => 'Factory',
},
);
Argument details:
- $from_class
-
The name of the class you set in the 'from_class' parameter.
- $app_or_ctx
-
Either your application class or a reference to the current context, depending on how the adaptore is scoped (PerRequest and Factory get $ctx).
- %args
-
A Hash of the configuration parameters from your application configuration. If the adaptor is context/request scoped, also combines any arguments included in the call for the component. for example:
package MyApp; use Catalyst; MyApp->inject_components( 'Model::Foo' => { from_class=>"Foo", adaptor=>'Factory' }); MyApp->config( 'Model::Foo' => { aaa => 111 } ) MyApp->setup;
If in an action you say:
my $model = $c->model('Foo', bbb=>222);
Then
%args
would be:(aaa=>111, bbb=>222);
NOTE Please keep in mind supplying arguments in the ->model call (or ->view for that matter) only makes sense for components that ACCEPT_CONTEXT (in this case are Factory, PerRequest or PerSession adaptor types).
transform_args
A coderef that you can use to transform configuration arguments into something more suitable for your class. For example, the configuration args is typically a hash, but your object class may require some positional arguments.
MyApp->inject_components(
'Model::Foo' => {
from_class = 'Foo',
transform_args => sub {
my (%args) = @_;
my $path = delete $args{path},
return ($path, %args);
},
},
);
Should return the args as they as used by the initialization method of the 'from_class'.
Use 'transform_args' when you just need to tweak how your object uses arguments and use 'from_code' or 'method' when you need more control on what kind of object is returned (in other words choose the smallest hammer for the job).
adaptor
The adaptor used to bring your 'from_class' into Catalyst. Out of the box there are three adaptors (described in detail below): Application, Factory and PerRequest. The default is Application. You may create your own adaptors; if you do so you should use the full namespace as the value (MyApp::Adaptors::MySpecialAdaptor).
ADAPTORS
Out of the box this plugin comes with the following three adaptors. All canonical adaptors are under the namespace 'Catalyst::Model::InjectionHelpers'.
Application
Model is application scoped. This means you get one instance shared for the entire lifecycle of the application.
Factory
Model is scoped to the request. Each call to $c->model($model_name) returns a new instance of the model. You may pass additional parameters in the model call, which are merged to the global parameters defined in configuration and used as part of the object initialization.
PerRequest
Model is scoped to the request. The first time in a request that you call for the model, a new model is created. After that, all calls to the model return the original instance, until the request is completed, after which the instance is destroyed when the request goes out of scope.
The first time you call this model you may pass additional parameters, which get merged with the global configuration and used to initialize the model.
PerSession.
Scoped to a session. Requires the Session plugin. See Catalyst::Model::InjectionHelpers::PerSession for more.
Creating your own adaptor
Your new adaptor should consume the role Catalyst::ModelRole::InjectionHelpers and provide a method ACCEPT_CONTEXT which must return the component you wish to inject. Please review the existing adaptors and that role for insights.
DEPENDENCY INJECTION
Often when you are setting configuration options for your components, you might desire to 'depend on' other existing components. This design pattern is called 'Inversion of Control', and you might be familiar with it from prior art on CPAN such as IOC, Bread::Board and Beam::Wire.
The IOC features that are exposed via this plugin are basic and marked experimental (please see preceding note). The are however presented to the Catalyst community with the hope of provoking thought and discussion (or at the very least put an end to the idea that this is something people actually care about).
To use this feature you simply tag configuration keys as 'dependent' using a hashref for the key value. For example, here we define an inline model that is a DBI $dbh
and a User model that depends on it:
MyApp->config(
'Model::DBH' => {
-inject => {
adaptor => 'Application',
from_code => sub {
my ($app, @args) = @_;
return DBI->connect(@args);
},
},
%DBI_Connection_Args,
},
'Model::User' => {
-inject => {
from_class => 'MyApp::User',
adaptor => 'Factory',
},
dbh => { -model => 'DBH' },
},
# Additional configuration as needed
);
Now in you code (say in a controller if you do:
my $user = $c->model('User');
We automatically resolve the value for dbh
to be $c->model('DBH') and supply it as an argument.
Currently we only support dependency substitutions on the first level of arguments.
All injection syntax takes the form of "$argument_key => { $type => $parameter }" where the following $types are supported
- -model => $model_name
- -view => $view_name
- -controller => $controller_name
-
Provide dependency in the form of $c->model($model_name) (or $c->view($view_name), $c->controller($controller_name)).
- -code => $subref
-
Custom dependency that resolves from a subref. Example:
MyApp->config( 'Model::User' => { current_time => { -code => sub { my $app_or_context = shift; return DateTime->now; }, }, }, # Rest of configuration );
Please keep in mind that you must return an object.
$app_or_context
will be either the application class or $c (context) depending on the type of model (if it accepts context or not). - -core => $target
-
This exposes some core objects such as $app, $c etc. Where $target is:
- $app
-
The name of the application class.
- $ctx
-
The result of
$c
. Please note its probably bad form to pass the entire context object as it leads to unnecessary tight coupling. - $req
-
The result of
$c->req
- $res
-
The result of
$c->res
- $log
-
The result of
$c->log
- $user
-
The result of
$c->user
(if it exists, you should either define it or use the Authentication plugin).
CONFIGURATION
This plugin defines the following possible configuration. As per Catalyst standards, these configuration keys fall under the 'Plugin::InjectionHelpers' namespace in the configuration hash.
adaptor_namespace
Default namespace to look for adaptors. Defaults to Catalyst::Model::InjectionHelpers
default_adaptor
The default adaptor to use, should you not set one. Defaults to 'Application'.
dispatchers
Allows you to add to the default dependency injection handers:
MyApp->config(
'Plugin::InjectionHelpers' => {
dispatchers => {
'-my' => sub {
my ($app_ctx, $what) = @_;
warn "asking for a -my $what";
return ....;
},
},
},
# Rest of configuration
);
version
Default is 2. Set to 1 if you are need compatibility version 0.011 or older style of arguments for 'method' and 'from_code'.
Catalyst::Plugin::ConfigLoader
When using this plugin with Catalyst::Plugin::ConfigLoader you should add it to the plugin list afterward, for example:
package MyApp;
use Catalyst 'ConfigLoader',
'InjectionHelpers';
Please keep in mind that due to the way Configloader merges the configuration files you might have to set some things to undef
in order to get the correct behavior. For example you might define a model by default using from_code:
package MyApp;
use Catalyst 'ConfigLoader',
'InjectionHelpers';
MyApp->config(
'Model::Foo' => {
-inject => {
from_code => sub {
my ($app, %args) = @_;
return bless +{ %args, app=>$app }, 'Dummy1';
},
},
bar => 'baz',
},
);
MyApp->setup;
But then in youe configuration file overlay, you want to specify a class. In that case you will need to undefine the default keys:
# File:myapp_local.pl
return +{
'Model::Foo' => {
-inject => {
from_class => 'MyApp::Dummy2',
from_code => undef, # Need to blow away the existing...
},
},
};
Its probably not ideal that the configuration overlay doesn't permit you to tag refs as 'replace' rather than 'merge' but this is not a problem with this plugin. If it bothers you that a configuration overlay would require to have understanding of how 'lower' configurations are setup you should be able to avoid it by using all the same keys.
PRIOR ART
You may wish to review other similar approach on CPAN:
AUTHOR
John Napiorkowski email:jjnapiork@cpan.org
SEE ALSO
Catalyst, Catalyst::Model::InjectionHelpers::Application, Catalyst::Model::InjectionHelpers::Factory, Catalyst::Model::InjectionHelpers::PerRequest Catalyst::ModelRole::InjectionHelpers
COPYRIGHT & LICENSE
Copyright 2016, John Napiorkowski email:jjnapiork@cpan.org
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.