From fbb10556b74527bd26009de26404e5d055745a4b Mon Sep 17 00:00:00 2001 From: Doug Bell Date: Sat, 27 Feb 2016 23:44:05 -0600 Subject: [PATCH] release v1.017 - add bare services - rewrite and expand the documentation --- CHANGES | 6 +- Makefile.PL | 2 +- README | 703 +++++++++++++++++++++---------------------------- README.mkdn | 739 ++++++++++++++++++++++------------------------------ 4 files changed, 606 insertions(+), 844 deletions(-) diff --git a/CHANGES b/CHANGES index bc9c57f..4c5e421 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,10 @@ Changelog for Beam-Wire -1.016 2016-02-24T23:37:58 +1.017 2016-02-28T05:43:50 + - add bare services + - rewrite and expand the documentation + +1.016 2016-02-24T23:37:58Z - Compose Roles _before_ instantiation so attrs will work in init 1.015 2016-02-23T14:33:00Z diff --git a/Makefile.PL b/Makefile.PL index 806a7ab..b8d630e 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -40,7 +40,7 @@ my %WriteMakefileArgs = ( "Test::Lib" => 0, "Test::More" => "1.001005" }, - "VERSION" => "1.016", + "VERSION" => "1.017", "test" => { "TESTS" => "t/*.t t/container/*.t t/dependency/*.t t/deprecated/*.t t/service/*.t" } diff --git a/README b/README index 541abf6..c579d43 100644 --- a/README +++ b/README @@ -1,526 +1,403 @@ =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 + first_officer: + $class: Person + name: Zoë Alleyne Washburne + rank: Commander + + # 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). +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. -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. +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. -=head1 OVERVIEW +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. -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. +Using Beam::Wire in your application brings great flexibility, +allowing users to easily add their own code to customize how your +project behaves. -=head2 WHAT IS A DEPENDENCY? +For an L. -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. +=attr file -=head2 WHAT IS A SERVICE? +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. -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. +=attr dir -=head2 HOW ARE SERVICES CONFIGURED? +The directory path to use when searching for inner container files. +Defaults to the directory which contains the file specified by the +L. - # databases.yml +=attr config - 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' } +The raw configuration data. By default, this data is loaded by +L using the file specified by the L. -=head3 Service Attributes +See L. -=head4 class +If you don't want to load a file, you can specify this attribute in the +Beam::Wire constructor. -The class to instantiate. The class will be loaded and the C (below) -method called. +=attr services -=head4 method +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. -The class method to call to construct the object. Defaults to C. +=attr meta_prefix -If multiple methods are needed to initialize an object, C can be an -arrayref of hashrefs, like so: +The character that begins a meta-property inside of a service's C. This +includes C<$ref>, C<$path>, C<$method>, and etc... - my_service: - class: My::Service - method: - - method: new - args: - foo: bar - - method: set_baz - args: - - Fizz +The default value is C<$>. The empty string is allowed. -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. +=method get -To chain methods together, add C: + my $service = $wire->get( $name ); + my $service = $wire->get( $name, %overrides ) - 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 +The get method resolves and returns the service named C<$name>, creating +it, if necessary, with L. -This example is equivalent to the following code: +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: - my $service = My::Service->new( foo => "bar" )->set_baz( "Fizz" ) - ->set_buzz( "Bork" ); + # test.pl + use Beam::Wire; + my $wire = Beam::Wire->new( + config => { + foo => { + args => { + text => 'Hello, World!', + }, + }, + }, + ); -=head4 args + my $foo = $wire->get( 'foo', args => { text => 'Hello, Chicago!' } ); + print $foo; # prints "Hello, Chicago!" -The arguments to the C method. This can be either an array or a hash, -like so: +This allows you to create factories out of any service, overriding service +configuration at run-time. - # array - dbh: - class: DBI - method: connect - args: - - 'dbi:mysql:dbname' +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: - # hash - cache: - class: CHI - args: - driver: Memory - max_size: 16MB + $bar = $wire->get( 'foo/bar' ); + $bar = $wire->get( 'foo' )->get( 'bar' ); -Using the array of arguments, you can give arrayrefs or hashrefs: +Inner containers can be nested as deeply as desired (C). - # 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 +=method set -=head4 extends + $wire->set( $name => $service ); -Inherit and override attributes from another service. +The set method configures and stores the specified C<$service> with the +specified C<$name>. Use this to add or replace built services. - dbh: - class: DBI - method: connect - args: - - 'dbi:mysql:dbname' - dbh_dev: - extends: 'dbh' - args: - - 'dbi:mysql:devdb' +Like L, C<$name> can contain a slash (C) +character to traverse through nested containers. -Hash C will be merged seperately, like so: +=method get_config - activemq: - class: My::ActiveMQ - args: - host: example.com - port: 61312 - user: root - password: 12345 - activemq_dev: - extends: 'activemq' + 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. + +=method normalize_config + + my $out_conf = $self->normalize_config( $in_conf ); + +Normalize the given C<$in_conf> into to hash that L expects. This method allows a service to be +defined with prefixed meta-names (C<$class> instead of C) and +the arguments specified without prefixes. + +For example, these two services are identical. + + foo: + class: Foo args: - host: dev.example.com + fizz: buzz -C will get the C, C, and C arguments -from the base service C. + foo: + $class: Foo + fizz: buzz -=head4 with +The C<$in_conf> must be a hash, and must already pass L. -Compose roles into the service object. +=method create_service - app: - class: My::App - with: My::FeatureRole + my $service = $wire->create_service( $name, %config ); - otherapp: - class: My::App - with: - - My::FeatureRole - - My::OtherFeatureRole +Create the service with the given C<$name> and C<%config>. Config can +contain the following keys: -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. +=over 4 -=head4 lifecycle +=item class -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: +The class name of an object to create. Can be combined with C, +and C. - 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 } +=item args -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. +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. -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. +=item method -=head4 on +The method to call to create the object. Only used with C. +Defaults to C<"new">. -Attach event listeners using L. +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). - 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: +If an array is used, the top-level C key is not used. - 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 +=item value -=head3 Config Services +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. -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. +C can not be used with C or C. -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). +=item config - # db_config.yml - dsn: 'dbi:mysql:dbname' - user: 'mysql' - pass: '12345' +The path to a configuration file, relative to L. +The file will be read with L, and the resulting data +structure returned. - # container.yml - db_config: - config: db_config.yml +=item extends -You can pass in the entire config to an object using C<$ref>: +The name of a service to extend. The named service's configuration will +be merged with this configuration (via L). - # container.yml - db_config: - config: db_config.yml - dbobj: - class: My::DB - args: - conf: - $ref: db_config +This can be used in place of the C key if the extended configuration +contains a class. -If you only need the config file once, you can create an anonymous config -object. +=item with - # container.yml - dbobj: - class: My::DB - args: - conf: - $config: db_config.yml +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. -The config file can be used as all the arguments to the service: +This uses L and L, which should work with any +class (as it uses L under the hood). - # container.yml - dbobj: - class: My::DB - args: - $config: db_config.yml +This can be used with the C key. -In this example, the constructor will be called like: +=item on - my $dbobj = My::DB->new( - dsn => 'dbi:mysql:dbname', - user => 'mysql', - pass => '12345', - ); +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>). -You can reference individual items in a configuration hash using C<$path> -references: +For example: - # 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 + emitter: + class: My::Emitter + on: + - my_event: + $ref: my_handler + $sub: on_my_event -B You cannot use C<$path> and anonymous config objects. +This can be used with the C key. +=back -=head3 Inner Containers +This method uses L to parse the C key, +L as needed. -Beam::Wire objects can hold other Beam::Wire objects! +=method merge_config - 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 + my %merged = $wire->merge_config( %config ); -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: +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. - # share/parent.yml - inner: - class: Beam::Wire - args: - file: inner.yml +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. - # share/inner.yml - dbh: - class: DBI - method: connect - args: - - 'dbi:sqlite:data.db' +The configuration returned is a safe copy and can be modified without +effecting the original config. - # myscript.pl - use Beam::Wire; +=method parse_args - my $container = Beam::Wire->new( - file => 'share/parent.yml', - ); + my @args = $wire->parse_args( $for_name, $class, $args ); - my $dbh = $container->get( 'inner/dbh' ); +Parse the arguments (C<$args>) for the given service (C<$for_name>) with +the given class (C<$class>). -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. +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. -=head3 Service/Configuration References +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. - 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. +=method find_refs -=attr file + my @resolved = $wire->find_refs( $for_name, @args ); -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. +Go through the C<@args> and recursively resolve any references and +services found inside, returning the resolved result. References are +identified with L. -=attr dir +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. -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. +This is used when L to ensure all +dependencies are created first. -=attr config +=method is_meta -The config attribute contains a hashref of service configurations. This data is -loaded by L using the file specified by the -L. + my $is_meta = $wire->is_meta( $ref_hash ); -=attr services +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 hashref of services. If you have any services already built, add them here. +A service hash reference must contain at least one key, and must either +be made completely of meta keys (as returned by L), or contain a L key that +could create or reference an object (one of C, C, +C, C, or C); -=attr meta_prefix +=method get_meta_names -The character that begins a meta-property inside of a service's C. This -includes C<$ref>, C<$path>, C<$method>, and etc... + my %meta_keys = $wire->get_meta_names; -The default value is '$'. The empty string is allowed. +Get all the possible service keys with the L already +attached. -=method get( name, [ overrides ] ) +=method resolve_ref -The get method resolves and returns the service named C. + my @value = $wire->resolve_ref( $for_name, $ref_hash ); -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: +Resolves the given dependency from the configuration hash (C<$ref_hash>) +for the named service (C<$for_name>). Reference hashes contain the +following keys: - # test.pl - use Beam::Wire; - my $wire = Beam::Wire->new( - config => { - foo => { - args => { - text => 'Hello, World!', - }, - }, - }, - ); - my $foo = $wire->get( 'foo', args => { text => 'Hello, Chicago!' } ); - print $foo; # prints "Hello, Chicago!" +=over 4 -This allows you to create factories out of any service, overriding service -configuration at run-time. +=item $ref -=method set +The name of a service in the container. Required. -The set method configures and stores the specified service. +=item $path -=method get_config +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 -Get the config with the given name, searching inner containers if required +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 + +=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. =method new + my $wire = Beam::Wire->new( %attributes ); + Create a new container. =head1 EXCEPTIONS diff --git a/README.mkdn b/README.mkdn index 9f50f90..db61518 100644 --- a/README.mkdn +++ b/README.mkdn @@ -4,7 +4,7 @@ Beam::Wire - Lightweight Dependency Injection Container # VERSION -version 1.016 +version 1.017 # STATUS @@ -13,529 +13,398 @@ version 1.016 # SYNOPSIS # wire.yml - - dbh: - class: 'DBI' - method: connect + captain: + class: Person args: - - 'dbi:mysql:dbname' - - { - PrintError: 1 - } - - # myscript.pl - + name: Malcolm Reynolds + rank: Captain + first_officer: + $class: Person + name: Zoë Alleyne Washburne + rank: Commander + + # 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" # 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). +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. -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. +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. -# OVERVIEW +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. -Beam::Wire loads a configuration [file](https://metacpan.org/pod/file) and stores the specified configuration -in the [config](https://metacpan.org/pod/config attribute) 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. +Using Beam::Wire in your application brings great flexibility, +allowing users to easily add their own code to customize how your +project behaves. -## WHAT IS A DEPENDENCY? +For an [introduction to the Beam::Wire service configuration format, +see Beam::Wire::Help::Config](https://metacpan.org/pod/Beam::Wire::Help::Config). -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. +# ATTRIBUTES -## WHAT IS A SERVICE? +## file -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. +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 [service +configurations](https://metacpan.org/pod/Beam::Wire::Help::Config). -## HOW ARE SERVICES CONFIGURED? +## dir - # databases.yml +The directory path to use when searching for inner container files. +Defaults to the directory which contains the file specified by the +[file attribute](#file). - 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' } +## config -### Service Attributes +The raw configuration data. By default, this data is loaded by +[Config::Any](https://metacpan.org/pod/Config::Any) using the file specified by the [/file](https://metacpan.org/pod/file +attribute). -#### class +See [Beam::Wire::Help::Config for details on what the configuration +data structure looks like](https://metacpan.org/pod/Beam::Wire::Help::Config). -The class to instantiate. The class will be loaded and the `method` (below) -method called. +If you don't want to load a file, you can specify this attribute in the +Beam::Wire constructor. -#### method +## services -The class method to call to construct the object. Defaults to `new`. +A hashref of cached services built from the [configuration](#config). If +you want to inject a pre-built object for other services to depend on, +add it here. -If multiple methods are needed to initialize an object, `method` can be an -arrayref of hashrefs, like so: +## meta\_prefix - my_service: - class: My::Service - method: - - method: new - args: - foo: bar - - method: set_baz - args: - - Fizz +The character that begins a meta-property inside of a service's `args`. This +includes `$ref`, `$path`, `$method`, and etc... -In this example, first we call `My::Service-`new( foo => "bar" );> to get our -object, then we call `$obj-`set\_baz( "Fizz" );> as a further initialization -step. +The default value is `$`. The empty string is allowed. -To chain methods together, add `return: chain`: +# METHODS - 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 +## get -This example is equivalent to the following code: + my $service = $wire->get( $name ); + my $service = $wire->get( $name, %overrides ) - my $service = My::Service->new( foo => "bar" )->set_baz( "Fizz" ) - ->set_buzz( "Bork" ); +The get method resolves and returns the service named `$name`, creating +it, if necessary, with [the create\_service method](#create_service). -#### args +`%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: -The arguments to the `method` method. This can be either an array or a hash, -like so: + # test.pl + use Beam::Wire; + my $wire = Beam::Wire->new( + config => { + foo => { + args => { + text => 'Hello, World!', + }, + }, + }, + ); - # array - dbh: - class: DBI - method: connect - args: - - 'dbi:mysql:dbname' + my $foo = $wire->get( 'foo', args => { text => 'Hello, Chicago!' } ); + print $foo; # prints "Hello, Chicago!" - # hash - cache: - class: CHI - args: - driver: Memory - max_size: 16MB +This allows you to create factories out of any service, overriding service +configuration at run-time. -Using the array of arguments, you can give arrayrefs or hashrefs: +If `$name` contains a slash (`/`) character (e.g. `foo/bar`), the left +side (`foo`) will be used as the name of an inner container, and the +right side (`bar`) is a service inside that container. For example, +these two lines are equivalent: - # 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 + $bar = $wire->get( 'foo/bar' ); + $bar = $wire->get( 'foo' )->get( 'bar' ); -#### extends +Inner containers can be nested as deeply as desired (`foo/bar/baz/fuzz`). -Inherit and override attributes from another service. +## set - dbh: - class: DBI - method: connect - args: - - 'dbi:mysql:dbname' - dbh_dev: - extends: 'dbh' - args: - - 'dbi:mysql:devdb' + $wire->set( $name => $service ); -Hash `args` will be merged seperately, like so: +The set method configures and stores the specified `$service` with the +specified `$name`. Use this to add or replace built services. - activemq: - class: My::ActiveMQ - args: - host: example.com - port: 61312 - user: root - password: 12345 - activemq_dev: - extends: 'activemq' +Like [the get() method, above](#get), `$name` can contain a slash (`/`) +character to traverse through nested containers. + +## get\_config + + my $conf = $wire->get_config( $name ); + +Get the config with the given `$name`. Like [the get() method, +above](#get), `$name` can contain slash (`/`) characters to traverse +through nested containers. + +## normalize\_config + + my $out_conf = $self->normalize_config( $in_conf ); + +Normalize the given `$in_conf` into to hash that [the create\_service +method](#create_service) expects. This method allows a service to be +defined with prefixed meta-names (`$class` instead of `class`) and +the arguments specified without prefixes. + +For example, these two services are identical. + + foo: + class: Foo args: - host: dev.example.com + fizz: buzz -`activemq_dev` will get the `port`, `user`, and `password` arguments -from the base service `activemq`. + foo: + $class: Foo + fizz: buzz -#### with +The `$in_conf` must be a hash, and must already pass [an is\_meta +check](#is_meta). -Compose roles into the service object. +## create\_service - app: - class: My::App - with: My::FeatureRole + my $service = $wire->create_service( $name, %config ); - otherapp: - class: My::App - with: - - My::FeatureRole - - My::OtherFeatureRole +Create the service with the given `$name` and `%config`. Config can +contain the following keys: -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, `with` allows you to pick the features -you want. +- class -#### lifecycle + The class name of an object to create. Can be combined with `method`, + and `args`. -Control how your service is created. The default value, `singleton`, will cache -the resulting service and return it for every call to `get()`. The other -value, `factory`, will create a new instance of the service every time: +- args - 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 } - -`DateTime-`add> modifies the object and returns the newly-modified object (to -allow for method chaining.) Without `lifecycle: factory`, the `today` service -would become yesterday, making it hard to know what `report_today` would -report on. - -An `eager` 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 `lifecycle: eager`. - -#### on - -Attach event listeners using [Beam::Emitter](https://metacpan.org/pod/Beam::Emitter). - - 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 `emitter` 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 - -### 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 `config` key. The value is the path to the -file to read. By default, YAML, JSON, XML, and Perl files are supported (via -[Config::Any](https://metacpan.org/pod/Config::Any)). - - # 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 `$ref`: - - # container.yml - db_config: - config: db_config.yml - dbobj: - class: My::DB - args: - conf: - $ref: db_config + The arguments to the constructor method. Used with `class` and + `method`. 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. -If you only need the config file once, you can create an anonymous config -object. +- method - # container.yml - dbobj: - class: My::DB - args: - conf: - $config: db_config.yml + The method to call to create the object. Only used with `class`. + Defaults to `"new"`. -The config file can be used as all the arguments to the service: + 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 `method` key, which is a string containing the + method to call, and optionally `args` and `return` keys. The `args` + key works like the top-level `args` key, above. The optional `return` + key can have the special value `"chain"`, which will use the return + value from the method as the value for the service ([The tutorial shows + examples of this](https://metacpan.org/pod/Beam::Wire::Help::Config#Multiple-Constructor-Methods)). - # container.yml - dbobj: - class: My::DB - args: - $config: db_config.yml + If an array is used, the top-level `args` key is not used. -In this example, the constructor will be called like: +- value - my $dbobj = My::DB->new( - dsn => 'dbi:mysql:dbname', - user => 'mysql', - pass => '12345', - ); + 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. -You can reference individual items in a configuration hash using `$path` -references: + `value` can not be used with `class` or `extends`. - # 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 +- config -**NOTE:** You cannot use `$path` and anonymous config objects. + The path to a configuration file, relative to [the dir attribute](#dir). + The file will be read with [Config::Any](https://metacpan.org/pod/Config::Any), and the resulting data + structure returned. -### Inner Containers +- extends -Beam::Wire objects can hold other Beam::Wire objects! + The name of a service to extend. The named service's configuration will + be merged with this configuration (via [the merge\_config + method](#merge_config)). - 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' ); - -### Inner Files - - inner: - class: Beam::Wire - args: - file: inner.yml + This can be used in place of the `class` key if the extended configuration + contains a class. -Inner containers can be created by reading files just like the main container. -If the `file` attribute is relative, the parent's `dir` attribute will be -added: +- with - # share/parent.yml - inner: - class: Beam::Wire - args: - file: inner.yml + 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. - # share/inner.yml - dbh: - class: DBI - method: connect - args: - - 'dbi:sqlite:data.db' + This uses [Moo::Role](https://metacpan.org/pod/Moo::Role) and [the create\_class\_with\_roles + method](https://metacpan.org/pod/Role::Tiny#create_class_with_roles), which should work with any + class (as it uses [the Role::Tiny module](https://metacpan.org/pod/Role::Tiny) under the hood). - # myscript.pl - use Beam::Wire; + This can be used with the `class` key. - my $container = Beam::Wire->new( - file => 'share/parent.yml', - ); +- on - my $dbh = $container->get( 'inner/dbh' ); + Attach an event handler to a [Beam::Emitter subclass](https://metacpan.org/pod/Beam::Emitter). This + is an array of hashes of event names and handlers. A handler is made from + a service reference (`$ref` or an anonymous service), and a subroutine to + call on that service (`$sub`). -If more control is needed, you can set the [dir](https://metacpan.org/pod/dir attribute) on the parent -container. If even more control is needed, you can make a subclass of Beam::Wire. + For example: -### Service/Configuration References + emitter: + class: My::Emitter + on: + - my_event: + $ref: my_handler + $sub: on_my_event - 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 `$ref` key whose value is the name of another -service. Optionally, this hashref may contain a `$path` key whose value is a -[Data::DPath](https://metacpan.org/pod/Data::DPath) 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 `value` key whose value contains -the raw-value you wish to reuse. + This can be used with the `class` key. -# ATTRIBUTES +This method uses [the parse\_args method](#parse_args) to parse the `args` key, +[resolving references](https://metacpan.org/pod/resolve_ref) as needed. -## file +## merge\_config -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. + my %merged = $wire->merge_config( %config ); -## dir +If `%config` contains an `extends` 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. -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 [file](https://metacpan.org/pod/file attribute). +When merging, hashes are combined, with the child configuration taking +precedence. The `args` key is handled specially to allow a hash of +args to be merged. -## config +The configuration returned is a safe copy and can be modified without +effecting the original config. -The config attribute contains a hashref of service configurations. This data is -loaded by [Config::Any](https://metacpan.org/pod/Config::Any) using the file specified by the -[file](https://metacpan.org/pod/file attribute). +## parse\_args -## services + my @args = $wire->parse_args( $for_name, $class, $args ); -A hashref of services. If you have any services already built, add them here. +Parse the arguments (`$args`) for the given service (`$for_name`) with +the given class (`$class`). -## meta\_prefix +`$args` can be an array reference, a hash reference, or a simple +scalar. The arguments will be searched for references using [the +find\_refs method](#find_refs), and then a list of arguments will be +returned, ready to pass to the object's constructor. -The character that begins a meta-property inside of a service's `args`. This -includes `$ref`, `$path`, `$method`, and etc... +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. -The default value is '$'. The empty string is allowed. +## find\_refs -# METHODS + my @resolved = $wire->find_refs( $for_name, @args ); -## get( name, \[ overrides \] ) +Go through the `@args` and recursively resolve any references and +services found inside, returning the resolved result. References are +identified with [the is\_meta method](#is_meta). -The get method resolves and returns the service named `name`. +If a reference contains a `$ref` key, it will be resolved by [the +resolve\_ref method](#resolve_ref). Otherwise, the reference will be +treated as an anonymous service, and passed directly to [the +create\_service method](#create_service). -`overrides` may be a list of name-value pairs. If specified, get() -will create an anonymous service that extends the `name` service -with the given config overrides: +This is used when [creating a service](https://metacpan.org/pod/create_service) to ensure all +dependencies are created first. - # test.pl - use Beam::Wire; - my $wire = Beam::Wire->new( - config => { - foo => { - args => { - text => 'Hello, World!', - }, - }, - }, - ); - my $foo = $wire->get( 'foo', args => { text => 'Hello, Chicago!' } ); - print $foo; # prints "Hello, Chicago!" +## is\_meta -This allows you to create factories out of any service, overriding service -configuration at run-time. + my $is_meta = $wire->is_meta( $ref_hash ); -## set +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. -The set method configures and stores the specified service. +A service hash reference must contain at least one key, and must either +be made completely of meta keys (as returned by [the get\_meta\_names +method](#get_meta_names)), or contain a [prefixed](#meta_prefix) key that +could create or reference an object (one of `class`, `extends`, +`config`, `value`, or `ref`); -## get\_config +## get\_meta\_names + + my %meta_keys = $wire->get_meta_names; + +Get all the possible service keys with the [meta prefix](#meta_prefix) already +attached. + +## resolve\_ref + + my @value = $wire->resolve_ref( $for_name, $ref_hash ); + +Resolves the given dependency from the configuration hash (`$ref_hash`) +for the named service (`$for_name`). Reference hashes contain the +following keys: + +- $ref + + The name of a service in the container. Required. + +- $path + + A data path to pick some data out of the reference. Useful with `value` + and `config` services. + + # container.yml + bounties: + value: + malcolm: 50000 + zoe: 35000 + simon: 100000 -Get the config with the given name, searching inner containers if required + captain: + class: Person + args: + name: Malcolm Reynolds + bounty: + $ref: bounties + $path: /malcolm + +- $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 + `$method` and `$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 + +## fix\_refs + + my @fixed = $wire->fix_refs( $for_name, @args ); + +Similar to [the find\_refs method](#find_refs). This method searches +through the `@args` and recursively fixes any reference paths to be +absolute. References are identified with [the is\_meta +method](#is_meta). + +This is used by [the get\_config method](#get_config) to ensure that the +configuration can be passed directly in to [the create\_service +method](https://metacpan.org/pod/create_service). ## new + my $wire = Beam::Wire->new( %attributes ); + Create a new container. # EXCEPTIONS @@ -587,3 +456,15 @@ This software is copyright (c) 2015 by Doug Bell. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. + +# POD ERRORS + +Hey! **The above document had some coding errors, which are explained below:** + +- Around line 21: + + Non-ASCII character seen before =encoding in 'Zoë'. Assuming CP1252 + +- Around line 70: + + alternative text '/file' contains non-escaped | or /