The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

MooseX::Orochi - Annotated Your Moose Classes With Orochi

SYNOPSIS

package MyApp::MyClass;
use Moose;
use MooseX::Orochi;

bind_constructor '/myapp/myclass' => (
    args => {
        arg1 => bind_value '/myapp/some/dep1',
        arg2 => bind_value '/myapp/some/dep2',
    }
);

# you can also inject random things
inject '/foo/bar/baz' => Orochi::Injection::Constructor->new(
    class => 'FooBar',
    args  => { ... }
);

has arg1 => (...);
has arg2 => (...);

# Then, somewhere in your main code...

my $c = Orochi->new();
$c->inject_class( 'MyApp::MyClass' );

my $object = $c->get( '/myapp/myclass' );

DESCRIPTION

MooseX::Orochi is a transparent add-on to your Moose-based classes that allows create a Depdency Injection Containers. If you don't know what that is, it basically allows you to define and assemble a set of objects that depend on eachother with no external configuration required.

With MooseX::Orochi, all you really need to do is

1. Build a set of objects with MooseX::Orochi annotations
2. use Orochi->inject_class() to inject your definitions
3. use Orochi->get() to access the resulting objects.

PROVIDED DSL

bind_constructor $path => %injection_args

Creates a Orochi::Injection::Constructor (or subclass thereof)

bind_value $path

Returns a Orochi::Injection::BindValue object, which will materialize when the containing Injection is expanded.

inject $path => $injection

Injects the given injection.

ANNOTATIONS WITH MooseX::Orochi

Suppose you have a dependency graph like the following:

MyApp ---> MyApp::Model::Foo ---> MyApp::Schema ---> DBI Args
                             |
                             ---> MyApp::Logger ---> File Name

If you're building everything based on moose from scratch, you will define this dependency like the following. First, MyApp:

package MyApp;
use Moose;
use MooseX::Orochi;

has 'foo' => (
  is => 'ro',
  isa => 'MyApp::Model::Foo'
);

bind_constructor 'myapp' => (
  args => {
    foo => bind_value 'myapp/model/foo'
  }
);

Notice the use of MooseX::Orochi, and the calls to bind_constructor. It tells us that an instance of MyApp can be retrieved by the name 'myapp'

$c->get('myapp');

and that the value for argument foo should be taken from another injected resource named 'myapp/model/foo'

MyApp->new(foo => $c->get('myapp/model/foo'));

If you were to use Setter injection instead, then it will lead to Orochi calling Orochi::Injection::Setter, which in turn will call MyApp's constructor, and setter(s) like so:

bind_constructor 'myapp' => (
  injection_type => 'Setter',
  setter_params => {
    foo => bind_value 'myapp/model/foo'
  }
);

# above will trigger the following code:
my $app = MyApp->new();
$app->foo($c->get('myapp/model/foo'));

The rest of the classes works mostly the same way. Here's MyApp::Model::Foo, and MyApp::Logger:

package MyApp::Model::Foo;
use Moose;
use MooseX::Orochi;

has 'schema' => (
  is => 'ro',
  isa => 'MyApp::Schema',
);

bind_constructor 'myapp/model/foo' => (
  args => {
    schema => bind_value 'myapp/schema',
    logger => bind_value 'myapp/logger',
  }
);

package MyApp::Logger;
use Moose;
use MooseX::Orochi;
use MooseX::Types::Path::Class;

has 'filename' => (
  is => 'ro',
  isa => 'Path::Class::File',
  coerce => 1
);

bind_constructor 'myapp/logger' => (
  args => {
    filename => bind_value 'myapp/logger/filename'
  }
);

MyApp::Schema is a bit different, in that it is DBIx::Class::Schema based, and you won't be calling new() to instantiate it (you'd call connection()), and you don't pass a name => value pair (you'd pass @connect_info).

package MyApp::Schema;
use Moose;
use MooseX::Orochi;
extends 'DBIx::Class::Schema';

bind_contructor 'schema/master' => (
  args        => bind_value 'myapp/schema/connect_info',
  deref_args  => 1,
  constructor => 'connection'
);

Here, we declare that MyApp::Schema will use myapp/schema/connect_info as its arguments (which will be de-referenced when passed to the constructor), and that we should use the method named 'connection' as the constructor.

The value to 'myapp/schema/connect_info' needs to be declared else where:

my $c = Orochi->new();
$c->inject_literal(
  'myapp/schema/connect_info' => [ 'dbi:mysql:dbname=foo', .... ] );

Finally, we need to put everything together by registering these classes to our Orochi instance:

my $c = Orochi->new();
$c->inject_literal(
  'myapp/logger/filename' => '/path/to/file.txt');
$c->inject_literal(
  'myapp/schema/connect_info' => [ 'dbi:mysql:dbname=foo', .... ] );
$c->inject_class($_) for qw(
  MyApp::Logger
  MyApp::Schema
  MyApp::Model::Foo
  MyApp
);
# or $c->inject_namespace('MyApp');

my $app = $c->get('myapp');

There are sometimes those modules that you just can touch from outside. In those cases, you will have to provide the objects yourself:

$c->inject('/path/to/another/dependency' => 
  $c->construct( sub { ... } ) );

SUBCLASS

Once you use MooseX::Orochi, every subclass can re-use the bind instructions.

package MyApp;
use Moose;
use MooseX::Orochi;

bind_constructor 'myapp' => ( ... );


package MyApp::Extended;
use Moose;

extends 'MyApp';

In the above case, unless you explicitly override the bind instructions in MyApp::Extended, you can inject MyApp::Extended and expect it to be available at

$c->get('myapp');

TODO

Documentation. Samples. Tests.

AUTHOR

Daisuke Maki <daisuke@endeworks.jp>

LICENSE

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

See http://www.perl.com/perl/misc/Artistic.html