Skip to content

A minimal JavaScript and TypeScript dependency injection container

License

Notifications You must be signed in to change notification settings

justinhoward/papaya

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

60 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Papaya npm version

Build Status Code Climate Test Coverage Known Vulnerabilities

Papaya is a dependency injection container. It's a way to organize your JavaScript application to take advantage of the dependency inversion principle.

Installation

Papaya is available as a npm package.

npm
npm install --save papaya

Getting Started

The examples here use modern JavaScript syntax, but Papaya is compatible back to ES5.

To start, create a new instance of Papaya:

const { Papaya } = require('papaya')
const app = new Papaya()

The methods you will use most often in Papaya are the get, constant, and service methods. These allow you to create and access services and attributes.

// setting a constant
app.constant('api.url', 'http://example.com/api')

// setting a service
app.service('api', () => {
    return new RestApi(app.get('api.url'))
})

// Now we access the api service
// This would typically be done in a controller
app.get('api').request()

In this example, we set up and use an api service.

  1. First, we use the constant method to create an attribute called api.url. There is no special meaning to the . in the service name. It's used purely for clarity.
  2. Then, we create a service called api. This time we use the service method. This allows the service to be instantiated asynchronously. The RestApi instance won't be created until we use it on the final line.
  3. On the last line, we use the get method to retrieve the instance of RestApi and call a request method on it. Because of the way we defined the api service, the api.url parameter will be passed into the RestApi constructor when it is created.

Once it's constructed, the api service will be cached, so if we call it again, Papaya will use the same instance of RestApi.

Organizing Your Project

Feel free to manage your containers however you like, but this is the pattern I typically use. To make it easier to reuse your container, you may want to extend the Papaya class.

# App.js

const env = require('./providers/env')
const db = require('./providers/db')

module.exports = class App extends Papaya {
  constructor() {
    super()

    this.register(env.provider)
    this.register(db.provider)
  }
}

Then split up your services into logical groups and move them into separate provider files.

# providers/env.js

module.exports = function provider(app) {
  app.constant('env.dbUser', process.env.DB_USER)
  app.constant('env.dbPassword', process.env.DB_PASS)
}
# providers/db.js
const Database = require('./Database')

module.exports = function provider(app) {
  app.service('db', () => {
    return new Database(app.get('env.dbUser'), app.get('env.dbPassword'))
  })
}

Now when you want to boot your app, just create a new instance of your custom class.

const App = require('./App')
const app = new App()
app.get('db').connect()

TypeScript

Papaya fully supports both JavaScript and TypeScript. To use types with your container, you should define interfaces for each service.

# app.ts
import { Papaya } from 'papaya'
import * as env from './providers/env'
import * as db from './providers/db'

export class App extends Papaya<
  EnvServices
  & DbServices
> {
  constructor() {
    super()

    this.register(env.provider)
    this.register(db.provider)
  }
}
# providers/env.ts
export interface EnvServices {
  'env.baseUrl': string
  'env.dbUser': string
  'env.dbPassword': string
}

export function provider(app: Papaya<EnvServices>) {
  app.constant('env.dbUser', process.env.DB_USER)
  app.constant('env.dbPassword', process.env.DB_PASS)
}
# providers/db.ts
import { Database } from './database'

// for providers with dependencies, define the types of the dependencies
export interface DbServices {
  'env.dbUser': string
  'env.dbPassword': string
  db: Database
}

export function provider(app: Papaya<DbServices>) {
  app.service('db', () => {
    return new Database(app.get('env.dbUser'), app.get('env.dbPassword'))
  })
}

If you strictly define your service interfaces this way, the TypeScript compiler will be able to do compile-time type checking on your services.

const db = app.get('db').connect()
# The TypeScript compiler knows that get returns a "Database"

If you want to turn off type checking, simply set the Papaya type to any.

// to extend Papaya
export class App extends Papaya<any> {
  ...
}

// or to create a one-off instance
const app = new Papaya<any>()

const db = app.get('db').connect()
// typescript will allow this
// but doesn't gurantee that get returns a "Database"

API

The generated API docs provide exact API definitions. See below for more user-friendly descriptions.

constant(name, constant)

Creates a simple named value service from constant. The most common use of attributes is to provide parameters for other services.

// Create an attribute
app.constant('urlPrefix', 'http://example.com')
const prefix = app.get('urlPrefix') // http://example.com

service(name, service)

Creates a singleton service.

Creates a singleton service. This is a service that will only be constructed once. Services are lazy, so it will not be created until it is used. When the service is requested with get, the function will be called to create the service.

app.service('images', function(container) {
  return new ImageService(app.get('urlPrefix'))
})

app.get('images').download('cat')

Notice that this is used to access the Papaya instance. This is a matter of preference. Papaya sets this to itself when calling service functions. In this example, this, app, and the container argument are the same thing.

get(name)

Gets a service or attribute by name. get returns the value of a service regardless of the function that was used to create a service.

app.constant('foo', 'abc')
app.service('bar', () => '123')
app.factory('baz', () => 'xyz')

app.get('foo') // 'abc'
app.get('bar') // '123'
app.get('baz') // 'xyz'

factory(name, factory)

Creates a service that will be reconstructed every time it is used. factory is similar to service, but it does not cache the return value of your service function.

app.factory('api.request', () => {
   return app.get('api').request()
})

// Calls the factory function above
const request = app.get('api.request')

// Calls the factory function again
const otherRequest = app.get('api.request')

// request !== otherRequest

extend(name, extender(extended, this))

The extend method can be used to modify existing services.

app.service('api' () => new RestApi())

app.extend('api', api => {
  api.plugin(new MyPlugin())
  return api
})

app.get('api') // Creates RestApi with MyPlugin added

In this example, we use extend to add a plugin to the api service. Because we defined api as a singleton service above, it will remain a singleton. It will also remain lazy, meaning the api service and the extender will not be called until the api service is used.

When extending a factory, it will remain a factory service, otherwise it will be converted to a singleton service (like when using the service method). Services can also be extended multiple times.

The extender function is passed 2 arguments, the previous value of the service and the container. If the service does not exist when it is extended, extend will throw an error. The extender function should return the new value for the service.

register(provider)

Registers a provider function, a convenient way to organize services into groups.

function apiProvider(container) {
  container.constant('api.url', 'http://example.com')

  container.service('api', function() {
      return new RestApi(container.get('api.url'))
  })
}

app.register(apiProvider)

The register method itself does not create services, but it allows you to register functions that create related services. In this example, we use register to group api related services together.

The provider function will be called immediately when it is registered. Its this and first argument will be set to the Papaya instance, just like in service.

keys()

Get an array of all the registered service names with keys.

app.constant('foo', '123')
app.factory('bar', () => 'abc')

app.keys() // ['foo', 'bar']

has(name)

Check if a given service is registered with has.

app.constant('api.url', 'http://example.com/api')

app.has('api.url') // true
app.has('foo') // false

Changes

Version 3

  • Support strict TypeScript types

Version 2

  • Add typescript support
  • Split the set method into service and constant.
  • Remove the protect method (replaced by constant)
  • Constants are no longer allowed in place of service functions.
  • All service functions are now passed the container as an argument

Credits

Created by Justin Howard

Thank you to Fabien Potencier, the creator of Pimple for PHP for the inspiration for Papaya.