Giter Site home page Giter Site logo

zazoomauro / node-dependency-injection Goto Github PK

View Code? Open in Web Editor NEW
270.0 8.0 30.0 1.68 MB

The NodeDependencyInjection component allows you to standarize and centralize the way objects are constructed in your application.

Home Page: https://github.com/zazoomauro/node-dependency-injection/wiki

License: MIT License

JavaScript 94.66% TypeScript 5.34%
dependency-injection dependency-manager nodejs es6 es2017 es2015 service-injector ioc ioc-container javascript

node-dependency-injection's People

Contributors

avdg avatar barinbritva avatar carlino3 avatar cjuega avatar clement-michelet avatar coal182 avatar dependabot[bot] avatar dornasportse avatar madhug-nadig avatar rafal-ksiazek-rmtcfm-com avatar rexuswolf avatar rsaladocid avatar snyk-bot avatar stevearm avatar vilasmaciel avatar zazoomauro avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

node-dependency-injection's Issues

Prototype pollution attack

The merge function, and the applyToDefaults and applyToDefaultsWithShallow functions which leverage merge behind the scenes, are vulnerable to a prototype pollution attack when provided an unvalidated payload created from a JSON string containing the proto property.

Affected versions: 4.x, 5.x
Fixed versions: 4.2.1, 5.0.3
Identifier: CVE-2018-3728
Solution: Upgrade to latest version.
Credit: HoLyVieR
Sources: https://nodesecurity.io/advisories/566
https://hackerone.com/reports/310439
hapijs/hoek@32ed5c9
hapijs/hoek@5aed1a8

Deprecating Services

Deprecating Services

Once you have decided to deprecate the use of a service (because it is outdated or you decided not to maintain it anymore), you can deprecate its definition:

let definition = container.register('mailer', Mailer)
definition.deprecated = 'The mailer service is deprecated since 3.4 and will be removed in 4.0'

or

mailer:
    class: ./Mailer
    deprecated: 'The mailer service is deprecated since 3.4 and will be removed in 4.0'

Now, every time this service is used, a deprecation warning is triggered, advising you to stop or to change the usage of that service.

It is strongly recommended that you define a custom message because there is no generic message provided. A good message informs when this service was deprecated, until when it will be maintained and the alternative services to use (if any).

How to Manage Common Dependencies with Parent Services

As you add more functionality to your application, you may well start to have related classes that share some of the same dependencies. For example, you may have multiple repository classes which need some manager service and an optional logger service:

class BaseRepository {
  constructor (someManager, logger) {
    this._entityManager = someManager
    this._logger = logger
  }
}

Just as you use inheritance to avoid duplication in your code, the service container allows you to extend parent services in order to avoid duplicated service definitions:

YAML
services:
    app.base_repository:
        # as no class is configured, the parent service MUST be abstract
        abstract:  true
        arguments: ['@some_manager', '@logger']

    app.user_repository:
        class:  ./App/Repository/UserRepository
        parent: app.base_repository

    app.post_repository:
        class:  ./App/Repository/PostRepository
        parent: app.base_repository

    # ...
JS
import UserRepository from './App/Repository/UserRepository'
import PostRepository from './App/Repository/PostRepository'
import {Reference, DefinitionDecorator} from 'node-dependency-injection'

container.register('app.base_repository')
  .addArgument(new Reference('some_manager'))
  .addArgument(new Reference('logger'))

let definition = new DefinitionDecorator('app.base_repository')
definition.Object = UserRepository
container.setDefinition('app.user_repository', definition)

let definition = new DefinitionDecorator('app.base_repository')
definition.Object = PostRepository
container.setDefinition('app.post_repository', definition)
// ...

In this context, having a parent service implies that the arguments and method calls of the parent service should be used for the child services. Specifically, SomeManager and Logger will be injected.

Node Dependency Injection Command Line Interface

Create a new node-dependency-injection-cli on /bin folder called

ndi

  • Should use CommanderJS
  • Should be available on the node_modules/.bin folder

TASKS

  • Can create a new config file: $ ndi config:create --name=services --format=yml|json|js /path/folder/
    • default filename: services
    • default format: yml
    • path must be mandatory
  • Can check for container errors: $ ndi container:check /path/to/configurationfile
    • Output would be SUCCESS or ERROR -> error type
  • Can show you information about a single service: $ ndi container:service project.some_manager /path/to/configurationfile
    • Output should contain key, service path, arguments, public, calls, tags, properties, laziness, deprecated message, factory, synthetic, decoration configuration, shared
  • Can search for services: $ ndi container:search *Manager /path/to/configurationfile
    • If the results contain only one result will show the same information as the previous task
    • If the results contains more than one result will show a list of services and the user would select one with some nice/cool command line interface

Aliasing

Aliasing

You may sometimes want to use shortcuts to access some services.

container.setAlias('mailer', 'app.mailer')

or

services:
    app.mailer:
        class: ./Mailer

    mailer: '@app.mailer'

This means that when using the container directly, you can access the app.mailer service by asking for the mailer service like this:

let mailer = container.get('mailer')

Refactor the way we optimize and remove on compile

Instead of:

// optimize
if (this._compilerPass[PassConfig.TYPE_OPTIMIZE].length > 0) {
  this._processCompilerPass(PassConfig.TYPE_OPTIMIZE)
} else {
  for (let [id, definition] of this._definitions) {
    if (!this._container.get(id) && !definition.lazy) {
      let instance = this._getInstanceFromDefinition(definition)
      this._container.set(id, instance)
    }
  }
  this._frozen = true
}

// remove
if (this._compilerPass[PassConfig.TYPE_REMOVE].length > 0) {
  this._processCompilerPass(PassConfig.TYPE_REMOVE)
} else {
  for (let [id, definition] of this._definitions) {
    if (definition.public === false) {
      this._container.delete(id)
    }
  }
}

add this code to default proper compiler pass class and add it by default in to the compiler.
So create

  • CompilerPass/OptimizePass
  • CompilerPass/RemovePass

codecov.io integration

  • Add codecov.io integration
  • Travis will upload the new code coverage
  • Add code coverage unit testing

Using a Factory to Create Services

Using a Factory to Create Services

Suppose you have a factory that configures and returns a new NewsletterManager object:

import NewsletterManager from './NewsletterManager'

class NewsletterManagerFactory {
    static createNewsletterManager() {
        let newsletterManager = new NewsletterManager()

        // ...

        return newsletterManager
    }
}

To make the NewsletterManager object available as a service, you can configure the service container to use the NewsletterManagerFactory.createNewsletterManager() factory method:

YAML
services:
    # call a static method
    app.newsletter_manager:
        factory: 
          class: './App/Email/NewsletterManager'
          method: 'createNewsletterManager'

    # OR

    # call a method on the specified service
    app.newsletter_manager_factory:
        class: ./App/Email/NewsletterManagerFactory

    app.newsletter_manager:
        factory:
          class: '@app.newsletter_manager_factory'
          method: 'createNewsletterManager'      
JS
let definition = new Definition()
definition.setFactory(NewsletterManagerFactory, 'createNewsletterManager')
container.setDefinition('app.newsletter_manager', definition)

// or

let definitionFactory = new Definition(NewsletterManagerFactory)
container.setDefinition('app.newsletter_manager_factory', definitionFactory)

let definition = new Definition()
definition.setFactory(new Reference('app.newsletter_manager_factory'), 'createNewsletterManager')
container.setDefinition('app.newsletter_manager', definition)

You can also send some arguments to the static method if needed

YAML
services:
    app.newsletter_manager:
        arguments: ['simple_string', '@another_service', '%package_reference']
        factory: 
          class: './App/Email/NewsletterManager'
          method: 'createNewsletterManager'
JS
let definition = new Definition()
definition.args = ['simple_string', new Reference('another_service'), new PackageReference('package_reference')]
definition.setFactory(NewsletterManagerFactory, 'createNewsletterManager')
container.setDefinition('app.newsletter_manager', definition)

Sent the file path in the load method instead in the loader constructor

Instead of loading the service configuration file in the FileLoader constructor load the file directly in the load method.

import {ContainerBuilder, YamlFileLoader} from 'node-dependency-injection'

let container = new ContainerBuilder()
let loader = new YamlFileLoader(container)
loader.load('services.yml')
  1. Deprecate the second parameter usage in the constructor. (throw a deprecated warning if the second parameter is sent)
  2. Enable an optional parameter in the load method (throw a deprecated warning if the parameter is not set)

Documentation

  • Adding documentation folder and improvements on the documentation
  • Use readthedocs.org tool

Parameters

Parameters in Configuration Files

Use the parameters section of a config file to set parameters:

parameters:
    mailer.transport: sendmail

You can refer to parameters elsewhere in any config file by surrounding them with percent (%) signs, e.g. %mailer.transport%.

parameters:
    mailer.transport: sendmail

services:
    app.mailer:
        class:     ./App/Mailer
        arguments: ['%mailer.transport%']

Getting and Setting Container Parameters in JavaScript

Working with container parameters is straightforward using the container's accessor methods for parameters:

import {ContainerBuilder} from 'node-dependency-injection'
let container = new ContainerBuilder()

if (container.hasParameter('mailer.transport')) {
    // ...
}

let mailerTransport = container.getParameter('mailer.transport')

// add a new parameter
container.setParameter('mailer.transport', 'sendmail')

Array Parameters

Parameters do not need to be flat strings, they can also contain array values.
For example

parameters:
    my_mailer.gateways: [mail1, mail2, mail3]

    my_multilang.language_fallback:
        en:
            - en
            - fr
        fr:
            - fr
            - en

How to Decorate Services

This configuration replaces app.mailer with a new one, but keeps a reference of the old one as app.decorating_mailer.inner:

YAML
services:
    app.mailer:
        class: ./AppBundle/Mailer

    app.decorating_mailer:
      class: ./AppBundle/DecoratingMailer
      decorates: app.mailer
      arguments: ['@app.decorating_mailer.inner']
      public: false
JS
import DecoratingMailer from './DecoratingMailer'
import {Reference} from 'node-dependency-injection'

let definition = container.register('app.decorating_mailer', DecoratingMailer)
definition.decoratedService = 'app.mailer'
definition.addArgument(new Reference('app.decorating_mailer.inner'))
definition.public = false

Here is what's going on here: the decorates option tells the container that the app.decorating_mailer service replaces the app.mailer service. By convention, the old app.mailer service is renamed to app.decorating_mailer.inner, so you can inject it into your new service.

Most of the time, the decorator should be declared private, as you will not need to retrieve it as app.decorating_mailer from the container.

The visibility of the decorated app.mailer service (which is an alias for the new service) will still be the same as the original app.mailer visibility.

The generated code will be the following:

let mailerService = new DecoratingMailer(new Mailer()));

Decoration Priority

If you want to apply more than one decorator to a service, you can control their order by configuring the priority of decoration, this can be any integer number (decorators with higher priorities will be applied first).

YAML
services:
  foo:
      class: Foo
  
  bar:
      class: Bar
      public: false
      decorates: foo
      decoration_priority: 5
      arguments: ['@bar.inner']
  
  baz:
      class: Baz
      public: false
      decorates: foo
      decoration_priority: 1
      arguments: ['@baz.inner']
JS
import {Reference} from 'node-dependency-injection';

container.register('foo', Foo)

let definition = container.register('bar', Bar)
definition.addArgument(new Reference('bar.inner'))
definition.public = false
definition.decoratedService = 'foo'
definition.decorationPriority = 5

let definition = container.register('baz', Baz)
definition.addArgument(new Reference('baz.inner'))
definition.public= false
definition.decoratedService = 'foo'
definition.decorationPriority = 1

The generated code will be the following:

let fooService = new Baz(new Bar(new Foo())));

Configure a Service with a Configurator

The service configurator is a feature of the service container that allows you to use a callable to configure a service after its instantiation.

A service configurator can be used, for example, when you have a service that requires complex setup based on configuration settings coming from different sources/services. Using an external configurator, you can maintain the service implementation cleanly and keep it decoupled from the other objects that provide the configuration needed.

Another use case is when you have multiple objects that share a common configuration or that should be configured in a similar way at runtime.

For example, suppose you have an application where you send different types of emails to users. Emails are passed through different formatters that could be enabled or not depending on some dynamic application settings. You start defining a NewsletterManager class like this:

class NewsletterManager {
    setEnabledFormatters(enabledFormatters) {
        this.enabledFormatters = enabledFormatters
    }

    // ...
}

and also a GreetingCardManager class:

class GreetingCardManager {
    setEnabledFormatters(enabledFormatters) {
        this.enabledFormatters = enabledFormatters
    }

    // ...
}

As mentioned before, the goal is to set the formatters at runtime depending on application settings. To do this, you also have an EmailFormatterManager class which is responsible for loading and validating formatters enabled in the application:

class EmailFormatterManager {

    // ...

    getEnabledFormatters() {
        // code to configure which formatters to use
        let enabledFormatters = [...]

        // ...

        return enabledFormatters
    }
}

If your goal is to avoid having to couple NewsletterManager and GreetingCardManager with EmailFormatterManager, then you might want to create a configurator class to configure these instances:

class EmailConfigurator {
    /**
     * @param  {EmailFormatterManager} formatterManager
     */
    constructor(formatterManager) {
        this.formatterManager = formatterManager
    }

    configure(emailManager) {
        emailManager.setEnabledFormatters(this.formatterManager.getEnabledFormatters())
    }

    // ...
}

The EmailConfigurator's job is to inject the enabled formatters into NewsletterManager and GreetingCardManager because they are not aware of where the enabled formatters come from. On the other hand, the EmailFormatterManager holds the knowledge about the enabled formatters and how to load them, keeping the single responsibility principle.

You can configure the service configurator using the configurator option:

container.register('app.email_formatter_manager', EmailFormatterManager)
container
  .register('app.email_configurator', EmailConfigurator)
  .addArgument(new Reference('app.email_formatter_manager'))

container
  .register('app.newsletter_manager', NewsletterManager)
  .addArgument(new Reference('mailer'))
  .setConfigurator(new Reference('app.email_configurator'), 'configure')

container
  .register('app.greeting_card_manager', GreetingCardManager);
  .addArgument(new Reference('mailer'))
  .setConfigurator(new Reference('app.email_configurator'), 'configure')
YAML
services:
    app.email_formatter_manager:
        class: ./EmailFormatterManager

    app.email_configurator:
        class:     ./App/Mail/EmailConfigurator
        arguments: ['@app.email_formatter_manager']

    app.newsletter_manager:
        class:        ./App/Mail/NewsletterManager
        arguments:    ['@mailer']
        configurator: ['@app.email_configurator', configure]

    app.greeting_card_manager:
        class:        ./App/Mail/GreetingCardManager
        arguments:    ['@mailer']
        configurator: ['@app.email_configurator', configure]
JSON
{
	"services": {
		"app.email_formatter_manager": {
			"class": "./EmailFormatterManager"
		},
		"app.email_configurator": {
			"class": "./App/Mail/EmailConfigurator",
			"arguments": [
				"@app.email_formatter_manager"
			]
		},
		"app.newsletter_manager": {
			"class": "./App/Mail/NewsletterManager",
			"arguments": [
				"@mailer"
			],
			"configurator": [
				"@app.email_configurator",
				"configure"
			]
		},
		"app.greeting_card_manager": {
			"class": "./App/Mail/GreetingCardManager",
			"arguments": [
				"@mailer"
			],
			"configurator": [
				"@app.email_configurator",
				"configure"
			]
		}
	}
}

When requesting the app.newsletter_manager or app.greeting_card_manager service, the created instance will first be passed to the EmailConfigurator.configure() method

How to get required name in factory method?

I want to create something like to Abstract Factory and I need a required name.
I check arguments but this was empty. Is there any possibility to get a required name in the factory method?

Override class object on definition

import SomeManager from './SomeManager'
import SomeOtherManager from './SomeOtherManager'

definition = new Definition(SomeManager)
definition.Object = SomeOtherManager

Wrong documentation

Fix the README documentation file when instantiating new services with string instead of class object

Managing Configuration with Extensions

As well as loading configuration directly into the container you can manage it by registering extensions with the container. The first step in the compilation process is to load configuration from any extension classes registered with the container. Unlike the configuration loaded directly, they are only processed when the container is compiled. If your application is modular then extensions allow each module to register and manage their own service configuration.

The extensions must have load method and can be registered with the container with:

container.registerExtension(new DemoExtension())

The main work of the extension is done in the load() method. In the load() method you can load configuration from one or more configuration files as well as manipulate the container definitions using the methods shown in Working with definitions.

The load() method is passed a fresh container to set up, which is then merged afterwards into the container it is registered with. This allows you to have several extensions managing container definitions independently. The extensions do not add to the containers configuration when they are added but are processed when the container's compile() method is called.

A very simple extension may just load configuration files into the container:

class DemoExtension {
  /**
   * @param {ContainerBuilder} container
   */
  load (container) {
    let loader = new YamlFileLoader(container)
    loader.load('path/to/some/services.yml')
  }
}

This does not gain very much compared to loading the file directly into the overall container being built. It just allows the files to be split up amongst the modules. Being able to affect the configuration of a module from configuration files outside of the module/bundle is needed to make a complex application configurable. This can be done by specifying sections of config files loaded directly into the container as being for a particular extension. These sections on the config will not be processed directly by the container but by the relevant Extension.

Passing Arguments to the Factory Method

Passing Arguments to the Factory Method

If you need to pass arguments to the factory method, you can use the arguments options inside the service container.

services:
    # ...

    app.newsletter_manager:
        class:     ./App/Email/NewsletterManager
        factory:   'newsletter_manager_factory:createNewsletterManager'
        arguments: ['@templating']

How to Define Non Shared Services

In the service container, all services are shared by default. This means that each time you retrieve the service, you'll get the same instance. This is often the behaviour you want, but in some cases, you might want to always get a new instance.

In order to always get a new instance, set the shared setting to false in your service definition:

YAML
services:
    app.some_not_shared_service:
        class: ...
        shared: false
        # ...
JS
import {Definition} from 'node-dependency-injection'

let definition = new Definition(...)
definition.shared = false

container.setDefinition('app.some_not_shared_service', definition)

Now, whenever you call container.get('app.some_not_shared_service') or inject this service, you'll receive a new instance.

Allow complex parameter & arguments

For a service, I need to pass a configuration object as an argument.

My use case is a custom factory using axios library which receive a configuration object (base URL, custom headers, etc.) and return a configured instance of axios.

I can neither use parameter or direct arguments to pass my configuration to my factory.

With a parameter configuration like this :

{
  "parameters": {
    "api_client.facebook.config": {
      "baseURL": "https://graph.facebook.com/v2.9"
    }
  }
}

The configuration cannot be loaded. There is a type check on parameter value.

When using a service configuration like this :

{
  "services": {
    "api_client.facebook": {
      "factory": {
        "class": "../../../service/factory/axios-factory",
        "method": "create"
      },
      "arguments": [
        {
          "baseURL": "https://graph.facebook.com/v2.9"
        }
      ]
    }
  }
}

There is an error because the argument is expected to be a string (for argument replacing).

My change proposal is to :

  • simply allow complex types
  • or introduce a flag (ex: ~json({"foo": "bar"})) to pass the parameter to a parameter converter

Tagging

How to Work with Service Tags

In the service container, a tag implies that the service is meant to be used for a specific purpose. Take the following example:

import {Definition, ContainerBuilder} from 'node-dependency-injection'
import FileRepository from './Entity/Repository/FileRepository'

let definition = new Definition(FileRepository)
definition.addTag('repository')

container.setDefinition('app.entity.file_repository', definition)

or

services:
    app.entity.file_repository:
        class: ./Entity/Repository/FileRepository
        tags:
            - { name: repository }

You can now use a compiler pass to ask the container for any services with the repository tag. Example:

class RepositoryPass {
    process (container) {
        taggedServices = container.findTaggedServiceIds('repository');

        foreach (let [id, definition] of taggedServices) {
            // ...
        }
    }
}

Working with definitions

Build new helpful methods in the container

  1. hasDefinition(serviceName) find out if there is an serviceName definition
  2. has(serviceName) find out if there is an serviceName definition or alias
  3. getDefinition(serviceName) get the serviceName definition
  4. findDefinition(serviceName) get the definition with the serviceName ID or alias

Ability to register the definition in the container

import {ContainerBuilder, Definition} from 'node-dependency-injection'

let container = new ContainerBuilder()
let definition = new Definition(SomeClass)
container.setDefinition('some_manager', definition)

Add arguments in the Definition constructor

import {ContainerBuilder, Definition, Reference} from 'node-dependency-injection'

let container = new ContainerBuilder()
let definition = new Definition(SomeClass, [new Reference('other_manager'), 'some_text'])
container.setDefinition('some_manager', definition)

How to Import Configuration Files/Resources

Importing Configuration with imports

If your application ends up having many services, this file becomes huge and hard to maintain. To avoid this, you can split your service configuration into multiple service files:

For example:

# app/config/services/mailer.yml

services:
    app.mailer:
        class:        ./App/Mailer
        arguments:    ['mail_transport']

The definition itself hasn't changed, only its location. To make the service container load the definitions in this resource file, use the imports key in any already loaded resource

YAML
# app/config/services.yml
imports:
    - { resource: services/mailer.yml }
    - { resource: services/another.yml }
JSON
{
  "imports": [
    {"resource": "services/mailer.json"},
    {"resource": "services/another.json"}
  ]
}
JS
module.exports = {
  imports: [
    {resource: 'services/mailer.js'},
    {resource: 'services/another.js'}
  ]
}

Reference Symfony as a source of inspiration

First of all, good job on implementing DI for JavaScript. Coming from Symfony ecosystem, it's what i've been looking for and will use it for a project.

But, I'm a little disappointed to not see any reference to the framework as a source of inspiration, especially when most of the architecture comes from it. It's usually a good faith to tell where the inspiration comes from.

So, my issue/request is just to add a reference to Symfony in the README.md file

Lazy Services

Lazy Services

In some cases, you may want to inject a service that is a bit heavy to instantiate, but is not always used inside your object. Configuring lazy services is one answer to this. With a lazy service the container builder will instantiate the service only when we need it (during the container builder get call method)

services:
   app.mailer:
     class: ./App/Mailer
     lazy:  true

The actual class will be instantiated as soon as you try to interact with the service (e.g. call one of its methods).

Adding Additional Attributes on Tags

Sometimes you need additional information about each service that's tagged with your tag.
For example, you might want to add an event to each listener.

To begin with, change the services configuration file:

YAML
services:
    app.listener.user_listener:
        class: ./Listener/UserListener
        tags:
              - {name: listener, attributes: {event: postUpdate}}
    
    app.listener.account_listener:
        class: ./Listener/AccountListener
        tags:
              - {name: listener, attributes: {event: prePersist}}
JS
let definition = new Definition(SomeObject)
let attributes = new Map()
attributes.set('event', 'prePersist')
definition.addTag('listener', attributes)
container.setDefinition('app.listener', definition)

Notice that you've added a generic event key to the tag.
To actually use this, update the compiler:

class ListenerPass {
    /**
     * @param {ContainerBuilder} container
     */
    process (container) {
       let taggedServices = container.findTaggedServiceIds('listener')
       
       for (let definition of taggedServices.values()) {
         for (let tag of definition.tags) {
           definition.addMethodCall(tag.attributes.get('event'))
         }
       }
    }
}

The double loop may be confusing. This is because a service can have more than one tag. You tag a service twice or more with the listener tag. The second for loop iterates over the listener tags set for the current service and gives you one tag object.

Remove not necessary instances from container on compile

We can also remove some unused instances from container on compile

  • private instances

Create 3 news pass ordering types

  • PassConfig.TYPE_BEFORE_REMOVING: Happens before default remove type or custom remove
  • PassConfig.TYPE_REMOVE: Overrides default remove type
  • PassConfig.TYPE_AFTER_REMOVING: Happens after default remove or custom remove

Set a proper logger service to avoid console output

By default, we are using console.warn to output some deprecating and other client messages.

Add a parameter in the ContainerBuilder constructor to use the sent logger instance instead of the default console service.

Ex. using logger setter

import winston from 'winston'

let container = new ContainerBuilder()
container.logger = winston

Your logger instance MUST implement warn method

Example made with WinstonJS but feel free to use your desired logger library or your own logger service

Ignoring Missing Dependencies

Ignoring Missing Dependencies

The behavior of ignoring missing dependencies is the same as the "null" behavior except when used within a method call, in which case the method call itself will be removed.

In the following example the container will inject a service using a method call if the service exists and remove the method call if it does not:

YAML
services:
    app.newsletter_manager:
        class:     ./App/Newsletter/NewsletterManager
        calls:
            - [setMailer, ['@?app.mailer']]
JSON
{
	"services": {
		"app.newsletter_manager": {
			"class": "./App/Newsletter/NewsletterManager",
			"calls": [
				[
					"setMailer",
					[
						"@?app.mailer"
					]
				]
			]
		}
	}
}

In YAML or JSON, the special @? syntax tells the service container that the dependency is optional. Of course, the NewsletterManager must also be rewritten by adding a setMailer() method:

class NewsletterManager { 
    /*
     * @param {Mailer|null} mailer
     */
    setMailer(mailer = null) {
        // ...
    }
}

Preventing compiling an already compiled container

For example

class FooPass {
  process (container) {
    // this will be called twice!
  }
}

container.addCompilerPass(new FooPass())

// Acting
container.compile()
container.compile()

Prevent calling the process compiler class method twice

Property Injection

Property Injection

Another possibility is just setting public fields of the class directly:

class NewsletterManager {
     /**
      * @param {Mailer} mailer
      */
     set mailer (value) {
          this._mailer = value
     }
}
services:
     # ...

     app.newsletter_manager:
         class: ./App/Mail/NewsletterManager
         properties:
             mailer: '@mailer'

Inject Instances into the Container

In some applications, you may need to inject a class instance as service, instead of configuring the container to create a new instance.
Services that are set at runtime are called synthetic services.
This service has to be configured in the container, so the container knows the service does exist during compilation, otherwise service will get a service does not exist error.

In order to do so, mark the service as synthetic in your service definition configuration:

JS
container
  .register('app.synthetic_service')
  .synthetic = true
YAML
services:
  app.synthetic_service:
    synthetic: true

Now, you can inject the instance in the container using:

let someInstance = new SomeCoolInstance(someArgument)
container.set('app.synthetic_service', someInstance)

Compiler Pass

Creating Separate Compiler Passes

Register compiler pass from the ContainerBuilder

class CustomPass {
    /**
     * @param {ContainerBuilder} container
     */
    process (container) {
       // ... do something during the compilation
    }
}

containerBuilder.addCompilerPass(new CustomPass())
containerBuilder.compile()

Be able to change the container behaviour before compiling it

Container CLI

Use https://github.com/tj/commander.js to build the CLI

  1. Build the container CLI skeleton
  2. Add on to the package.json our bin configuration

You can find out what services are registered with the container using the console. To show all services and the class for each service, run:

$ ndi --config=/path/to/services.yml

By default, only public services are shown, but you can also view private services:

$ ndi --config=/path/to/services.yml --show-private

You can get more detailed information about a particular service by specifying its id:

$ ndi --config=/path/to/services.yml app.mailer

Order when controlling the Pass Ordering

blocked by #32

You can also control the order in which compiler passes are run for each compilation phase. Use the optional third argument of addCompilerPass() to set the priority as an integer number. The default priority is 0 and the higher its value, the earlier it's executed:

// FirstPass is executed after SecondPass because its priority is lower
container.addCompilerPass(new FirstPass(), PassConfig.TYPE_AFTER_REMOVING, 10)
container.addCompilerPass(new SecondPass(), PassConfig.TYPE_AFTER_REMOVING, 30)

Controlling the pass ordering

The optimisation passes run first and include tasks such as resolving references within the definitions. When registering compiler passes using addCompilerPass(), you can configure when your compiler pass is run. By default, they are run before the optimisation passes.

You can use the following constants to determine when your pass is executed:

  • PassConfig.TYPE_BEFORE_OPTIMIZATION
  • PassConfig.TYPE_OPTIMIZE

For example, to run your custom pass after the default removal passes have been run, use:

import {PassConfig} from 'node-dependency-injection'

container.addCompilerPass(new CustomPass(), PassConfig.TYPE_OPTIMIZE)

The default pass config type is PassConfig.TYPE_BEFORE_OPTIMIZATION
PassConfig.TYPE_OPTIMIZE will override the actual container optimization on compile

Marking Services as Public / Private

When defining services, you'll usually want to be able to access these definitions within your application code. These services are called public. This means that you can fetch it from the container using the get() method:

let publicManager = container.get('public_manager')

In some cases, a service only exists to be injected into another service and is not intended to be fetched directly from the container as shown above.

In these cases, to get a minor performance boost and ensure the service will not be retrieved directly from the container, you can set the service to be not public (i.e. private):

import {ContainerBuilder} from 'node-dependency-injection'
import SomeManager from './SomeManager'

let container = new ContainerBuilder()
let definition = container.register('some_manager') // Returns a new Definition instance
definition.public = false
With Configuration files
YAML
services:
    some_manager:
        class: ./SomeManager
        public: false
JSON
{
  "services": {
    "some_manager": {
      "class": "./SomeManager",
      "public": false
    }
  }
}

Finally if we do

let someManager = container.get('some_manager')

Now that the service is private, you should not fetch the service directly from the container. This will throw an exception The service {service name} is private

Services are by default public, but it's considered a good practice to mark as more services private as possible.

Support PackageReference syntax as class in Definition

Usage:

For example, I want to declare a redis client from ioredis package as a service.

I expect to be able to use the PackageReference syntax (i.e. "class": "%ioredis") to use it instead of having to walk to the class file in node_modules folder (i.e. "class": "/path/to/node_modules/ioredis/lib/index").

Using PackageReference syntax produce an error because the loader try to require the file from the current folder.

Proposal:

Update the method FileLoader._requireClassNameFromPath to handle PackageReference syntax.

I will work on a PR for this and will push it on 1.9 branch.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.