The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Beam::Wire - Lightweight Dependency Injection Container

VERSION

version 1.012

STATUS

Coverage Status

SYNOPSIS

# wire.yml

dbh:
    class: 'DBI'
    method: connect
    args:
        - 'dbi:mysql:dbname'
        - {
            PrintError: 1
          }

# myscript.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' ) );

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.

OVERVIEW

Beam::Wire loads a configuration file and stores the specified configuration in the config 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.

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.

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.

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' }

Service Attributes

class

The class to instantiate. The class will be loaded and the method (below) method called.

method

The class method to call to construct the object. Defaults to new.

If multiple methods are needed to initialize an object, method 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 My::Service-new( foo => "bar" );> to get our object, then we call $obj-set_baz( "Fizz" );> as a further initialization step.

To chain methods together, add return: chain:

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" );

args

The arguments to the method 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:

# 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

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 args 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

activemq_dev will get the port, user, and password arguments from the base service activemq.

lifecycle

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:

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.

emitter:
    class: My::Emitter
    on:
        before_my_event:
            $ref: listener
            $method: on_before_my_event
        my_event:
            - $ref: listener
              $method: on_my_event
            - $ref: other_listener
              $method: 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
            $method: on_before_my_event
        - my_event:
            $ref: listener
            $method: on_my_event
        - my_event:
            $ref: other_listener
            $method: 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).

# 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

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 $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

NOTE: You cannot use $path and anonymous config objects.

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' );

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 file attribute is relative, the parent's dir 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 dir on the parent container. If even more control is needed, you can make a subclass of Beam::Wire.

Service/Configuration References

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 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.

ATTRIBUTES

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.

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 file.

config

The config attribute contains a hashref of service configurations. This data is loaded by Config::Any using the file specified by the file.

services

A hashref of services. If you have any services already built, add them here.

meta_prefix

The character that begins a meta-property inside of a service's args. This includes $ref, $path, $method, and etc...

The default value is '$'. The empty string is allowed.

METHODS

get( name, [ overrides ] )

The get method resolves and returns the service named name.

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:

# 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!"

This allows you to create factories out of any service, overriding service configuration at run-time.

set

The set method configures and stores the specified service.

get_config

Get the config with the given name, searching inner containers if required

new

Create a new container.

EXCEPTIONS

If there is an error internal to Beam::Wire, an exception will be thrown. If there is an error with creating a service or calling a method, the exception thrown will be passed- through unaltered.

Beam::Wire::Exception

The base exception class

Beam::Wire::Exception::Constructor

An exception creating a Beam::Wire object

Beam::Wire::Exception::Service

An exception with service information inside

Beam::Wire::Exception::NotFound

The requested service or configuration was not found.

Beam::Wire::Exception::InvalidConfig

The configuration is invalid:

AUTHORS

COPYRIGHT AND LICENSE

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.