File Coverage

blib/lib/Catalyst/Plugin/InjectionHelpers.pm
Criterion Covered Total %
statement 15 15 100.0
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 20 20 100.0


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::InjectionHelpers;
2              
3 2     2   226712 use Moose::Role;
  2         5  
  2         15  
4 2     2   10169 use Catalyst::Utils;
  2         5  
  2         69  
5 2     2   902 use Catalyst::Model::InjectionHelpers::Application;
  2         688  
  2         85  
6 2     2   1105 use Catalyst::Model::InjectionHelpers::Factory;
  2         666  
  2         73  
7 2     2   974 use Catalyst::Model::InjectionHelpers::PerRequest;
  2         664  
  2         2780  
8              
9             requires 'setup_injected_component',
10             'setup_injected_components',
11             'config_for';
12              
13             our $VERSION = '0.015';
14              
15             my $adaptor_namespace = sub {
16             my $app = shift;
17             if(my $config = $app->config->{'Plugin::InjectionHelpers'}) {
18             my $namespace = $config->{adaptor_namespace};
19             return $namespace if $namespace;
20             }
21             return 'Catalyst::Model::InjectionHelpers';
22             };
23              
24             my $default_adaptor = sub {
25             my $app = shift;
26             if(my $config = $app->config->{'Plugin::InjectionHelpers'}) {
27             my $default_adaptor = $config->{default_adaptor};
28             return $default_adaptor if $default_adaptor;
29             }
30             return 'Application';
31             };
32              
33             my $normalize_adaptor = sub {
34             my $app = shift;
35             my $adaptor = shift || $app->$default_adaptor;
36             return $adaptor=~m/::/ ?
37             $adaptor : "${\$app->$adaptor_namespace}::$adaptor";
38             };
39              
40             my $debug = 1;
41             my $version = 2;
42             my %core_dispatch = (
43             '$app' => sub {
44             my $proto = shift;
45             my $maybe_app = ref $proto;
46             return $maybe_app ? $maybe_app : $proto;
47             },
48             '$ctx' => sub { shift },
49             '$req' => sub { shift->req },
50             '$res' => sub { shift->res },
51             '$log' => sub { shift->log },
52             '$user' => sub { shift->user },
53             );
54              
55             my %dispatch_table = (
56             '-core' => sub {
57             my ($app_ctx, $what) = @_;
58             return $core_dispatch{$what}->($app_ctx);
59             },
60             '-code' => sub {
61             my ($app_ctx, $code) = @_;
62             $app_ctx->log->debug("Executing code injection.") if $app_ctx->debug && $debug;
63             do {
64             $app_ctx->log->error("Provided value '$code' not a coderef.");
65             return undef;
66             } unless (ref($code) && ref($code) eq 'CODE');
67             return $code->($app_ctx);
68             },
69             '-model' => sub {
70             my ($app_ctx, $model) = @_;
71             $app_ctx->log->debug("Providing model '$model' for injection.") if $app_ctx->debug && $debug;
72             return $app_ctx->model($model);
73             },
74             '-view' => sub {
75             my ($app_ctx, $view) = @_;
76             $app_ctx->log->debug("Providing view '$view' for injection.") if $app_ctx->debug && $debug;
77             return $app_ctx->view($view);
78             },
79             '-controller' => sub {
80             my ($app_ctx, $controller) = @_;
81             $app_ctx->log->debug("Providing controller '$controller' for injection.") if $app_ctx->debug && $debug;
82             return $app_ctx->controller($controller);
83             },
84             );
85              
86             before 'setup_components', sub {
87             my ($c) = @_;
88             if (my $config = $c->config->{'Plugin::InjectionHelpers'}) {
89             if(my $custom_dispatch = $config->{dispatchers}) {
90             %dispatch_table = %{ Catalyst::Utils::merge_hashes(\%dispatch_table, $custom_dispatch) };
91             }
92             if(defined(my $has_debug_flag = $config->{debug})) {
93             $debug = $has_debug_flag;
94             }
95             if(defined(my $has_version = $config->{version})) {
96             $version = $has_version;
97             }
98             }
99             };
100              
101             before 'setup_injected_components', sub {
102             my ($class) = @_;
103             my @injectables = grep {
104             ($_ =~ m/^Model/)
105             || ($_ =~ m/^Controller/)
106             || ($_ =~ m/^View/)
107             } keys %{$class->config};
108             foreach my $comp (@injectables) {
109             next unless my $inject = delete $class->config->{$comp}->{'-inject'};
110             $class->config->{inject_components}->{$comp} = $inject;
111             }
112             };
113              
114             after 'setup_injected_component', sub {
115             my ($app, $injected_component_name, $config) = @_;
116             if(exists($config->{from_class}) || exists($config->{from_code})) {
117             my $from_class = $config->{from_class} || undef;
118             my $adaptor = $app->$normalize_adaptor($config->{adaptor});
119             my $method = $config->{method} || $config->{from_code} || 'new';
120             my @roles = @{$config->{roles} ||[]};
121              
122             Catalyst::Utils::ensure_class_loaded($from_class) if $from_class;
123             Catalyst::Utils::ensure_class_loaded($adaptor);
124              
125             my $from = $from_class || $config->{from_code};
126             my $config_namespace = $app .'::'. $injected_component_name;
127              
128             $app->components->{$config_namespace} = sub {
129             my $new_component = $adaptor->new(
130             _version=>$version,
131             application=>$app,
132             from=>$from,
133             injected_component_name=>$injected_component_name,
134             method=>$method,
135             roles=>\@roles,
136             injection_parameters=>$config,
137             ( exists $config->{transform_args} ? (transform_args => $config->{transform_args}) : ()),
138             get_config=> sub { shift->config_for($config_namespace) },
139             );
140             return $app->setup_component($new_component);
141             };
142             }
143             };
144              
145             around 'config_for', sub {
146             my ($orig, $app_or_ctx, $component_name, @args) = @_;
147             my $config = ($app_or_ctx->$orig($component_name, @args) || +{});
148             my $mapped_config = +{};
149             foreach my $key (keys %{$config||+{}}) {
150             if(ref(my $proto = $config->{$key}) eq 'HASH') {
151             my ($type) = keys %{$proto};
152             if(my $dispatchable = $dispatch_table{$type}) {
153             my $dependency = $dispatchable->($app_or_ctx, $proto->{$type});
154             if($dependency) {
155             $mapped_config->{$key} = $dependency;
156             } else {
157             $app_or_ctx->log->debug("No dependency type '$type' of '$proto->{$type}' for '$component_name'")
158             if $app_or_ctx->debug;
159             }
160             } else {
161             $app_or_ctx->log->debug("Can't inject dependency '$type' for '$component_name'")
162             if $app_or_ctx->debug;
163             }
164             }
165             }
166             return my $merged = Catalyst::Utils::merge_hashes($config, $mapped_config);
167             };
168              
169             1;
170              
171             =head1 NAME
172              
173             Catalyst::Plugin::InjectionHelpers - Enhance Catalyst Component Injection
174              
175             =head1 SYNOPSIS
176              
177             Use the plugin in your application class:
178              
179             package MyApp;
180             use Catalyst 'InjectionHelpers';
181              
182             MyApp->config(
183             'Model::SingletonA' => {
184             -inject => {
185             from_class=>'MyApp::Singleton',
186             adaptor=>'Application',
187             roles=>['MyApp::Role::Foo'],
188             method=>'new',
189             },
190             aaa => 100,
191             },
192             'Model::SingletonB' => {
193             -inject => {
194             from_class=>'MyApp::Singleton',
195             adaptor=>'Application',
196             method=>sub {
197             my ($adaptor_instance, $from_class, $app, %args) = @_;
198             return $from_class->new(aaa=>$args{arg});
199             },
200             arg => 300,
201             },
202             );
203              
204             MyApp->setup;
205              
206             Alternatively you can use the 'inject_components' class method:
207              
208             package MyApp;
209             use Catalyst 'InjectionHelpers';
210              
211             MyApp->inject_components(
212             'Model::SingletonA' => {
213             from_class=>'MyApp::Singleton',
214             adaptor=>'Application',
215             roles=>['MyApp::Role::Foo'],
216             method=>'new',
217             },
218             'Model::SingletonB' => {
219             from_class=>'MyApp::Singleton',
220             adaptor=>'Application',
221             method=>sub {
222             my ($adaptor_instance, $from_class, $app, %args) = @_;
223             return $class->new(aaa=>$args{arg});
224             },
225             },
226             );
227              
228             MyApp->config(
229             'Model::SingletonA' => { aaa=>100 },
230             'Model::SingletonB' => { arg=>300 },
231             );
232              
233             MyApp->setup;
234              
235             The first method is a better choice if you need to alter how your injections work
236             based on configuration that is controlled per environment.
237              
238             =head1 DESCRIPTION
239              
240             B<NOTE> Starting with C<VERSION> 0.012 there is a breaking change in the number
241             of arguments that the C<method> and C<from_code> callbacks get. If you need to
242             keep backwards compatibility you should set the version flag to 1:
243              
244             MyApp->config(
245             'Plugin::InjectionHelpers' => { version => 1 },
246             ## Additional configuration as needed
247             );
248              
249             This plugin enhances the build in component injection features of L<Catalyst>
250             (since v5.90090) to make it easy to bring non L<Catalyst::Component> classes
251             into your application. You may consider using this for what you often used
252             L<Catalyst::Model::Adaptor> in the past for (although there is no reason to
253             stop using that if you are doing so, its not a 'broken' approach, but for the
254             very simple cases this might suffice and allow you to reduce the number of nearly
255             empty 'boilerplate' classes in your application.)
256              
257             You should be familiar with how component injection works in newer versions of
258             L<Catalyst> (v5.90090+).
259              
260             It also experimentally supports a mechanism for dependency injection (that is
261             the ability to set other componements as initialization arguments, similar to
262             how you might see this work with inversion of control frameworks such as
263             L<Bread::Board>.) Author has no plan to move this past experimental status; he
264             is merely publishing code that he's used on jobs where the code worked for the
265             exact cases he was using it for the purposes of easing long term maintainance
266             on those projects. If you like this feature and would like to see it stablized
267             it will be on you to help the author validate it; its not impossible more changes
268             and pontentially breaking changes will be needed to make that happen, and its
269             also not impossible that changes to core L<Catalyst> would be needed as well.
270             Reports from users in the wild greatly appreciated.
271              
272             =head1 USAGE
273              
274             MyApp->config(
275             $model_name => +{
276             -inject => +{ %injection_args },
277             \%configuration_args;
278             or
279              
280             MyApp->inject_components($model_name => \%injection_args);
281             MyApp->config($model_name => \%configuration_args);
282              
283              
284             Where C<$model_name> is the name of the component as it is in your L<Catalyst>
285             application (ie 'Model::User', 'View::HTML', 'Controller::Static') and C<%injection_args>
286             are key /values as described below:
287              
288             =head2 from_class
289              
290             This is the full namespace of the class you are adapting to use as a L<Catalyst>
291             component. Example 'MyApp::Class'.
292              
293             =head2 from_code
294              
295             This is a codereference that generates your component instance. Used when you
296             don't have a class you wish to adapt (handy for prototyping or small components).
297              
298             MyApp->inject_components(
299             'Model::Foo' => {
300             from_code => sub {
301             my ($app_ctx, %args) = @_;
302             return $XX;
303             },
304             adaptor => 'Factory',
305             },
306             );
307              
308             C<$app_ctx> is either the application class or L<Catalyst> context, depending on the
309             scope of your component.
310              
311             If you use this you should not define the 'method' key or the 'roles' key (below).
312              
313             =head2 roles
314              
315             A list of L<Moose::Roles>s that will be composed into the 'from_class' prior
316             to creating an instance of that class. Useful if you apply roles for debugging
317             or testing in certain environments.
318              
319             =head2 method
320              
321             Either a string or a coderef. If left empty this defaults to 'new'.
322              
323             The name of the method used to create the adapted class instance. Generally this
324             is 'new'. If you have complex instantiation requirements you may instead use
325             a coderef. If so, your coderef will receive three arguments. The first is the name
326             of the from_class. The second is either
327             the application or context, depending on the type adaptor. The third is a hash
328             of arguments which merges the global configuration for the named component along
329             with any arguments passed in the request for the component (this only makes
330             sense for non application scoped models, btw).
331              
332             Example:
333              
334             MyApp->inject_components(
335             'Model::Foo' => {
336             from_class => 'Foo',
337             method => sub {
338             my ($from_class, $app_or_ctx, %args) = @_;
339             },
340             adaptor => 'Factory',
341             },
342             );
343              
344             Argument details:
345              
346             =over 4
347              
348             =item $from_class
349              
350             The name of the class you set in the 'from_class' parameter.
351              
352             =item $app_or_ctx
353              
354             Either your application class or a reference to the current context, depending on how
355             the adaptore is scoped (PerRequest and Factory get $ctx).
356              
357             =item %args
358              
359             A Hash of the configuration parameters from your application configuration. If the
360             adaptor is context/request scoped, also combines any arguments included in the call
361             for the component. for example:
362              
363             package MyApp;
364              
365             use Catalyst;
366              
367             MyApp->inject_components( 'Model::Foo' => { from_class=>"Foo", adaptor=>'Factory' });
368             MyApp->config( 'Model::Foo' => { aaa => 111 } )
369             MyApp->setup;
370              
371             If in an action you say:
372              
373             my $model = $c->model('Foo', bbb=>222);
374              
375             Then C<%args> would be:
376              
377             (aaa=>111, bbb=>222);
378              
379             B<NOTE> Please keep in mind supplying arguments in the ->model call (or ->view for
380             that matter) only makes sense for components that ACCEPT_CONTEXT (in this case
381             are Factory, PerRequest or PerSession adaptor types).
382              
383             =back
384              
385             =head2 transform_args
386              
387             A coderef that you can use to transform configuration arguments into something
388             more suitable for your class. For example, the configuration args is typically
389             a hash, but your object class may require some positional arguments.
390              
391             MyApp->inject_components(
392             'Model::Foo' => {
393             from_class = 'Foo',
394             transform_args => sub {
395             my (%args) = @_;
396             my $path = delete $args{path},
397             return ($path, %args);
398             },
399             },
400             );
401              
402             Should return the args as they as used by the initialization method of the
403             'from_class'.
404              
405             Use 'transform_args' when you just need to tweak how your object uses arguments
406             and use 'from_code' or 'method' when you need more control on what kind of object
407             is returned (in other words choose the smallest hammer for the job).
408              
409             =head2 adaptor
410              
411             The adaptor used to bring your 'from_class' into L<Catalyst>. Out of the box
412             there are three adaptors (described in detail below): Application, Factory and
413             PerRequest. The default is Application. You may create your own adaptors; if
414             you do so you should use the full namespace as the value (MyApp::Adaptors::MySpecialAdaptor).
415              
416             =head1 ADAPTORS
417              
418             Out of the box this plugin comes with the following three adaptors. All canonical
419             adaptors are under the namespace 'Catalyst::Model::InjectionHelpers'.
420              
421             =head2 Application
422              
423             Model is application scoped. This means you get one instance shared for the entire
424             lifecycle of the application.
425              
426             =head2 Factory
427              
428             Model is scoped to the request. Each call to $c->model($model_name) returns a new
429             instance of the model. You may pass additional parameters in the model call,
430             which are merged to the global parameters defined in configuration and used as
431             part of the object initialization.
432              
433             =head2 PerRequest
434              
435             Model is scoped to the request. The first time in a request that you call for the
436             model, a new model is created. After that, all calls to the model return the original
437             instance, until the request is completed, after which the instance is destroyed when
438             the request goes out of scope.
439              
440             The first time you call this model you may pass additional parameters, which get
441             merged with the global configuration and used to initialize the model.
442              
443             =head2 PerSession.
444              
445             Scoped to a session. Requires the Session plugin.
446             See L<Catalyst::Model::InjectionHelpers::PerSession> for more.
447              
448             =head2 Creating your own adaptor
449              
450             Your new adaptor should consume the role L<Catalyst::ModelRole::InjectionHelpers>
451             and provide a method ACCEPT_CONTEXT which must return the component you wish to
452             inject. Please review the existing adaptors and that role for insights.
453              
454             =head1 DEPENDENCY INJECTION
455              
456             Often when you are setting configuration options for your components, you might
457             desire to 'depend on' other existing components. This design pattern is called
458             'Inversion of Control', and you might be familiar with it from prior art on CPAN
459             such as L<IOC>, L<Bread::Board> and L<Beam::Wire>.
460              
461             The IOC features that are exposed via this plugin are basic and marked experimental
462             (please see preceding note). The are however presented to the L<Catalyst> community
463             with the hope of provoking thought and discussion (or at the very least put an
464             end to the idea that this is something people actually care about).
465              
466             To use this feature you simply tag configuration keys as 'dependent' using a
467             hashref for the key value. For example, here we define an inline model that
468             is a L<DBI> C<$dbh> and a User model that depends on it:
469              
470             MyApp->config(
471             'Model::DBH' => {
472             -inject => {
473             adaptor => 'Application',
474             from_code => sub {
475             my ($app, @args) = @_;
476             return DBI->connect(@args);
477             },
478             },
479             %DBI_Connection_Args,
480             },
481             'Model::User' => {
482             -inject => {
483             from_class => 'MyApp::User',
484             adaptor => 'Factory',
485             },
486             dbh => { -model => 'DBH' },
487             },
488             # Additional configuration as needed
489             );
490              
491             Now in you code (say in a controller if you do:
492              
493             my $user = $c->model('User');
494              
495             We automatically resolve the value for C<dbh> to be $c->model('DBH') and
496             supply it as an argument.
497              
498             Currently we only support dependency substitutions on the first level of
499             arguments.
500              
501             All injection syntax takes the form of "$argument_key => { $type => $parameter }"
502             where the following $types are supported
503              
504             =over 4
505              
506             =item -model => $model_name
507              
508             =item -view => $view_name
509              
510             =item -controller => $controller_name
511              
512             Provide dependency in the form of $c->model($model_name) (or $c->view($view_name),
513             $c->controller($controller_name)).
514              
515             =item -code => $subref
516              
517             Custom dependency that resolves from a subref. Example:
518              
519             MyApp->config(
520             'Model::User' => {
521             current_time => {
522             -code => sub {
523             my $app_or_context = shift;
524             return DateTime->now;
525             },
526             },
527             },
528             # Rest of configuration
529             );
530              
531             Please keep in mind that you must return an object. C<$app_or_context> will be
532             either the application class or $c (context) depending on the type of model (if
533             it accepts context or not).
534              
535             =item -core => $target
536              
537             This exposes some core objects such as $app, $c etc. Where $target is:
538              
539             =over 8
540              
541             =item $app
542              
543             The name of the application class.
544              
545             =item $ctx
546              
547             The result of C<$c>. Please note its probably bad form to pass the entire
548             context object as it leads to unnecessary tight coupling.
549              
550             =item $req
551              
552             The result of C<< $c->req >>
553              
554             =item $res
555              
556             The result of C<< $c->res >>
557              
558             =item $log
559              
560             The result of C<< $c->log >>
561              
562             =item $user
563              
564             The result of C<< $c->user >> (if it exists, you should either define it or
565             use the Authentication plugin).
566              
567             =back
568              
569             =back
570              
571             =head1 CONFIGURATION
572              
573             This plugin defines the following possible configuration. As per L<Catalyst>
574             standards, these configuration keys fall under the 'Plugin::InjectionHelpers'
575             namespace in the configuration hash.
576              
577             =head2 adaptor_namespace
578              
579             Default namespace to look for adaptors. Defaults to L<Catalyst::Model::InjectionHelpers>
580              
581             =head2 default_adaptor
582              
583             The default adaptor to use, should you not set one. Defaults to 'Application'.
584              
585             =head2 dispatchers
586              
587             Allows you to add to the default dependency injection handers:
588              
589             MyApp->config(
590             'Plugin::InjectionHelpers' => {
591             dispatchers => {
592             '-my' => sub {
593             my ($app_ctx, $what) = @_;
594             warn "asking for a -my $what";
595             return ....;
596             },
597             },
598             },
599             # Rest of configuration
600             );
601              
602             =head2 version
603              
604             Default is 2. Set to 1 if you are need compatibility version 0.011 or older
605             style of arguments for 'method' and 'from_code'.
606              
607             =head1 Catalyst::Plugin::ConfigLoader
608              
609             When using this plugin with L<Catalyst::Plugin::ConfigLoader> you should add it to the
610             plugin list afterward, for example:
611              
612             package MyApp;
613              
614             use Catalyst 'ConfigLoader',
615             'InjectionHelpers';
616              
617             Please keep in mind that due to the way Configloader merges the configuration files
618             you might have to set some things to C<undef> in order to get the correct behavior. For
619             example you might define a model by default using from_code:
620              
621             package MyApp;
622              
623             use Catalyst 'ConfigLoader',
624             'InjectionHelpers';
625              
626             MyApp->config(
627             'Model::Foo' => {
628             -inject => {
629             from_code => sub {
630             my ($app, %args) = @_;
631             return bless +{ %args, app=>$app }, 'Dummy1';
632             },
633             },
634             bar => 'baz',
635             },
636             );
637              
638             MyApp->setup;
639              
640             But then in youe configuration file overlay, you want to specify a class. In that case you
641             will need to undefine the default keys:
642              
643             # File:myapp_local.pl
644             return +{
645             'Model::Foo' => {
646             -inject => {
647             from_class => 'MyApp::Dummy2',
648             from_code => undef, # Need to blow away the existing...
649             },
650             },
651             };
652              
653             Its probably not ideal that the configuration overlay doesn't permit you to tag refs as 'replace'
654             rather than 'merge' but this is not a problem with this plugin. If it bothers you that a
655             configuration overlay would require to have understanding of how 'lower' configurations are setup
656             you should be able to avoid it by using all the same keys.
657              
658             =head1 PRIOR ART
659              
660             You may wish to review other similar approach on CPAN:
661              
662             L<Catalyst::Model::Adaptor>.
663              
664             =head1 AUTHOR
665              
666             John Napiorkowski L<email:jjnapiork@cpan.org>
667            
668             =head1 SEE ALSO
669            
670             L<Catalyst>, L<Catalyst::Model::InjectionHelpers::Application>,
671             L<Catalyst::Model::InjectionHelpers::Factory>, L<Catalyst::Model::InjectionHelpers::PerRequest>
672             L<Catalyst::ModelRole::InjectionHelpers>
673              
674             =head1 COPYRIGHT & LICENSE
675            
676             Copyright 2016, John Napiorkowski L<email:jjnapiork@cpan.org>
677            
678             This library is free software; you can redistribute it and/or modify it under
679             the same terms as Perl itself.
680            
681             =cut