UNPKG

papaya

Version:

A minimal dependency injection container

383 lines (288 loc) 10.4 kB
# 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) 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 ### 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