Service Container

Introduction

The Confetti service container is a powerful tool for managing struct dependencies and performing dependency injection. Dependency injection is a fancy phrase that essentially means this: struct dependencies are "injected" into the struct via the constructor or, in some cases, "setter" methods.

Let's look at a simple example:

package model

import (
    "github.com/confetti-framework/contract/inter"
    "github.com/confetti-framework/foundation"
    "confetti/app/repository"
)

type User struct {
    app        inter.App
    repository repository.User
}

func NewUser(app inter.App) User {

    // Receive the repository from the application container
    userRepository := app.Make(repository.User{}).(repository.UserInterface)

    return User{app: app, repository: userRepository}
}

func (u User) IsAdmin() bool {
    return u.repository.HasRole(u, "admin")
}
















 







In this example, the User struct needs to retrieve users from a data source. So, we will inject a service is able to retrieve users. In this context, our User struct most likely uses UserRepository to retrieve user information from the database. However, since the repository is injected, we are able to easily swap it out with another implementation. We are also able to easily "mock", or create a dummy implementation of the repository.User when testing our application.

A deep understanding of the Confetti service container is essential to building a powerful, large application, as well as for contributing to the Confetti core itself.

Binding

Binding Basics

Almost all of your service container bindings will be registered within service providers, so most of these examples will demonstrate using the container in that context.

There is no need to bind structs into the container if they do not depend on any interfaces. The container does not need to be instructed on how to build these objects, since it can automatically resolve these objects.

Simple Bindings

We can register a binding using the Bind method, passing the struct or interface that we wish to register along with a Closure that returns an instance of the struct:

app.Bind((*contract.ErrorHandling)(nil), function () {
    return logging.Error{app, app.Make(http.Client{}).(http.Client)}
}

Note that we can then use the container to resolve sub-dependencies of the object we are building.

Binding A Singleton

The Singleton method binds a struct or interface into the container that should only be resolved one time. Once a singleton binding is resolved, the same object instance will be returned on subsequent calls into the container:

app.Singleton(
    model.User{},
    func() interface{} {
        return model.User{}
    },
)

Binding Instances

You may also bind an existing object instance into the container using the Instance method. The given instance will always be returned on subsequent calls into the container:

user := model.NewUser()
app.Instance("admin.User", user)

Binding Interfaces To Implementations

A very powerful feature of the service container is its ability to bind an interface to a given implementation. For example, let's assume we have an contract.EventPusher interface and a redis.EventPusher implementation. Once we have coded our redis.EventPusher implementation of this interface, we can register it with the service container like so:

app.Bind(
    (*contract.EventPusher)(nil),
    redis.EventPusher{},
)

This statement tells the container that it should inject the redis.EventPusher when a struct needs an implementation of contract.EventPusher. Now we can type-hint the contract.EventPusher interface in a constructor, or any other location where dependencies are injected by the service container:

eventPusher := app.Make((*contract.EventPusher)(nil)).(contract.EventPusher)

Binding Without Abstract

If you want to bind a struct, but do not want to use an abstract, you can also omit the abstract:

app.BindStruct(http.Client{})

client := app.Make(http.Client{}).(http.Client)

Extending Bindings

The Extend method allows the modification of resolved services. For example, when a service is resolved, you may run additional code to decorate or configure the service. The Extend method accepts a Closure, which should return the modified service, as its only argument. The Closure receives the service being resolved and the container instance:

(*app.Container()).Extend(redis.connection{}, func(service interface{}) interface{} {
    service := service.(redis.Connection)
    service.SetName("cache")

    return service
})

Resolving

You may use the Make method to resolve a concrete struct instance out of the container.

The Make method accepts the name of the struct:

client := app.Make("http.Client").(http.Client)

An interface you wish to resolve:

client := app.Make((*http.ClientInterface)(nil)).(http.ClientInterface)

An struct you wish to resolve:

client := app.Make(http.Client{}).(http.Client)

An pointer/reference you wish to resolve:

var client http.Client
app.Make(&client)

Use MakeE to get more control over the errors. For example, if you don't know if it can be resolved:

client, err := app.MakeE(http.Client{})
Contributors: Reindert Vetter, Vaggelis Yfantis