justinhoward/papaya

View on GitHub
README.md

Summary

Maintainability
Test Coverage
# Papaya [![npm version](https://badge.fury.io/js/papaya.svg)](http://badge.fury.io/js/papaya)

[![Build Status](https://travis-ci.org/justinhoward/papaya.svg?branch=master)](https://travis-ci.org/justinhoward/papaya)
[![Code Climate](https://codeclimate.com/github/justinhoward/papaya/badges/gpa.svg)](https://codeclimate.com/github/justinhoward/papaya)
[![Test Coverage](https://codeclimate.com/github/justinhoward/papaya/badges/coverage.svg)](https://codeclimate.com/github/justinhoward/papaya)
[![Known Vulnerabilities](https://snyk.io/test/github/justinhoward/papaya/badge.svg?targetFile=package.json)](https://snyk.io/test/github/justinhoward/papaya?targetFile=package.json)

[Papaya](https://github.com/justinhoward/papaya) is a dependency injection
container. It's a way to organize your JavaScript application to take advantage
of the [dependency inversion principle][di].

- [Installation](#installation)
- [Getting Started](#getting-started)
- [TypeScript](#typescript)
- [Organizing Your Project](#organizing-your-project)
- [API](#api)
  - [constant](#constantname-constant)
  - [service](#servicename-service)
  - [get](#getname)
  - [factory](#factoryname-factory)
  - [extend](#extendname-extender)
  - [register](#registerprovider)
  - [keys](#keys)
  - [has](#hasname)
- [Changes](#changes)
- [Credits](#credits)

## Installation

Papaya is available as a npm package.

##### npm
```bash
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:

```javascript
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.

```javascript
// 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.

```javascript
# 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.

```javascript
# providers/env.js

module.exports = function provider(app) {
  app.constant('env.dbUser', process.env.DB_USER)
  app.constant('env.dbPassword', process.env.DB_PASS)
}
```

```javascript
# 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.
```javascript
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.

```typescript
# 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)
  }
}
```
```typescript
# 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)
}
```
```typescript
# 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.

```typescript
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`.

```typescript
// 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](https://www.justinhoward.org/papaya/classes/papaya.html)
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.

```javascript
// 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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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`.

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

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

### has(name)

Check if a given service is registered with `has`.

```javascript
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][github]

Thank you to [Fabien Potencier][fabien], the creator of [Pimple][pimple] for PHP
for the inspiration for Papaya.

[di]: https://en.wikipedia.org/wiki/Dependency_inversion_principle
[fabien]: http://fabien.potencier.org
[github]: https://github.com/justinhoward
[latest release]: https://github.com/justinhoward/papaya/releases/latest
[pimple]: http://pimple.sensiolabs.org