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

Introduction

We know that ioc.bind method can be used to register bindings. However, we’re yet to define where to call this method.

This is where service providers come into play. Service providers are pure 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 initial setup
  }
}

module.exports = MyProvider
  1. The register method is used to register bindings, 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, and is the right place to use existing bindings to bootstrap application 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 in a service provider.

Do not wrap packages like lodash in a servive provider since it can be used directly and doesn’t require any setup process.

All application specific providers live inside the providers directory in the root of your app:

Directory structure

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

Principles

We are going to wrap bee-queue as a provider.

Here is a set of principles we want to follow:

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

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

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

Implementation

Let’s implement the wrapper inside the providers/Queue/index.js file:

providers/Queue/index.js
'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 has only one method called get, which returns an instance of the queue for a given queue name.

The steps performed by the get method are:

  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 relies on Dependency Injection to provide the Config Provider.

Service provider

Now let’s create a service provider that does the instantiation of this class and binds it to the IoC container.

The code lives inside providers/Queue/Provider.js:

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

Note that 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:

start/app.js
const providers = [
  path.join(__dirname, '..', 'providers', 'Queue/Provider')
]

Now, we can call use('Bee/Queue') inside any file in your application to use it:

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 structure. However, we can extract it into its own package.

Let’s create a new directory with the following directory structure:

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

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

Also, we have to make the following changes:

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

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

providers/QueueProvider.js
const { ServiceProvider } = require('@adonisjs/fold')

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

module.exports = QueueProvider
Do not include @adonisjs/fold as a dependency for your provider, as this should be installed by the main application only. For testing, you can install it as a dev dependency.

Writing provider tests

AdonisJs officially uses japa to write provider tests, though you can use any testing engine you want.

Setting up japa is straightforward:

> 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 the node command:

> node test/example.spec.js

To run all your tests together, you can use japa-cli:

> npm i --save-dev japa-cli

And run all tests via:

> ./node_modules/.bin/japa

FAQ’s

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