File Coverage

lib/MooseX/DIC.pm
Criterion Covered Total %
statement 11 11 100.0
branch 1 2 50.0
condition 2 2 100.0
subroutine 3 3 100.0
pod 0 1 0.0
total 17 19 89.4


line stmt bran cond sub pod time code
1             package MooseX::DIC;
2              
3             our $VERSION = '0.4.0';
4              
5 5     5   1063967 use MooseX::DIC::ContainerFactory;
  5         27  
  5         537  
6             use MooseX::DIC::Injected
7 5     5   2406 ; # We load this here to have the trait available further on.
  5         20  
  5         1400  
8              
9             require Exporter;
10             @ISA = qw/Exporter/;
11             @EXPORT_OK = qw/build_container/;
12              
13             sub build_container {
14 8     8 0 42618 my %options = @_;
15              
16             ContainerConfigurationException->throw(
17             message => 'scan_path is a mandatory parameter')
18 8 50       50 unless exists $options{scan_path};
19 8   100     66 $options{environment} = $options{environment} || 'default';
20              
21             my $container_factory = MooseX::DIC::ContainerFactory->new(
22             scan_path => $options{scan_path},
23             environment => $options{environment}
24 8         147 );
25              
26 8         13442 return $container_factory->build_container;
27             }
28              
29             1;
30              
31             =encoding UTF-8
32              
33             =head1 NAME
34              
35             MooseX::DIC - A dependency injector container for Moose
36              
37             =head1 DESCRIPTION
38              
39             Full documentation on the L<MooseX::DIC Webpage|http://docs.moosex-dic.org>.
40              
41             MooseX::DIC is a dependency injection container tailored to L<Moose>, living in a full OOP environment and greatly
42             inspired by Java DIC frameworks like L<Spring|https://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html>
43             or L<CDI|http://docs.oracle.com/javaee/6/tutorial/doc/gjbnr.html>.
44              
45             The goal of this library is to provide an easy to use DI container with automatic wiring of dependencies via constructor
46             by class type (ideally by Role/Interface).
47              
48             The configuration is performed either by the use of L<Marker roles|https://en.wikipedia.org/wiki/Marker_interface_pattern> and
49             a specific trait on attributes that have to be injected, or by use of a very terse and composable yaml config file, using sensible
50             defaults to cover 90% of the use cases to minimize boilerplate.
51              
52             One of the principal tenets of the library is that while code may be poluted by the use of DIC roles and traits, it
53             should work without a running container. The classes are fully functional without the dependency injection, the library
54             is just a convenient way to wire dependencies (this is mainly accomplished by forbidding non L<constructor injection|https://en.wikipedia.org/wiki/Dependency_injection#Constructor_injection>).
55              
56             This library is designed to be used on long-running processes where startup time is not a concern (within reason, of
57             course). The container will scan all configured paths to look for services to inject and classes that need injection.
58              
59             There is a great amount of flexibility to account for testing environments, non-moose libraries, alternative
60             implementations of services, etc, although none of it is needed for a simple usage.
61              
62             =head1 SYNOPSIS
63              
64             A service is injectable if it consumes the Role L<MooseX::DIC::Injectable>, which is a parameterized role.
65              
66             package MyApp::LDAPAuthService;
67            
68             use Moose;
69             with 'MyApp::AuthService';
70            
71             with 'MooseX::DIC::Injectable' => {
72             implements => 'MyApp::AuthService',
73             qualifiers => [ 'LDAP' ],
74             environment => 'test',
75             scope => 'singleton'
76             };
77              
78             has ldap => (is => 'ro', does => 'LDAP' );
79              
80             1;
81              
82             We can see that this service is both an injectable service and consumes another injectable service,LDAP. We register a
83             class as injectable into the container registry by consuming the L<MooseX::DIC::Injectable> role, and we get injected
84             dependencies automatically if the container can find them.
85              
86             None of the parameters of the L<MooseX::DIC::Injectable> role are mandatory, they have defaults or can be inferred.
87             On the example above, the role/interface the LDAPAuthService was implementing could be inferred from the
88             C<with 'MyApp::AuthService'> previous line.
89              
90             To use this service:
91              
92             package MyApp::LoginController;
93            
94             use Moose;
95              
96             has auth_service => ( is=>'ro', does => 'MyApp::AuthService' );
97              
98             sub do_login {
99             my ($self,$request) = @_;
100            
101             if($self->auth_service->login($request->username,$request->password)) {
102             print 'this is fine';
103             }
104             }
105              
106             1;
107              
108             The dependency will have been injected automatically as long as the Logincontroller
109             was created by the container.
110              
111             =head1 Starting the Container
112              
113             When starting your application, the container must be launched to start it's
114             scanning. You can tell the container which folders to scan in search of injectable
115             services or of wiring files (or both!).
116             This operation is slow as it has to scan every file under the specified folders,
117             which means you will usually only use one container per application.
118              
119             To start the container:
120              
121             #!/usr/bin/env perl
122             use strict;
123             use warning;
124              
125             use MooseX::DIC 'build_container';
126             use MyApp::Launcher;
127              
128             # This may take some time depending on your lib size
129             my $container = build_container( scan_path => [ 'lib' ] );
130              
131             # The launcher is a fully injected service, with all dependencies
132             # provided by the container.
133             my $app = $container->get_service 'MyApp::Launcher';
134             $app->start;
135              
136             exit 0;
137              
138             As in the rest of dependency injection containers: Once the root object of your
139             application is created by the container, the rest of object will have been fetched
140             automatically.
141              
142             =head1 Advanced use cases
143              
144             =head2 Scopes
145              
146             =head3 Service scope
147              
148             Although the vast majority of services we want to inject are by their stateless nature candidates to be singletons, we
149             may want for our service to be instantiated every time they are requested. For example, an http agent could be
150             instantiated once per service.
151              
152             package MyApp::LWPHTTPAgent;
153              
154             use LWP::UserAgent;
155              
156             use Moose;
157             with 'MyApp::HTTPAgent';
158             with 'MooseX::DIC::Injectable' => { scope => 'request' };
159              
160             has ua => ( is => 'ro', isa => 'LWP::UserAgent', default => sub { LWP::UserAgent->new; } );
161              
162             sub request {
163             $self->ua->request(shift);
164             }
165              
166             1;
167              
168             This service declares that it can be injected on attributes that need an object that does 'MyApp::HTTPAgent' and that
169             each time it is called, it will be created anew. To use it:
170              
171             package MyApp::RESTUserService;
172              
173             use Moose;
174             with 'MyApp::UserService';
175              
176             has http_client => ( is => 'ro', does => 'MyApp::HTTPAgent' );
177              
178             sub persist {
179             my ($self,$user) = @_;
180              
181             # A new instance is created here and lives for as long as
182             # the RESTUserService lives.
183             $self->http_client->request(...);
184             }
185              
186             Two types of scope are available for services:
187              
188             =over 4
189              
190             =item singleton
191              
192             The default scope, the registry will only keep one copy of the service and will inject it into every attribute it is
193             requested.
194              
195             Make sure the service is stateless or you will run into race conditions.
196              
197             =item request
198              
199             Each time the service is requested, a new instance of it will be created. Useful for stateful services.
200              
201             =back
202              
203             =head3 Injection scope
204              
205             For services which are request scoped, the requester can also ask the injection container to create a new service each
206             time the accessor is used, for stateful services that should only live once per use. For example, we may be interested
207             in using an http user agent that somehow keeps some states between callings and if used for different purposes would be
208             corrupted.
209              
210             package MyApp::RESTUserService;
211              
212             use Moose;
213             with 'MyApp::UserService';
214              
215             has http_client => ( is => 'ro', does => 'MyApp::HTTPAgent', scope => 'request', traits => [ 'Injected' ] );
216              
217             sub persist {
218             my ($self,$user) = @_;
219              
220             # A new instance of MyApp::LWPHTTPAgent is created here
221             $self->http_client->request(...);
222              
223             # Yet another instance of MyApp::LWPHTTPAgent is created again
224             $self->http_client->request(...);
225              
226             # If we want to keep the same instance for a series of calls, reference it.
227             my $ua = $self->http_client;
228             $ua->request(...);
229             $ua->request(...);
230             }
231              
232             Please take note of the new trait we've used for the injected attribute. When we only want singleton services to be injected,
233             there's no need to configure the attribute. But when we want to apply a configuration on how the attribute must be
234             injected, then we must use the L<MooseX::DIC::Injected> trait on the attribute which allows to specify scope and qualifiers for the
235             injection.
236              
237             There are two scopes available for the injection scope:
238              
239             =over 4
240              
241             =item object
242              
243             The default scope. For request scoped services, the service is instantiated once per object.
244              
245             =item request
246              
247             For request scoped services, if the injection scope is request too, an accessor is created that will fetch a new
248             instance of the service each time it is called.
249              
250             =back
251              
252             The injection scope only makes sense for request scoped services, since singleton services will only be instantiated
253             once.
254              
255             It is a configuration error to ask for a singleton scoped service into a request-scoped injection point, and the
256             container will generate an exception when it encounters this situation (in the spirit of detecting errors as soon as
257             possible).
258              
259             =head2 Qualifiers (TBD)
260              
261             =head3 Qualifiers usage
262              
263             Sometimes, we want a Role/Interface to be implemented by many classes and to let the caller specify which one it wants.
264              
265             While this would seem to oppose the very idea of letting a container to give you objects, in fact it doesn't, and gives
266             a great deal of flexibility while still allowing the container to choose the best implementator for your caller and
267             initialize it.
268              
269             Qualifiers let a service specify with a more fine-grained precision how they implement an interface, so that callers can
270             choose them based on those qualifiers.
271              
272             For example, we can have two implementators of an HTTPAgent service:
273              
274             package MyApp::LWPHTTPAgent;
275              
276             use Moose;
277             with 'MyApp::HTTPAgent';
278              
279             with 'MooseX::DIC::Injectable' => { qualifiers => [ 'sync' ] };
280              
281             sub request {
282             # returns the response
283             }
284              
285              
286             package MyApp::AsyncHTTPAgent;
287             use Moose;
288             with 'MyApp::HTTPAgent';
289              
290             with 'MooseX::DIC::Injectable' => { qualifiers => [ 'async' ] };
291              
292             sub request {
293             # returns a Promise with the response
294             }
295              
296             package MyApp::RESTUserService;
297              
298             use Moose;
299             use MooseX::DIC;
300              
301             has http_client => ( is => 'ro', does => 'MyApp::HTTPAgent', qualifiers => [ 'async' ], injected);
302              
303             sub persist {
304             # This service knows it can expect a Promise result
305             # from the http agent, since it asked for the async version.
306             return $self->http_client->request(...)
307             ->then(sub {
308             ...
309             })
310             ->catch(sub {
311             ...
312             });
313             }
314              
315             It is a configuration error to have two implementators of the same service living in the same L<environment|/Environments>
316             without at least one of them having a qualifier, and the container will generate an exception when it encounters that
317             situation.
318              
319             Take note of the B<injected> keyword. It's sugar syntax to avoid using the trait. Although by using it, you tie your code
320             more tightly to the MooseX::DIC framework.
321              
322             =head3 Qualifiers match resolution
323              
324             When there are competing implementators for the same caller, which have different qualifiers, the resolution is based
325             on the following rule: The longest most precise qualifier match is returned
326              
327             If the caller requests for qualifiers 'a','b' and 'c', given the following service implementations:
328              
329             =over 4
330              
331             =item Impl1 => qualifiers 'a','d'
332              
333             =item Impl2 => qualifiers 'b', 'c'
334              
335             =item Impl3 => qualifiers 'a'
336              
337             =back
338              
339             The implementator Impl2 will be selected, since it has the greater number of matching qualifiers.
340              
341             If no exact qualifier match is found, the next best match is selected. Example:
342              
343             Given a caller that requests a Service with qualifiers 'a', 'b', and 'c'. For the following implementations:
344              
345             =over 4
346              
347             =item Impl1 => qualifiers 'a'
348              
349             =item Impl2 => no qualifiers
350              
351             =back
352              
353             The Impl1 will be selected even though it doesn't match all caller qualifiers.
354              
355             Given a caller that requests a Service with qualifiers and only one implementator with no qualifiers, the implementator
356             will still be selected.
357              
358             Given a caller that requests a Service with qualifier 'a', for the following implementations:
359              
360             =over 4
361              
362             =item Impl1 => qualifier 'b'
363              
364             =item Impl2 => qualifier 'c'
365              
366             =item Impl3 => no qualifiers
367              
368             =back
369              
370             One of the three implementations (always randomly) will be returned, since they are all equal matches. The random
371             selection will be enforced to avoid library clients shooting themselves on the foot by relying on a specific selection
372             when there are equal matches.
373              
374             Following the last example, if a client specifically wants an implementation with no qualifiers it can specify it by
375             setting the qualifier parameter of the attribute to empty array:
376              
377             package MyApp::ExampleController;
378              
379             use Moose;
380             use MooseX::DIC;
381              
382             has service => ( is => 'ro', does => 'ServiceRole', qualifiers => [], injected );
383              
384             =head2 Environments
385              
386             Sometimes, we want the wiring of services to depend on a runtime environment. To this end, we use the concept of
387             environments.
388              
389             By default (that is, if no environment is declared by an C<MooseX::DIC::Injectable> service) all services live inside the 'default'
390             environment. But we can do more. Let's consider the following services:
391              
392             package MyApp::UserRepository;
393              
394             use Moose::Role;
395              
396              
397             package MyApp::UserRepository::Database;
398              
399             use Moose;
400             with 'MyApp::UserRepository';
401              
402             with 'MooseX::DIC::Injectable' => { environment => 'production' };
403              
404              
405             package MyApp::UserRepository::InMemory;
406              
407             use Moose;
408             with 'MyApp::UserRepository';
409              
410             with 'MooseX::DIC::Injectable' => { environment => 'test' };
411              
412             With the following caller:
413              
414             package MyApp::UserController;
415              
416             use Moose;
417              
418             has repository => (is => 'ro', does => 'MyApp::UserRepository' );
419              
420             sub do_something {
421             my ($self,$user) = @_;
422             $self->repository->persist($user);
423             }
424              
425             These implementations live in different environments and they won't see each other. The selection of one or the other
426             will depend on which environment we launch the container in, as in:
427              
428             #!/usr/bin/env perl
429             use strict;
430             use warning;
431              
432             use MooseX::DIC 'build_container';
433              
434             my $container = build_container ( scan_path => 'lib', environment => 'test' );
435              
436             # In the test environment, the UserController class will have received
437             # The InMemory user repository.
438             my $user_controller = $container->get_service 'MyApp::UserController'
439              
440             When the container doesn't find a service in a given environment, it will fall back to the default environment. If it
441             doesn't find a service there, it will throw an exception.
442              
443             =head1 Configuration by YAML
444              
445             While you can configure the services and the attribute injection points by use of the
446             C<MooseX::DIC::Injectable> and C<MooseX::DIC::Injected> roles directly on your code, you may want to
447             configure the container with an external YAML config file.
448              
449             This way, you can avoid tainting your code with infrastructure concerns. Everything that you can
450             configure with the marker interfaces, you can do with the yaml config file.
451              
452             Example:
453              
454             # moosex-dic-wiring.yml
455             include:
456             - included_config_file.yml
457             mappings:
458             MyApp::LoginService:
459             MyApp::LoginService::LDAP:
460             qualifiers:
461             - ldap
462             dependencies:
463             ldap:
464             scope: request
465             MyApp::LoginService::Database:
466             qualifiers:
467             - database
468             MyApp::LoginService::InMemory:
469             environment: test
470             MyApp::LDAP:
471             MyApp::LDAP:
472             scope: request
473             builder: factory
474             MyApp::HTTPClient:
475             MyApp::HTTPClient::LWP:
476             builder: factory
477             scope: request
478             qualifiers:
479             - sync
480             MyApp::HTTPClient::Mojo:
481             builder: factory
482             scope: request
483             qualifiers:
484             - async
485              
486             Only what is different from the defaults needs to be configured. A wiring config file could be reduced to:
487              
488             # moosex-dic-wiring.yml
489             mappings:
490             MyApp::LoginService: MyApp::LoginService::Database
491             MyApp::LDAP: MyApp::LDAP
492             MyApp::HTTPClient: MyApp::HTTPClient::LWP
493              
494             If there's only a single mapping between an interface and it's implementation, and it's a Moose singleton
495             stateless service.
496              
497             =head1 AUTHOR
498              
499             Loïc Prieto Dehennault
500             CPAN ID: LPRIETO
501             CAPSiDE
502             loic.prieto@capside.com
503              
504             =head1 SEE ALSO
505              
506             L<https://metacpan.org/pod/Moose>
507              
508             L<http://docs.oracle.com/javaee/6/tutorial/doc/giwhl.html>
509              
510             L<https://spring.io/>
511              
512             =head1 BUGS and SOURCE
513              
514             The source code is located here: L<https://github.com/loic-prieto/moosex-dic>
515              
516             Please report bugs to: L<https://github.com/loic-prieto/moosex-dic/issues>
517              
518             =head1 COPYRIGHT and LICENSE
519              
520             Copyright (c) 2017 by CAPSiDE
521              
522             This code is distributed under the Apache 2 License. The full text of the license can be found in the LICENSE file included with this module.