Papaya is a dependency injection container. It's a way to organize your JavaScript application to take advantage of the dependency inversion principle.
Papaya is available as a npm package.
npm install --save papaya
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.
- First, we use the
constant
method to create an attribute calledapi.url
. There is no special meaning to the.
in the service name. It's used purely for clarity. - Then, we create a service called
api
. This time we use theservice
method. This allows the service to be instantiated asynchronously. TheRestApi
instance won't be created until we use it on the final line. - On the last line, we use the
get
method to retrieve the instance ofRestApi
and call arequest
method on it. Because of the way we defined theapi
service, theapi.url
parameter will be passed into theRestApi
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
.
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()
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"
The generated API docs provide exact API definitions. See below for more user-friendly descriptions.
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
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 setsthis
to itself when calling service functions. In this example,this
,app
, and thecontainer
argument are the same thing.
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'
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
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.
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
.
Get an array of all the registered service names with keys
.
app.constant('foo', '123')
app.factory('bar', () => 'abc')
app.keys() // ['foo', 'bar']
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
- Support strict TypeScript types
- Add typescript support
- Split the
set
method intoservice
andconstant
. - Remove the
protect
method (replaced byconstant
) - Constants are no longer allowed in place of service functions.
- All service functions are now passed the container as an argument
Created by Justin Howard
Thank you to Fabien Potencier, the creator of Pimple for PHP for the inspiration for Papaya.