-
Notifications
You must be signed in to change notification settings - Fork 34
Autowire
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.
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
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.
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.
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();
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.
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.
Copyright © 2023-2024 Mauro Gadaleta