Skip to content

Autowire

Mauro Gadaleta edited this page Oct 1, 2024 · 5 revisions

Defining Services Dependencies Automatically (Autowiring)

Only available for TypeScript

Autowiring simplifies the process of managing service dependencies within the container by minimizing configuration. It leverages TypeScript's type annotations to automatically inject the appropriate services into class constructors. Node Dependency Injection's autowiring feature is designed for predictability: if the system cannot clearly determine which dependency to inject, it raises an actionable exception to guide you.

By utilizing a compiled container, autowiring introduces no runtime performance overhead.

⚠️ Currently, autowiring only supports private constructor properties.

Example: Autowiring in Action

Let's start by creating a class to perform Base64 transformations:

export default class Base64Transformer
{
    public transform(value: string): string
    {
        return Buffer.from(value).toString('base64');
    }
}

Now, create a client class that uses this transformer:

export default class SomeClient
{
    constructor(
      private readonly transformer: Base64Transformer,
    ) {}

    async execute(value: string): Promise<void>
    {
        const transformedString = this.transformer.transform(value);
        // Further logic...
    }
}

If you're using the default services.yaml configuration, both Base64Transformer and SomeClient are automatically registered as services, ready for autowiring. This means you can use them without any additional configuration.

To understand the configuration behind this, here's how you could explicitly set it up:

# config/services.yaml
services:
    _defaults:
        autowire: true
        rootDir: ../src  # Set this relative to your file path

Autowiring with Interfaces

When you need to type-hint interfaces instead of concrete classes, autowiring still works seamlessly. This is useful when you want to decouple your code by programming to abstractions.

For example, let's define an interface for the transformer:

export default interface Transformer
{
    transform(value: string): string;
}

Next, update Base64Transformer to implement the Transformer interface:

export default class Base64Transformer implements Transformer
{
    transform(value: string): string
    {
        return Buffer.from(value).toString('base64');
    }
}

Now, modify SomeClient to depend on the interface instead of the concrete class:

export default class SomeClient
{
    constructor(
      private readonly transformer: Transformer,
    ) {}

    async execute(value: string): Promise<void>
    {
        const transformedString = this.transformer.transform(value);
        // Further logic...
    }
}

This allows for more flexibility, enabling you to swap out implementations of Transformer without changing the client.

Overriding Arguments with Autowiring

Sometimes, you may want to override certain arguments when autowiring services. You can achieve this through the service definition in services.yaml:

services:
    _defaults:
        autowire: true
        rootDir: ../src

    App.FooBarAutowireOverride:
        class: './FooBarAutowireOverride'
        override_arguments:
            - '@CiAdapter'
            - '@SomeService'
            - '@AnotherService'

    App.AnotherFooBarAutowireOverride:
        class: './AnotherFooBarAutowireOverride'
        override_arguments:
            - '@BarAdapter'
            - '@SomeService'
            - '@AnotherService'

In this case, the override_arguments directive specifies which services to inject, allowing you to replace dependencies selectively.

Dumping a Service File from Autowiring

When transpiling TypeScript, you may need to dump a service configuration file, which can be loaded in a production environment. Here's an example of how to do this:

import { ContainerBuilder, Autowire, ServiceFile } from 'node-dependency-injection';

const container = new ContainerBuilder(false, '/path/to/src');
const autowire = new Autowire(container);
autowire.serviceFile = new ServiceFile('/some/path/to/dist/services.yaml');
await autowire.process();
await container.compile();

Loading the Configuration in Production

To handle different environments, like development vs. production, here's a suggestion for loading the correct configuration file:

if (process.env.NODE_ENV === 'dev') {
  this._container = new ContainerBuilder(false, '/src');
  this._autowire = new Autowire(this._container);
  this._autowire.serviceFile = new ServiceFile('/some/path/to/dist/services.yaml');
  await this._autowire.process();
} else {
  this._container = new ContainerBuilder(false, '/dist');
  this._loader = new YamlFileLoader(this._container);
  await this._loader.load('/some/path/to/dist/services.yaml');
}
await this._container.compile();

This setup ensures that in production, the container loads from the transpiled dist folder, while in development, it uses the source directory.

Excluding Folders from the Root Directory

In some cases, you might want to exclude specific folders from autowiring. Here's how you can configure that:

# /path/to/services.yml
services:
  _defaults:
    autowire: true
    rootDir: "../path/to/src"
    exclude: ["ToExclude"]

In this example, the folder ../path/to/src/ToExclude is excluded from the container, preventing it from being scanned for services.

Clone this wiki locally