From f0b5a156e89456e6280066b71f82cfa8af483ea9 Mon Sep 17 00:00:00 2001 From: Doug Bell Date: Fri, 26 Feb 2016 21:18:40 -0600 Subject: [PATCH] rewrite and expand the documentation The new documentation moves the tutorial stuff into another file, and better explains the inner workings of the container by explaining its methods. Fixes #21 Fixes #29 Fixes #41 --- lib/Beam/Wire.pm | 953 +++++++++++++++------------------- lib/Beam/Wire/Help/Config.pod | 755 +++++++++++++++++++++++++++ 2 files changed, 1164 insertions(+), 544 deletions(-) create mode 100644 lib/Beam/Wire/Help/Config.pod diff --git a/lib/Beam/Wire.pm b/lib/Beam/Wire.pm index b8c70b2..0c1451f 100644 --- a/lib/Beam/Wire.pm +++ b/lib/Beam/Wire.pm @@ -1,484 +1,65 @@ package Beam::Wire; -# ABSTRACT: Lightweight Dependency Injection Container - -use strict; -use warnings; -use Scalar::Util qw( blessed ); -use Moo; -use Config::Any; -use Module::Runtime qw( use_module ); -use Data::DPath qw ( dpath ); -use Path::Tiny qw( path ); -use File::Basename qw( dirname ); -use Types::Standard qw( :all ); +# ABSTRACT: Lightweight Dependency Injection Container =head1 SYNOPSIS # wire.yml - - dbh: - class: 'DBI' - method: connect + captain: + class: Person args: - - 'dbi:mysql:dbname' - - { - PrintError: 1 - } - - # myscript.pl + name: Malcolm Reynolds + rank: Captain + # script.pl use Beam::Wire; - my $wire = Beam::Wire->new( file => 'wire.yml' ); - my $dbh = $wire->get( 'dbh' ); - $wire->set( 'dbh' => DBI->new( 'dbi:pgsql:dbname' ) ); + my $captain = $wire->get( 'captain' ); + print $captain->name; # "Malcolm Reynolds" =head1 DESCRIPTION -Beam::Wire is a dependency injection (DI) container. A DI (dependency injection) -container is a framework/mechanism where dependency creation and instantiation is -handled automatically (e.g. creates instances of classes that implement a given -dependency interface on request). DI does not require a container, in-fact, DI -without a container is possible and simply infers that dependency creation isn't -automatically handled for you (i.e. you have to write code to instantiate the -dependencies manually). - -Dependency injection (DI) at it's core is about creating loosely coupled code by -separating construction logic from application logic. This is done by pushing -the creation of services (dependencies) to the entry point(s) and writing the -application logic so that dependencies are provided for its components. The -application logic doesn't know or care how it is supplied with its dependencies; -it just requires them and therefore receives them. - -=head1 OVERVIEW - -Beam::Wire loads a configuration L and stores the specified configuration -in the L which is used to resolve it's services. This section -will give you an overview of how to declare dependencies and services, and shape -your configuration file. - -=head2 WHAT IS A DEPENDENCY? - -A dependency is a declaration of a component requirement. In layman's terms, a -dependency is a class attribute (or any value required for class construction) -which will likely be used to define services. - -=head2 WHAT IS A SERVICE? - -A service is a resolvable interface which may be selected and implemented on -behalf of a dependent component, or instantiated and returned per request. In -layman's terms, a service is a class configuration which can be used -independently or as a dependent of other services. - -=head2 HOW ARE SERVICES CONFIGURED? - - # databases.yml - - production_db: - class: 'DBI' - method: connect - args: - - 'dbi:mysql:master' - - { PrintError: 0, RaiseError: 0 } - production_cache: - class: 'CHI' - args: - driver: 'DBI' - dbh: { $ref: 'production_db' } - development_db: - class: 'DBI' - method: connect - args: - - 'dbi:mysql:slave' - - { PrintError: 1, RaiseError: 1 } - development_cache: - class: 'CHI' - args: - driver: 'DBI' - dbh: { $ref: 'development_db' } - -=head3 Service Attributes - -=head4 class - -The class to instantiate. The class will be loaded and the C (below) -method called. - -=head4 method - -The class method to call to construct the object. Defaults to C. - -If multiple methods are needed to initialize an object, C can be an -arrayref of hashrefs, like so: - - my_service: - class: My::Service - method: - - method: new - args: - foo: bar - - method: set_baz - args: - - Fizz - -In this example, first we call Cnew( foo => "bar" );> to get our -object, then we call C<$obj->set_baz( "Fizz" );> as a further initialization -step. - -To chain methods together, add C: - - my_service: - class: My::Service - method: - - method: new - args: - foo: bar - - method: set_baz - return: chain - args: - - Fizz - - method: set_buzz - return: chain - args: - - Bork - -This example is equivalent to the following code: - - my $service = My::Service->new( foo => "bar" )->set_baz( "Fizz" ) - ->set_buzz( "Bork" ); - -=head4 args - -The arguments to the C method. This can be either an array or a hash, -like so: - - # array - dbh: - class: DBI - method: connect - args: - - 'dbi:mysql:dbname' - - # hash - cache: - class: CHI - args: - driver: Memory - max_size: 16MB - -Using the array of arguments, you can give arrayrefs or hashrefs: +Beam::Wire is a configuration module and a dependency injection +container. In addition to complex data structures, Beam::Wire configures +and creates plain old Perl objects. - # arrayref of arrayrefs - names: - class: 'Set::CrossProduct' - args: - - - - [ 'Foo', 'Barkowictz' ] - - [ 'Bar', 'Foosmith' ] - - [ 'Baz', 'Bazleton' ] - - # arrayrefs of hashrefs - cache: - class: CHI - args: - - driver: Memory - max_size: 16MB - -=head4 extends - -Inherit and override attributes from another service. - - dbh: - class: DBI - method: connect - args: - - 'dbi:mysql:dbname' - dbh_dev: - extends: 'dbh' - args: - - 'dbi:mysql:devdb' - -Hash C will be merged seperately, like so: - - activemq: - class: My::ActiveMQ - args: - host: example.com - port: 61312 - user: root - password: 12345 - activemq_dev: - extends: 'activemq' - args: - host: dev.example.com - -C will get the C, C, and C arguments -from the base service C. - -=head4 with +A dependency injection (DI) container creates an inversion of control: +Instead of manually creating all the dependent objects (also called +"services") before creating the main object that we actually want, a DI +container handles that for us: We describe the relationships between +objects, and the objects get built as needed. -Compose roles into the service object. +Dependency injection is sometimes called the opposite of garbage +collection. Rather than ensure objects are destroyed in the right order, +dependency injection makes sure objects are created in the right order. - app: - class: My::App - with: My::FeatureRole +Using Beam::Wire in your application brings great flexibility, +allowing users to easily add their own code to customize how your +project behaves. - otherapp: - class: My::App - with: - - My::FeatureRole - - My::OtherFeatureRole +For an L. -This lets you break features out into roles, and compose those roles a la carte -on the fly. If you have 20 different optional features, it is difficult to create -every possible combination of them. So, C allows you to pick the features -you want. - -=head4 lifecycle - -Control how your service is created. The default value, C, will cache -the resulting service and return it for every call to C. The other -value, C, will create a new instance of the service every time: - - today: - class: DateTime - method: today - lifecycle: factory - args: - time_zone: US/Chicago - report_yesterday: - class: My::Report - args: - date: { $ref: today, $method: add, $args: [ "days", "-1" ] } - report_today: - class: My::Report - args: - date: { $ref: today } - -Cadd> modifies the object and returns the newly-modified object (to -allow for method chaining.) Without C, the C service -would become yesterday, making it hard to know what C would -report on. - -An C value will be created as soon as the container is created. If you -have an object that registers itself upon instantiation, you can make sure your -object is created as soon as possible by doing C. - -=head4 on - -Attach event listeners using L. - - emitter: - class: My::Emitter - on: - before_my_event: - $ref: listener - $sub: on_before_my_event - my_event: - - $ref: listener - $sub: on_my_event - - $ref: other_listener - $sub: on_my_event - listener: - class: My::Listener - other_listener: - class: My::Listener - -Now, when the C fires off its events, they are dispatched to the -appropriate listeners. - -In order to work around a bug in YAML.pm, you can also specify event listeners -as an array of hashes: - - emitter: - class: My::Emitter - on: - - before_my_event: - $ref: listener - $sub: on_before_my_event - - my_event: - $ref: listener - $sub: on_my_event - - my_event: - $ref: other_listener - $sub: on_my_event - -=head3 Config Services - -A config service allows you to read a config file and use it as a service, giving -all or part of it to other objects in your container. - -To create a config service, use the C key. The value is the path to the -file to read. By default, YAML, JSON, XML, and Perl files are supported (via -L). - - # db_config.yml - dsn: 'dbi:mysql:dbname' - user: 'mysql' - pass: '12345' - - # container.yml - db_config: - config: db_config.yml - -You can pass in the entire config to an object using C<$ref>: - - # container.yml - db_config: - config: db_config.yml - dbobj: - class: My::DB - args: - conf: - $ref: db_config - -If you only need the config file once, you can create an anonymous config -object. - - # container.yml - dbobj: - class: My::DB - args: - conf: - $config: db_config.yml - -The config file can be used as all the arguments to the service: - - # container.yml - dbobj: - class: My::DB - args: - $config: db_config.yml - -In this example, the constructor will be called like: - - my $dbobj = My::DB->new( - dsn => 'dbi:mysql:dbname', - user => 'mysql', - pass => '12345', - ); - -You can reference individual items in a configuration hash using C<$path> -references: - - # container.yml - db_config: - config: db_config.yml - dbh: - class: DBI - method: connect - args: - - $ref: db_config - $path: /dsn - - $ref: db_config - $path: /user - - $ref: db_config - $path: /pass - -B You cannot use C<$path> and anonymous config objects. - - -=head3 Inner Containers - -Beam::Wire objects can hold other Beam::Wire objects! - - inner: - class: Beam::Wire - args: - config: - dbh: - class: DBI - method: connect - args: - - 'dbi:mysql:dbname' - cache: - class: CHI - args: - driver: Memory - max_size: 16MB - -Inner containers' contents can be reached from outer containers by separating -the names with a slash character: - - my $dbh = $wire->get( 'inner/dbh' ); - -=head3 Inner Files - - inner: - class: Beam::Wire - args: - file: inner.yml - -Inner containers can be created by reading files just like the main container. -If the C attribute is relative, the parent's C attribute will be -added: - - # share/parent.yml - inner: - class: Beam::Wire - args: - file: inner.yml - - # share/inner.yml - dbh: - class: DBI - method: connect - args: - - 'dbi:sqlite:data.db' - - # myscript.pl - use Beam::Wire; - - my $container = Beam::Wire->new( - file => 'share/parent.yml', - ); - - my $dbh = $container->get( 'inner/dbh' ); - -If more control is needed, you can set the L on the parent -container. If even more control is needed, you can make a subclass of Beam::Wire. - -=head3 Service/Configuration References +=cut - chi: - class: CHI - args: - driver: 'DBI' - dbh: { $ref: 'dbh' } - dbh: - class: DBI - method: connect - args: - - { $ref: dsn } - - { $ref: usr } - - { $ref: pwd } - dsn: - value: "dbi:SQLite:memory:" - usr: - value: "admin" - pwd: - value: "s3cret" - -The reuse of service and configuration containers as arguments for other -services is encouraged so we have provided a means of referencing those objects -within your configuration. A reference is an arugment (a service argument) in -the form of a hashref with a C<$ref> key whose value is the name of another -service. Optionally, this hashref may contain a C<$path> key whose value is a -L search string which should return the found data -structure from within the referenced service. - -It is also possible to use raw-values as services, this is done by configuring a -service using a single key/value pair with a C key whose value contains -the raw-value you wish to reuse. +use strict; +use warnings; -=cut +use Scalar::Util qw( blessed ); +use Moo; +use Config::Any; +use Module::Runtime qw( use_module ); +use Data::DPath qw ( dpath ); +use Path::Tiny qw( path ); +use File::Basename qw( dirname ); +use Types::Standard qw( :all ); =attr file -The file attribute contains the file path of the file where Beam::Wire container -services are configured (typically a YAML file). The file's contents should form -a single hashref. The keys will become the service names. +The path of the file where services are configured (typically a YAML +file). The file's contents should be a single hashref. The keys are +service names, and the values are L. =cut @@ -495,9 +76,9 @@ has file => ( =attr dir -The dir attribute contains the directory path to use when searching for inner -container files. Defaults to the directory which contains the file specified by -the L. +The directory path to use when searching for inner container files. +Defaults to the directory which contains the file specified by the +L. =cut @@ -516,9 +97,15 @@ has dir => ( =attr config -The config attribute contains a hashref of service configurations. This data is -loaded by L using the file specified by the -L. +The raw configuration data. By default, this data is loaded by +L using the file specified by the L. + +See L. + +If you don't want to load a file, you can specify this attribute in the +Beam::Wire constructor. =cut @@ -537,7 +124,9 @@ sub _build_config { =attr services -A hashref of services. If you have any services already built, add them here. +A hashref of cached services built from the L. If +you want to inject a pre-built object for other services to depend on, +add it here. =cut @@ -559,7 +148,7 @@ sub _build_services { The character that begins a meta-property inside of a service's C. This includes C<$ref>, C<$path>, C<$method>, and etc... -The default value is '$'. The empty string is allowed. +The default value is C<$>. The empty string is allowed. =cut @@ -569,13 +158,17 @@ has meta_prefix => ( default => sub { q{$} }, ); -=method get( name, [ overrides ] ) +=method get -The get method resolves and returns the service named C. + my $service = $wire->get( $name ); + my $service = $wire->get( $name, %overrides ) -C may be a list of name-value pairs. If specified, get() -will create an anonymous service that extends the C service -with the given config overrides: +The get method resolves and returns the service named C<$name>, creating +it, if necessary, with L. + +C<%overrides> is an optional list of name-value pairs. If specified, +get() will create an new, anonymous service that extends the named +service with the given config overrides. For example: # test.pl use Beam::Wire; @@ -588,12 +181,23 @@ with the given config overrides: }, }, ); + my $foo = $wire->get( 'foo', args => { text => 'Hello, Chicago!' } ); print $foo; # prints "Hello, Chicago!" This allows you to create factories out of any service, overriding service configuration at run-time. +If C<$name> contains a slash (C) character (e.g. C), the left +side (C) will be used as the name of an inner container, and the +right side (C) is a service inside that container. For example, +these two lines are equivalent: + + $bar = $wire->get( 'foo/bar' ); + $bar = $wire->get( 'foo' )->get( 'bar' ); + +Inner containers can be nested as deeply as desired (C). + =cut sub get { @@ -625,12 +229,19 @@ sub get { =method set -The set method configures and stores the specified service. + $wire->set( $name => $service ); + +The set method configures and stores the specified C<$service> with the +specified C<$name>. Use this to add or replace built services. + +Like L, C<$name> can contain a slash (C) +character to traverse through nested containers. =cut ## no critic ( ProhibitAmbiguousNames ) -# This was named set() before I started using Perl::Critic +# This was named set() before I started using Perl::Critic, and will +# continue to be named set() now that I no longer use Perl::Critic sub set { my ( $self, $name, $service ) = @_; if ( $name =~ q{/} ) { @@ -643,7 +254,11 @@ sub set { =method get_config -Get the config with the given name, searching inner containers if required + my $conf = $wire->get_config( $name ); + +Get the config with the given C<$name>. Like L, C<$name> can contain slash (C) characters to traverse +through nested containers. =cut @@ -658,78 +273,104 @@ sub get_config { return $self->config->{$name}; } -# TODO: Refactor fix_refs and find_refs into an iterator -sub fix_refs { - my ( $self, $container_name, @args ) = @_; - my @out; - my %meta = $self->get_meta_names; - for my $arg ( @args ) { - if ( ref $arg eq 'HASH' ) { - if ( $self->is_meta( $arg ) ) { - my %new = (); - for my $key ( @meta{qw( ref extends )} ) { - if ( $arg->{$key} ) { - $new{ $key } = join( q{/}, $container_name, $arg->{$key} ); - } - } - push @out, \%new; - } - else { - push @out, { $self->fix_refs( $container_name, %{$arg} ) }; - } - } - elsif ( ref $arg eq 'ARRAY' ) { - push @out, [ map { $self->fix_refs( $container_name, $_ ) } @{$arg} ]; - } - else { - push @out, $arg; # simple scalars - } - } - return @out; -} +=method create_service -sub parse_args { - my ( $self, $for, $class, $args ) = @_; - return if not $args; - my @args; - if ( ref $args eq 'ARRAY' ) { - @args = $self->find_refs( $for, @{$args} ); - } - elsif ( ref $args eq 'HASH' ) { - # Hash args could be a ref - # Subcontainers cannot scan for refs in their configs - if ( $class->isa( 'Beam::Wire' ) ) { - my %args = %{$args}; - my $config = delete $args{config}; - # Relative subcontainer files should be from the current - # container's directory - if ( exists $args{file} && !path( $args{file} )->is_absolute ) { - $args{file} = $self->dir->child( $args{file} ); - } - @args = $self->find_refs( $for, %args ); - if ( $config ) { - push @args, config => $config; - } - } - else { - my ( $maybe_ref ) = $self->find_refs( $for, $args ); - if ( blessed $maybe_ref ) { - @args = ( $maybe_ref ); - } - else { - @args = ref $maybe_ref eq 'HASH' ? %$maybe_ref - : ref $maybe_ref eq 'ARRAY' ? @$maybe_ref - : ( $maybe_ref ); - } - } - } - else { - # Try anyway? - @args = $args; - } + my $service = $wire->create_service( $name, %config ); - return @args; -} +Create the service with the given C<$name> and C<%config>. Config can +contain the following keys: + +=over 4 + +=item class + +The class name of an object to create. Can be combined with C, +and C. + +=item args + +The arguments to the constructor method. Used with C and +C. Can be a simple value, or a reference to an array or +hash which will be dereferenced and passed in to the constructor +as a list. + +=item method + +The method to call to create the object. Only used with C. +Defaults to C<"new">. + +This can also be an array of hashes which describe a list of methods +that will be called on the object. The first method should create the +object, and each subsequent method can be used to modify the object. The +hashes should contain a C key, which is a string containing the +method to call, and optionally C and C keys. The C +key works like the top-level C key, above. The optional C +key can have the special value C<"chain">, which will use the return +value from the method as the value for the service (L). + +If an array is used, the top-level C key is not used. + +=item value + +The value of this service. Can be a simple value, or a reference to an +array or hash. This value will be simply returned by this method, and is +mostly useful when using container files. + +C can not be used with C or C. + +=item config + +The path to a configuration file, relative to L. +The file will be read with L, and the resulting data +structure returned. + +=item extends + +The name of a service to extend. The named service's configuration will +be merged with this configuration (via L). + +This can be used in place of the C key if the extended configuration +contains a class. + +=item with + +Compose a role into the object's class before creating the object. This +can be a single string, or an array reference of strings which are roles +to combine. + +This uses L and L, which should work with any +class (as it uses L under the hood). + +This can be used with the C key. + +=item on + +Attach an event handler to a L. This +is an array of hashes of event names and handlers. A handler is made from +a service reference (C<$ref> or an anonymous service), and a subroutine to +call on that service (C<$sub>). + +For example: + + emitter: + class: My::Emitter + on: + - my_event: + $ref: my_handler + $sub: on_my_event + +This can be used with the C key. + +=back + +This method uses L to parse the C key, +L as needed. + +=cut sub create_service { my ( $self, $name, %service_info ) = @_; @@ -820,6 +461,23 @@ sub create_service { return $service; } +=method merge_config + + my %merged = $wire->merge_config( %config ); + +If C<%config> contains an C key, merge the extended config together +with this one, returning the merged service configuration. This works recursively, +so a service can extend a service that extends another service just fine. + +When merging, hashes are combined, with the child configuration taking +precedence. The C key is handled specially to allow a hash of +args to be merged. + +The configuration returned is a safe copy and can be modified without +effecting the original config. + +=cut + sub merge_config { my ( $self, %service_info ) = @_; if ( $service_info{ extends } ) { @@ -844,6 +502,85 @@ sub merge_config { return %service_info; } +=method parse_args + + my @args = $wire->parse_args( $for_name, $class, $args ); + +Parse the arguments (C<$args>) for the given service (C<$for_name>) with +the given class (C<$class>). + +C<$args> can be an array reference, a hash reference, or a simple +scalar. The arguments will be searched for references using L, and then a list of arguments will be +returned, ready to pass to the object's constructor. + +Nested containers are handled specially by this method: Their inner +references are not resolved by the parent container. This ensures that +references are always relative to the container they're in. + +=cut + +sub parse_args { + my ( $self, $for, $class, $args ) = @_; + return if not $args; + my @args; + if ( ref $args eq 'ARRAY' ) { + @args = $self->find_refs( $for, @{$args} ); + } + elsif ( ref $args eq 'HASH' ) { + # Hash args could be a ref + # Subcontainers cannot scan for refs in their configs + if ( $class->isa( 'Beam::Wire' ) ) { + my %args = %{$args}; + my $config = delete $args{config}; + # Relative subcontainer files should be from the current + # container's directory + if ( exists $args{file} && !path( $args{file} )->is_absolute ) { + $args{file} = $self->dir->child( $args{file} ); + } + @args = $self->find_refs( $for, %args ); + if ( $config ) { + push @args, config => $config; + } + } + else { + my ( $maybe_ref ) = $self->find_refs( $for, $args ); + if ( blessed $maybe_ref ) { + @args = ( $maybe_ref ); + } + else { + @args = ref $maybe_ref eq 'HASH' ? %$maybe_ref + : ref $maybe_ref eq 'ARRAY' ? @$maybe_ref + : ( $maybe_ref ); + } + } + } + else { + # Try anyway? + @args = $args; + } + + return @args; +} + +=method find_refs + + my @resolved = $wire->find_refs( $for_name, @args ); + +Go through the C<@args> and recursively resolve any references and +services found inside, returning the resolved result. References are +identified with L. + +If a reference contains a C<$ref> key, it will be resolved by L. Otherwise, the reference will be +treated as an anonymous service, and passed directly to L. + +This is used when L to ensure all +dependencies are created first. + +=cut + sub find_refs { my ( $self, $for, @args ) = @_; my @out; @@ -879,6 +616,20 @@ sub find_refs { return @out; } +=method is_meta + + my $is_meta = $wire->is_meta( $ref_hash ); + +Returns true if the given hash reference describes some kind of +Beam::Wire service. This is used to identify service configuration +hashes inside of larger data structures. + +A service hash reference must contain at least one key, and must not +contain any keys that are not meta-keys (as returned by L). + +=cut + sub is_meta { my ( $self, $arg ) = @_; my $prefix = $self->meta_prefix; @@ -886,6 +637,15 @@ sub is_meta { return @keys && !grep { !/^\Q$prefix/ } @keys; } +=method get_meta_names + + my %meta_keys = $wire->get_meta_names; + +Get all the possible service keys with the L already +attached. + +=cut + sub get_meta_names { my ( $self ) = @_; my $prefix = $self->meta_prefix; @@ -902,6 +662,65 @@ sub get_meta_names { return wantarray ? %meta : \%meta; } +=method resolve_ref + + my @value = $wire->resolve_ref( $for_name, $ref_hash ); + +Resolves the given dependency from the configuration hash (C<$ref_hash>) +for the named service (C<$for_name>). Reference hashes contain the +following keys: + +=over 4 + +=item $ref + +The name of a service in the container. Required. + +=item $path + +A data path to pick some data out of the reference. Useful with C +and C services. + + # container.yml + bounties: + value: + malcolm: 50000 + zoe: 35000 + simon: 100000 + + captain: + class: Person + args: + name: Malcolm Reynolds + bounty: + $ref: bounties + $path: /malcolm + +=item $call + +Call a method on the referenced object and use the resulting value. This +may be a string, which will be the method name to call, or a hash with +C<$method> and C<$args>, which are the method name to call and the +arguments to that method, respectively. + + captain: + class: Person + args: + name: Malcolm Reynolds + location: + $ref: beacon + $call: get_location + bounty: + $ref: news + $call: + $method: get_bounty + $args: + name: mreynolds + +=back + +=cut + sub resolve_ref { my ( $self, $for, $arg ) = @_; @@ -947,9 +766,55 @@ sub resolve_ref { return @ref; } +=method fix_refs + + my @fixed = $wire->fix_refs( $for_name, @args ); + +Similar to L. This method searches +through the C<@args> and recursively fixes any reference paths to be +absolute. References are identified with L. + +This is used by L to ensure that the +configuration can be passed directly in to L. + +=cut + +sub fix_refs { + my ( $self, $container_name, @args ) = @_; + my @out; + my %meta = $self->get_meta_names; + for my $arg ( @args ) { + if ( ref $arg eq 'HASH' ) { + if ( $self->is_meta( $arg ) ) { + my %new = (); + for my $key ( @meta{qw( ref extends )} ) { + if ( $arg->{$key} ) { + $new{ $key } = join( q{/}, $container_name, $arg->{$key} ); + } + } + push @out, \%new; + } + else { + push @out, { $self->fix_refs( $container_name, %{$arg} ) }; + } + } + elsif ( ref $arg eq 'ARRAY' ) { + push @out, [ map { $self->fix_refs( $container_name, $_ ) } @{$arg} ]; + } + else { + push @out, $arg; # simple scalars + } + } + return @out; +} + =method new + my $wire = Beam::Wire->new( %attributes ); + Create a new container. =cut diff --git a/lib/Beam/Wire/Help/Config.pod b/lib/Beam/Wire/Help/Config.pod new file mode 100644 index 0000000..2eae4a1 --- /dev/null +++ b/lib/Beam/Wire/Help/Config.pod @@ -0,0 +1,755 @@ +# ABSTRACT: A brief introduction to dependency injection with Beam::Wire +# PODNAME: Beam::Wire::Help::Config + +=head1 DESCRIPTION + +This is tutorial for Beam::Wire, starting with simple use as a configuration +file, to complex dependency injection. + +This tutorial will guide you through the YAML configuration, its equivalent +Perl data structure, and the equivalent Perl code that is executed. + +=head1 OBJECT CONFIGURATION + +The basic Beam::Wire configuration is a hash of hashes describing how to create +objects (which, in a dependency injection context, are called "services"). The +top-level keys are the name of the object, and the inner keys are object +configuration. To configure an object, you need the class and, optionally, +constructor arguments (C). + + # container.yml + malcolm: + class: Person + args: + name: Malcolm Reynolds + rank: Captain + + # container.pl + my $config = { + malcolm => { + class => 'Person', + args => { + name => 'Malcolm Reynolds', + rank => 'Captain', + }, + }, + }; + +Once we have a configuration file (also called a "container file"), we can give +it to Beam::Wire and get our objects ("services"). + + my $wire = Beam::Wire->new( file => 'container.yml' ); + my $malcolm = $wire->get( 'malcolm' ); + +You can also configure objects directly in Beam::Wire. + + my $wire = Beam::Wire->new( config => $config ); # $config from above + my $malcolm = $wire->get( 'malcolm' ); + +The configuration will be used by Beam::Wire to create your object, similar to +running this code: + + my $malcolm = Person->new( + name => 'Malcolm Reynolds', + rank => 'Captain', + ); + +=head2 Specifying Constructor Args + +Objects have varying ways of specifying arguments to their constructors. +The most common method, used by most Perl object frameworks, of +specifying name/value pairs is the easiest: + + # container.yml + malcolm: + class: Person + args: + name: Malcolm Reynolds + rank: Captain + + # container.pl + my $config = { + malcolm => { + class => 'Person', + args => { + name => 'Malcolm Reynolds', + rank => 'Captain', + }, + }, + }; + +For any other kind of constructor arguments, you can specify an arbitrary +array. If the object's constructor is not called C, you can use the +C key: + + # container.yml + dbh: + class: DBI + method: connect + args: + - 'dbi:SQLite:firefly.db' + - ~ + - ~ + - RaiseError: 1 + + # container.pl + my $config = { + sqlite => { + class => 'DBI', + method => 'connect', + args => [ + 'dbi:SQLite:firefly.db', + undef, + undef, + { RaiseError => 1 }, + ], + }, + }; + +This is the same as: + + my $dbh = DBI->connect( + 'dbi:SQLite:firefly.db', + undef, + undef, + { RaiseError => 1 }, + ); + +If you need a single hash reference of arguments, you can use an array +with a single element, like this: + + # container.yml + wash: + class: Person + args: + - name: "Hoban Washburne" + rank: Pilot + + # container.pl + my $config = { + wash => { + class => 'Person', + args => [ + { + name => 'Hoban Washburne', + rank => 'Pilot', + }, + ], + }, + }; + +Which is the same as: + + my $wash = Person->new( { + name => 'Hoban Washburne', + rank => 'Pilot', + } ); + +=head1 OBJECT LIFECYCLE + +By default, services are lazy and cached. They are not created until +they are asked for (lazy), and once created, they are reused if asked +for again (cached). + +=head2 factory + +By default, all objects are cached in the container, so asking for the +same object twice will get the B same object. To prevent this +caching, you can force the container to make a new object every time by +setting the C to C. Objects from a factory are not +cached. For example: + + # container.yml + light_drone: + class: Drone + lifecycle: factory + args: + model: Light + cost: 20 + + # container.pl + my $config = { + light_drone => { + class => 'Drone', + lifecycle => 'factory', + args => { + model => 'Light', + cost => 20, + }, + }, + }; + +This is basically the same as creating a sub to create our objects, +like so: + + my $light_drone_factory = sub { + return Drone->new( + model => 'Light', + cost => 20, + ); + }; + +We can then pull infinite numbers of separate drones out of our factory: + + my $wire = Beam::Wire->new( file => 'container.yml' ); + my $light_drone = $wire->get( 'light_drone' ); + my $replacement = $wire->get( 'light_drone' ); + my $other_drone = $wire->get( 'light_drone' ); + +=head2 eager + +Some special kinds of objects have global effects that happen when +they are created, like a global logging system (like L). + +To force an object to be created as soon as possible, you can set +C to C. + + # container.yml + black_box: + class: Logger + lifecycle: eager + args: + log_level: warn + + # container.pl + my $config = { + black_box => { + class => 'Logger', + lifecycle => 'eager', + args => { + log_level => 'warn', + }, + }, + }; + +Once the container has been read, all of the eager objects will be +created, and cached as normal. + + my $wire = Beam::Wire->new( file => 'container.yml' ); + # black_box is created automatically + +=head1 DEPENDENCY INJECTION + +The key feature of a dependency injection container is the ability to +inject dependencies into the services as they are created. Dependencies +are other services (objects) that must be created and passed-in to our +current object. + +Unlike above, where we were giving simple arguments to our constructors, +with dependency injection, we can give other objects as arguments. + +=head2 References ($ref) + +References allow us to refer to another object in our container. If +needed, the object is constructed for us, so that when we ask for an +object, the objects it depends are created automatically. + +To refer to another object, use C<$ref>: + + # container.yml + serenity: + class: Ship + args: + captain: + $ref: malcolm + pilot: + $ref: wash + engineer: + $ref: kaylee + + # container.pl + my $config = { + serenity => { + class => 'Ship', + args => { + captain => { '$ref' => 'malcolm' }, + pilot => { '$ref' => 'wash' }, + engineer => { '$ref' => 'kaylee' }, + }, + }, + }; + +This is equivalent to: + + my $malcolm = Person->new( ... ); + my $wash = Person->new( ... ); + my $kaylee = Person->new( ... ); + my $serenity = Ship->new( + captain => $malcolm, + pilot => $wash, + engineer => $kaylee, + ); + +Remember that, by default, all the objects are cached, so another +reference to C gets the same shuĂ i space captain. If +that's not desired, you can use L config|/OBJECT LIFECYCLE>. + +=head2 Anonymous Objects + +Instead of having to create a named service, you can create a new, +anonymous object as a dependency. This is useful when you want to keep +related objects together in the configuration file. + +You can create an anonymous object anywhere you could create a reference +(C<$ref>). To create an anonymous object, use C<$class> and optionally +C<$args> and C<$method>. + + # container.yml + cargo: + class: Box + args: + contents: + $class: Person + $args: + name: River Tam + status: Hibernating + + # container.pl + my $config = { + cargo => { + class => 'Box', + args => { + contents => { + '$class' => 'Person', + '$args' => { + name => 'River Tam', + status => 'Hibernating', + }, + }, + }, + }, + }; + +This is equivalent to: + + my $cargo = Box->new( + contents => Person->new( + name => 'River Tam', + status => 'Hibernating', + ), + ); + +=head1 OBJECT COMPOSITION + +One of the benefits of using Beam::Wire to define your configuration is +being able to intelligently compose your objects to reduce duplication +and prevent messy copy/paste jobs. + +=head2 extends + +If you have a bunch of objects that need to share properties, or that +only differ in one or two things, you can inherit properties using +C: + + # container.yml + serenity_crew: + class: Person + args: + ship: Serenity + model: Firefly + kaylee: + extends: serenity_crew + args: + name: Kaylee Frye + rank: Engineer + + # container.pl + my $config = { + serenity_crew => { + class => 'Person', + args => { + ship => 'Serenity', + model => 'Firefly', + }, + }, + kaylee => { + extends => 'serenity_crew', + args => { + name => 'Kaylee Frye', + rank => 'Engineer', + }, + }, + }; + +Which ends up composing our object as: + + my $kaylee = Person->new( + ship => 'Serenity', # from "serenity_crew" + model => 'Firefly', # from "serenity_crew" + name => 'Kaylee Frye', # from "kaylee" + rank => 'Engineer', # from "kaylee" + ); + +This allows us to quickly change any object config that extends the +parent object config (say, to update their C to C). + +=head1 NON-OBJECT SERVICES + +Not everything in our container needs to be an object. Some services may +need to share simple configuration values (such as usernames and +passwords) or even entire configuration files. + +=head2 Value Services + +Instead of creating an object, we can create simple values like strings, +numbers, arrays, and hashes using the C key: + + # container.yml + bounty: + value: 100000 + itinerary: + value: + - Heaven + - Highgate + - Muir + - Miranda + + # container.pl + my $config = { + bounty => { + value => 100000, + }, + itinerary => { + value => [ + 'Heaven', + 'Highgate', + 'Muir', + 'Miranda', + ], + }, + }; + +These services can be used like any other. You can get the value with +the C method: + + my $itinerary = $wire->get( 'itinerary' ); + +And you can set up relationships with C<$ref>: + + # container.yml + serenity_crew: + class: Person + args: + bounty: + $ref: bounty + +=head2 Config Services + +A config service allows you to read a config file and use it as +a service, giving all or part of it to other objects in your container. + +To create a config service, use the C key. The value is the path +to the file to read. By default, YAML, JSON, XML, and Perl files are +supported (via L). + +This works very much like a C service (above). The configuration +file is read, and the data inside is the result. + + # manifest.yml + - 12 pair socks + - 5 shirts, black + - 5 shirts, slightly darker black + - 1 strawberry + + # container.yml + manifest: + config: manifest.yml + + # container.pl + my $config = { + manifest => { + config => 'manifest.yml', + }, + }; + +These services can be used like any other. You can get the value with +the C method: + + my $manifest = $wire->get( 'manifest' ); + +And you can set up relationships with C<$ref>: + + # container.yml + serenity: + class: Ship + args: + cargo: + $ref: manifest + +If you only need the config file once, you can create an anonymous config +object. + + # container.yml + serenity: + class: Ship + args: + cargo: + $config: manifest.yml + +=head1 ADVANCED FEATURES + +=head2 Nested Containers + +Nested containers can be created by adding Beam::Wire objects to +a Beam::Wire container. This can be useful for sharing common objects +(logging, database, or others) between multiple containers, or combining +multiple containers into one. + + # actors.yml + malcolm: + class: Actor + args: + name: Nathan Fillion + zoe: + class: Actor + args: + name: Gina Torres + + # container.yml + actors: + class: Beam::Wire + args: + file: actors.yml + + # script.pl + my $wire = Beam::Wire->new( file => 'container.yml' ); + my $actor = $wire->get( 'actors/malcolm' ); + +Nested container file paths are relative to the current container file +by default. If needed, you can set the L +to change what directory to search in. + +=head2 Event Handlers (on) + +If your objects use L, you +can attach events to your object using the C key. This ensures that +when your object is created, all of its event handlers are also created. + +The C key should be an array of hashes. The hash key is the name of +the event. The hash value should be a reference (C<$ref>) or an +anonymous object (C<$class>), and must include a subroutine to call on +that service using the C<$sub> key. + + # container.yml + serenity: + class: Ship + on: + - compressor_alert: + $ref: ignore + $sub: ignore_alert + - airlock_open: + $class: Klaxon + $args: + volume: loud + $sub: alert + +If you're not using YAML, you can organize event handlers as a simple +hash, or a hash of arrays if you need multiple handlers for the same +event: + + # container.pl + my $config = { + serenity => { + class => 'Ship', + on => { + compressor_alert => { + '$ref' => 'ignore', + '$sub' => 'ignore_alert', + }, + airlock_open => { + '$class' => 'Klaxon', + '$args' => { + volume => 'loud', + }, + '$sub' => 'alert', + }, + }, + }, + }; + +=head2 Compose Roles (with) + +Sometimes we have an object, but we also want to add a role to it. +Instead of having to create a new, concrete class to compose every +possible combination of roles, we can instead compose those roles when +creating the object with the C key. + +C can be a single string, which is a role class to compose, or an +array of strings to compose multiple roles. + + # container.yml + shepherd: + class: Person + with: DarkPast + + # container.pl + my $config = { + shepherd => { + class => 'Person', + with => 'DarkPast', + }, + }; + +Then, when the C object is created, a new, anonymous class is +created that extends the C class and adds the C role. + +=head2 Multiple Constructor Methods + +Sometimes an object can't be constructed with just a single method. We +may have to call some methods to set attributes that are puzzlingly not +exposed in the constructor, or we may want to immediately try to connect +to a service. + +To call multiple methods during construction, we can pass an array to +the C key. Each member of the array should be a hash containing +another C key, which will be the method to call, and optionally +an C key, which will be the arguments to that specific method. + +The first constructor method must construct the object itself. Each +other method will be called on the object, and then the object will be +used as the service. + + # container.yml + malcolm: + class: Person + method: + - method: new + args: + name: 'Malcolm Reynolds' + - method: set_bounty + args: + - 100000 + - method: set_rank + args: + - Captain + + # container.pl + my $config = { + malcolm => { + class => 'Person', + method => [ + { + method => 'new', + args => { + name => 'Malcolm Reynolds', + }, + }, + { + method => 'set_bounty', + args => [ 100000 ], + }, + { + method => 'set_rank', + args => [ 'Captain' ], + }, + ], + }, + }; + +This is equivalent to doing: + + my $malcolm = Person->new( name => 'Malcolm Reynolds' ); + $malcolm->set_bounty( 100000 ); + $malcolm->set_rank( 'Captain' ); + return $malcolm; + +It's not a commonly-needed feature, but it exists just in case. Instead +of doing this, you may be better off wrapping the class that requires +this in your own class which provides a saner construction API. You +could then release this wrapper class to CPAN in the C +namespace). + +=head2 Chained Constructor Methods + +Chained constructor methods work the same as multiple constructor +methods, except the result of the first method is used as the invocant +of the second method, and the result of the second method is used as the +invocant of the third method. + +To chain a method to its following method, add C to the +hash of method attributes. The last instance of C will be +the return value used for the service. + + # container.yml + malcolm: + class: Person + method: + - method: new + args: + name: 'Malcolm Reynolds' + return: chain + - method: set_bounty + args: + - 100000 + return: chain + - method: set_rank + args: + - Captain + return: chain + + # container.pl + my $config = { + malcolm => { + class => 'Person', + method => [ + { + method => 'new', + args => { + name => 'Malcolm Reynolds', + }, + }, + { + method => 'set_bounty', + args => [ 100000 ], + }, + { + method => 'set_rank', + args => [ 'Captain' ], + }, + ], + }, + }; + +This is equivalent to doing: + + my $malcolm = Person->new( name => 'Malcolm Reynolds' ); + $malcolm = $malcolm->set_bounty( 100000 ); + $malcolm = $malcolm->set_rank( 'Captain' ); + return $malcolm; + +This is useful if you need to connect to a database, and then get +a specific object for a table (DBIx::Class) or collection (MongoDB). + +=head2 Data Paths + +You can reference individual items in a C or C service +using C<$path> references. This uses L to match parts of the data structure. This is +a powerful tool that can be used to create automatic filters on data +structures, even executing Perl code to find items to return. + + # container.yml + bounties: + value: + malcolm: 50000 + zoe: 35000 + simon: 100000 + + captain: + class: Person + args: + name: Malcolm Reynolds + bounty: + $ref: bounties + $path: /malcolm + +B You cannot use C<$path> and anonymous config objects. + +=head1 SEE ALSO + +=over 4 + +=item L + +=back +