Skip to content
This repository has been archived by the owner on Apr 1, 2019. It is now read-only.

Commit

Permalink
Add prototype and container scope notion in service
Browse files Browse the repository at this point in the history
* Add unit tests
* Add examples
* Update readme
  • Loading branch information
Kevin committed Sep 13, 2013
1 parent 4e96bac commit 41ad238
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 11 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Another big drawback is that you have to instantiate as many objects as you have
Our two classes stay untouched, the only thing you have to do is to add a configuration file.

```yaml
# services.yml
# services.yml
parameters:
mailer.transporter: 'smtp'
services:
Expand Down Expand Up @@ -176,6 +176,12 @@ And here's some more details about each keyword:

* `alias`: A string containing the target service name.

* `scope`: A string defining the service initialization scope:
* `container`: a service is initialized only one time throughout the container life (default)
* `prototype`: a new service is initialized each time you call the container

Note that the usage of a `prototype` service inside a `container` service raise a `ScopeWideningInjectionError`

__Please note:__
* You can reference a variable in the configuration with the following syntax: `%variable%`.
* You can reference declared services by prefixing it with an `@` sign.
Expand Down
28 changes: 28 additions & 0 deletions examples/scoped_services.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require 'dependency_injection/container'
require 'dependency_injection/loaders/yaml'

c = DependencyInjection::Container.new
loader = DependencyInjection::Loaders::Yaml.new(c)
loader.load(File.join(File.dirname(File.expand_path(__FILE__)), 'scoped_services.yml'))

class ContainerScopedService
def initialize
puts 'Container scoped initialization'
end
end

class PrototypeScopedService
def initialize
puts 'Prorotype scoped initialization'
end
end

c.get('my.container.scoped.service')
# => Container scoped initialization
c.get('my.container.scoped.service')
# =>

c.get('my.prototype.scoped.service')
# => Prorotype scoped initialization
c.get('my.prototype.scoped.service')
# => Prorotype scoped initialization
6 changes: 6 additions & 0 deletions examples/scoped_services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
services:
my.container.scoped.service:
class: 'ContainerScopedService'
my.prototype.scoped.service:
class: 'PrototypeScopedService'
scope: 'prototype'
2 changes: 1 addition & 1 deletion lib/dependency_injection/container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def get(name)

def register(name, klass_name, lazy=false)
definition = lazy ? LazyDefinition.new(klass_name, self) : Definition.new(klass_name, self)
@definitions[name] =definition
@definitions[name] = definition
end

def register_alias(name, alias_definition_name)
Expand Down
31 changes: 23 additions & 8 deletions lib/dependency_injection/definition.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
require 'active_support/core_ext/string/inflections'
require 'dependency_injection/scope_widening_injection_error'

module DependencyInjection
class Definition
attr_accessor :arguments, :configurator, :klass_name, :method_calls
attr_accessor :arguments, :configurator, :klass_name, :method_calls, :scope

def initialize(klass_name, container)
@container = container
self.arguments = []
self.klass_name = klass_name
self.method_calls = {}
self.scope = :container
end

def add_argument(argument)
Expand Down Expand Up @@ -38,20 +40,30 @@ def klass
end

def object
return @object if @object
self.send("#{self.scope}_scoped_object")
end

private

def container_scoped_object
@object ||= initialize_object
end

@object = self.klass.new(*resolve(self.arguments))
self.method_calls.each { |method_name, arguments| @object.send(method_name, *resolve(arguments)) }
def initialize_object
object = self.klass.new(*resolve(self.arguments))
self.method_calls.each { |method_name, arguments| object.send(method_name, *resolve(arguments)) }
if self.configurator
name, method_name = self.configurator
configurator_object = resolve([name]).first
configurator_object.send(method_name, @object)
configurator_object.send(method_name, object)
end

@object
object
end

private
def prototype_scoped_object
initialize_object
end

def resolve(arguments)
resolve_references(resolve_container_parameters(arguments))
Expand All @@ -70,7 +82,10 @@ def resolve_container_parameters(arguments)
def resolve_references(arguments)
arguments.map do |argument|
if /^@(?<reference_name>.*)/ =~ argument
@container.get(reference_name)
reference = @container.get(reference_name)
raise ScopeWideningInjectionError if reference.scope == :prototype && scope == :container

reference
else
argument
end
Expand Down
1 change: 1 addition & 0 deletions lib/dependency_injection/loaders/yaml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def add_service(name, parameters)
def add_standard_service(name, parameters)
lazy_load = parameters['lazy'] || false
definition = @container.register(name, parameters['class'], lazy_load)
definition.scope = parameters['scope'] if parameters['scope']
definition.add_arguments(*parameters['arguments']) if parameters['arguments']
if (configurator = parameters['configurator'])
definition.add_configurator(configurator[0], configurator[1])
Expand Down
2 changes: 2 additions & 0 deletions lib/dependency_injection/scope_widening_injection_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class ScopeWideningInjectionError < Exception
end
16 changes: 16 additions & 0 deletions test/dependency_injection/loaders/test_yaml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ def test_adding_service_with_alias_parameters
@yaml_loader.send(:add_service, 'my_alias', { 'alias' => 'my_definition' })
end

def test_adding_service_without_defined_scope
definition = mock
@container.stubs(:register).with('key_1', 'MyKlass', false).returns(definition)

definition.expects(:scope=).never
@yaml_loader.send(:add_service, 'key_1', { 'class' => 'MyKlass' })
end

def test_adding_service_with_defined_scope
definition = mock
@container.stubs(:register).with('key_1', 'MyKlass', false).returns(definition)

definition.expects(:scope=).with('awesome_scope')
@yaml_loader.send(:add_service, 'key_1', { 'class' => 'MyKlass', 'scope' => 'awesome_scope' })
end

def test_adding_standard_service_as_lazy
@container.expects(:register).with('my_lazy_definition', 'MyLazyDefinition', true)
@yaml_loader.send(:add_standard_service, 'my_lazy_definition', { 'class' => 'MyLazyDefinition', 'lazy' => true })
Expand Down
57 changes: 56 additions & 1 deletion test/dependency_injection/test_definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,32 @@ def test_getting_object_with_configurator
@definition.object
end

def test_getting_container_scoped_object
@definition.scope = :container
@definition.expects(:send).with('container_scoped_object')
@definition.object
end

def test_getting_container_scoped_object_multiple_times
@definition.stubs(:initialize_object).returns(:object_1, :object_2)
@definition.scope = :container
assert_equal(:object_1, @definition.object)
assert_equal(:object_1, @definition.object)
end

def test_getting_prototype_scoped_object
@definition.scope = :prototype
@definition.expects(:send).with('prototype_scoped_object')
@definition.object
end

def test_getting_prototype_scoped_object_multiple_times
@definition.stubs(:initialize_object).returns(:object_1, :object_2)
@definition.scope = :prototype
assert_equal(:object_1, @definition.object)
assert_equal(:object_2, @definition.object)
end

def test_resolving_first_container_parameters
changed_arguments = mock
arguments = mock
Expand Down Expand Up @@ -162,10 +188,39 @@ def test_resolving_references_without_references
assert_equal(%w(first second), @definition.send(:resolve_references, %w(first second)))
end

def test_resolving_references_with_references
def test_resolving_references_with_defintion_and_referenced_object_in_container_scope
referenced_object = mock
referenced_object.stubs(:scope).returns(:container)
@definition.scope= :container
@container.stubs(:get).with('reference.name').returns(referenced_object)

assert_equal(['first', referenced_object], @definition.send(:resolve_references, %w(first @reference.name)))
end

def test_resolving_references_with_defintion_and_referenced_object_in_prototype_scope
referenced_object = mock
referenced_object.stubs(:scope).returns(:prototype)
@definition.scope= :prototype
@container.stubs(:get).with('reference.name').returns(referenced_object)

assert_equal(['first', referenced_object], @definition.send(:resolve_references, %w(first @reference.name)))
end

def test_resolving_references_with_defintion_in_prototype_scope_and_referenced_object_in_container_scope
referenced_object = mock
referenced_object.stubs(:scope).returns(:container)
@definition.scope= :prototype
@container.stubs(:get).with('reference.name').returns(referenced_object)

assert_equal(['first', referenced_object], @definition.send(:resolve_references, %w(first @reference.name)))
end

def test_resolving_references_with_defintion_in_container_scope_and_referenced_object_in_prototype_scope
referenced_object = mock
referenced_object.stubs(:scope).returns(:prototype)
@definition.scope= :container
@container.stubs(:get).with('reference.name').returns(referenced_object)

assert_raises(ScopeWideningInjectionError) { @definition.send(:resolve_references, %w(first @reference.name)) }
end
end

0 comments on commit 41ad238

Please sign in to comment.