Service providers

So far we learned how to bind dependencies to the IoC container. In this guide, we take a step further to learn about service providers and how to distribute packages that play nicely with the AdonisJs eco-system.

Introduction

We know that ioc.bind method can be used to register bindings. However, there are no clear guidelines on where to call this method.

It is where service providers comes into the picture. Service providers are simple ES6 classes with lifecycle methods that are used to register and bootstrap bindings.

For example

const { ServiceProvider } = require('@adonisjs/fold')

class MyProvider extends ServiceProvider {
  register () {
    // register bindings
  }

  boot () {
    // optionally do some intial setup
  }
}

module.exports = MyProvider
  1. The register method is used to register the binding, and you should never try to use any other binding inside this method.

  2. The boot method is called when all providers have been registered. It is the right time to use existing bindings to bootstrap some state.

For example: Adding a view global.

boot () {
  const View = this.app.use('Adonis/Src/View')
  View.global('time', () => new Date().getTime())
}

Npm package as a service provider

Let’s see how we can wrap an existing npm package to a service provider. All application specific providers live inside the providers directory in the root of your app.

Do not wrap packages like lodash inside a provider, since it can be used directly and doesn’t require any setup process that can be abstracted.

Directory structure

├── app
└── providers
  └── Queue
    └── index.js
    └── Provider.js
└── start

Principles

We are going to wrap bee-queue as a provider. Below is the set of principles we want to follow.

  1. End-user should not have to worry about configuring the queue provider.

  2. All configuration should live inside config/queue.js file.

  3. It should be simple enough to create new queues with a different configuration.

Implementation

Let’s create implementing the wrapper inside providers/Queue/index.js file.

'use strict'

const BeeQueue = require('bee-queue')

class Queue {
  constructor (Config) {
    this.Config = Config
    this._queuesPool = {}
  }

  get (name) {
    /**
     * If there is an instance of queue already, then return it
     */
    if (this._queuesPool[name]) {
      return this._queuesPool[name]
    }

    /**
     * Read configuration using Config
     * provider
     */
    const config = this.Config.get(`queue.${name}`)

    /**
     * Create a new queue instance and save it's
     * reference
     */
    this._queuesPool[name] = new BeeQueue(name, config)

    /**
     * Return the instance back
     */
    return this._queuesPool[name]
  }
}

module.exports = Queue

The above class only has one method called get, which returns an instance of the queue for a given queue name.

Following are the steps performed by the get method

  1. Look for an instance of a given queue name.

  2. If an instance does not exist, read the configuration using the Config provider.

  3. Create a new bee-queue instance and store inside an object for future use.

  4. Finally, return the instance.

The Queue class is pure, since it does not have any hard dependencies on the framework and instead rely on Dependency Injection to provider the Config provider.

Service provider

Now let’s create a service provider who does the instantiation of this class and binds it to be IoC container. The code lives inside providers/Queue/Provider.js

const { ServiceProvider } = require('@adonisjs/fold')

class QueueProvider extends ServiceProvider {
  register () {
    this.app.singleton('Bee/Queue', () => {
      const Config = this.app.use('Adonis/Src/Config')
      return new (require('.'))(Config)
    })
  }
}

module.exports = QueueProvider

The this.app is a reference to the ioc object. Which means instead of calling ioc.singleton, we call this.app.singleton.

Finally, we need to register this provider like any other provider inside the start/app.js file.

const providers = [
  path.join(__dirname, '..', 'providers', 'Queue/provider')
]

Now, we can call use('Bee/Queue') inside any file of your application and use it as follows.

const Queue = use('Bee/Queue')

Queue
  .get('addition')
  .createJob({ x: 2, y: 3 })
  .save()

Distributing as a package

The bee queue provider we created lives in the same project struture. However, we can extract it into it’s own package.

Let’s create a new directory with following directory structure.

└── providers
    └── QueueProvider.js
├── src
  └── Queue
    └── index.js
└── package.json

All we did is moved the actual Queue implementation to the src directory and renamed the provider file to QueueProvider.js

Also we have to make following changes

  1. Since the Queue/index.js is in a different directory, we need to tweak the reference of this file inside our service provider.

  2. Rename Bee/Queue namespace to a more suited namespace, which has less changes of collision. For example, when creating this provider for AdonisJs, we will name it as Adonis/Addons/Queue.

const { ServiceProvider } = require('@adonisjs/fold')

class QueueProvider extends ServiceProvider {
  register () {
    this.app.singleton('Bee/Queue', () => {
      const Config = this.app.use('Adonis/Src/Config')
      return new (require('../src/Queue'))(Config)
    })
  }
}

module.exports = QueueProvider
Make sure you do not include @adonisjs/fold as a dependency for your provider. This should be installed by the main app only. For testing you can install it as a dev dependency.

Writing provider tests

You can use any testing engine you want. However we officially use japa as the testing engine to write tests for any providers.

Setting up japa is simple as shown below.

npm i --save-dev japa

Create the tests inside the test directory.

mkdir test

The tests can be executed by running the test file using node command.

node test/example.spec.js

But of course, you want to run all the test files together and for that you can make use of japa-cli.

npm i --save-dev japa-cli

And run tests as

./node_modules/.bin/japa

FAQ’s

  1. Why not install @adonisjs/fold as a dependency?
    If you have ever worked with gulp, they also recommend (p:14) not to install gulp as the dependency when creating plugins.
     
    It is required so that always the main application version of @adonisjs/fold is installed and your provider makes use of it. Otherwise, each provider will end up shipping it’s own version of IoC container.