Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 102 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,107 @@ I am answering this question from a framework creator perspective. I never use t

So, if you create packages for AdonisJS, I highly recommend using factory functions. Leave the `@inject` decorator for the end user.

## Conditional bindings

Use `container.if` to select a binding at resolution time. The condition may be synchronous or asynchronous, and it is evaluated every time the binding is resolved.

For example: You are rolling out a new payment gateway to a small set of users. Every class asking for `PaymentGateway` keeps doing so, and the container decides which implementation they get.

```ts
container.bind(PaymentGateway, (resolver) => {
return resolver.make(StripePaymentGateway)
})

container
.if(async (resolver) => {
const ctx = await resolver.make(HttpContext)
return ctx.auth.user?.featureFlags.includes('new-payment') === true
})
.bind(PaymentGateway, (resolver) => resolver.make(BetaPaymentGateway))
```

You may register multiple conditional bindings for the same key. Conditions are evaluated in registration order and the first match is used. When none match, the container falls back to a regular binding or its default resolution behavior.

### Conditional singletons

The builder also exposes a `singleton` method. Just like `container.singleton`, the factory function is called only once and its return value is cached forever.

The condition is still evaluated on every resolution. Only the factory function result is cached.

```ts
container
.if(() => env.get('SEARCH_DRIVER') === 'typesense')
.singleton(SearchService, (resolver) => resolver.make(TypesenseSearchService))
```

### Condition arguments

A condition receives the resolver performing the resolution, the runtime values, and the parent class asking for the binding.

```ts
container.if((resolver, runtimeValues, parent) => true)
```

The `resolver` gives the condition access to values local to that resolver. In the example at the top of this section, `HttpContext` is available because the AdonisJS starter kits bind it per request using `ContainerBindingsMiddleware`.

The `parent` is the class the binding is getting injected into, or `null` when the binding is resolved directly using `make`. Use it to combine a predicate with the calling class.

```ts
container
.if((_, __, parent) => parent === CheckoutController && isBetaUser())
.bind(PaymentGateway, (resolver) => resolver.make(BetaPaymentGateway))
```

The `runtimeValues` are only available when the binding is resolved directly using `make` or `call`. Dependencies resolved for a class receive `undefined`.

### Conditions run everywhere

A condition is evaluated for every resolution of its binding key, not just during HTTP requests. The same binding is also resolved inside queue workers, Ace commands, and at boot time, where request specific values do not exist.

Guard against their absence, otherwise the condition throws when it reaches for a binding that was never registered.

```ts
container
.if(async (resolver) => {
if (!resolver.hasBinding(HttpContext)) {
return false
}

const ctx = await resolver.make(HttpContext)
return ctx.auth.user?.featureFlags.includes('new-payment') === true
})
.bind(PaymentGateway, (resolver) => resolver.make(BetaPaymentGateway))
```

### Resolution order

The container tries the following sources in order and returns the first one that produces a value.

| Order | Source | Registered using | Applies to |
| ----- | -------------------- | ---------------------------------------- | --------------------- |
| 1 | Swaps | `container.swap` | Classes only |
| 2 | Contextual bindings | `container.when().asksFor().provide()` | Classes with a parent |
| 3 | Resolver values | `resolver.bindValue` | All binding keys |
| 4 | Container values | `container.bindValue` | All binding keys |
| 5 | Conditional bindings | `container.if().bind()` | All binding keys |
| 6 | Bindings | `container.bind` / `container.singleton` | All binding keys |
| 7 | Class construction | — | Classes only |

Two consequences worth knowing. Swaps win over conditional bindings, so faking a class in tests works regardless of any condition. And values win over conditional bindings, so `bindValue` overrides a conditional binding the same way it overrides a regular one.

### Conditional bindings and `hasBinding`

`container.hasBinding` returns `true` for a key that only has conditional bindings registered, because the binding genuinely is registered. It may still fail to resolve when none of the conditions match.

```ts
container.if(() => false).bind('gateway', () => new BetaPaymentGateway())

container.hasBinding('gateway') // true
await container.make('gateway') // throws
```

The error points at the unmatched conditions, so register a fallback using `container.bind` when the key must always resolve.

## Binding singletons

You can bind a singleton to the container using the `container.singleton` method. It is the same as the `container.bind` method, except the factory function is called only once, and the return value is cached forever.
Expand Down Expand Up @@ -436,7 +537,7 @@ This is where the `@bind` decorator comes into the picture. To perform database

If you are using the container inside a TypeScript project, then you can define the types for all the bindings in advance at the time of creating the container instance.

Defining types will ensure the `bind`, `singleton` and `bindValue` method accepts only the known bindings and assert their types as well.
Defining types will ensure the `bind`, `singleton`, `bindValue` and `if().bind()` methods accept only the known bindings and assert their types as well.

```ts
class Route {}
Expand Down
136 changes: 136 additions & 0 deletions src/conditional_bindings_builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* @adonisjs/fold
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import type { AbstractConstructor } from '@poppinss/utils/types'

import type { Container } from './container.ts'
import type { BindingCondition, BindingKey, BindingResolver } from './types.ts'

/**
* A fluent builder to register conditional bindings with the
* container.
*
* Bindings registered using this builder are only used when the condition
* returns true. The condition is evaluated on every resolution.
*/
export class ConditionalBindingsBuilder<KnownBindings extends Record<any, any>> {
/**
* The condition deciding whether the registered bindings
* should be used
*/
#condition: BindingCondition<KnownBindings>

/**
* Container instance for registering the conditional
* bindings
*/
#container: Container<KnownBindings>

/**
* Initialize the conditional bindings builder
*
* @param condition - Predicate deciding whether the bindings should be used
* @param container - The container instance to register bindings with
*/
constructor(condition: BindingCondition<KnownBindings>, container: Container<KnownBindings>) {
this.#condition = condition
this.#container = container
}

/**
* Register a binding to use when the condition returns true. The factory
* function is called on every resolution.
*
* @param binding - The binding key (string, symbol, or class constructor)
* @param resolver - Factory function that resolves the binding value
*
* @example
* ```ts
* container
* .if((resolver) => resolver.make(FeatureFlags).isEnabled('new-payment'))
* .bind(PaymentGateway, (resolver) => resolver.make(BetaPaymentGateway))
* ```
*/
bind<Binding extends keyof KnownBindings>(
/**
* Need to narrow down the "Binding" for the case where "KnownBindings" are <any, any>
*/
binding: Binding extends string | symbol ? Binding : never,
resolver: BindingResolver<KnownBindings, KnownBindings[Binding]>
): void
bind<Binding extends AbstractConstructor<any>>(
binding: Binding,
resolver: BindingResolver<KnownBindings, InstanceType<Binding>>
): void
bind<Binding>(
binding: Binding,
resolver: BindingResolver<
KnownBindings,
Binding extends AbstractConstructor<infer A>
? A
: Binding extends keyof KnownBindings
? KnownBindings[Binding]
: never
>
): void {
this.#container.conditionalBinding(
this.#condition,
binding as BindingKey,
resolver as BindingResolver<KnownBindings, any>,
false
)
}

/**
* Register a singleton to use when the condition returns true. The factory
* function is called only once and the return value is cached forever.
*
* The condition is still evaluated on every resolution. Only the factory
* function result is cached.
*
* @param binding - The binding key (string, symbol, or class constructor)
* @param resolver - Factory function that resolves the binding value
*
* @example
* ```ts
* container
* .if(() => env.get('SEARCH_DRIVER') === 'typesense')
* .singleton(SearchService, (resolver) => resolver.make(TypesenseSearchService))
* ```
*/
singleton<Binding extends keyof KnownBindings>(
/**
* Need to narrow down the "Binding" for the case where "KnownBindings" are <any, any>
*/
binding: Binding extends string | symbol ? Binding : never,
resolver: BindingResolver<KnownBindings, KnownBindings[Binding]>
): void
singleton<Binding extends AbstractConstructor<any>>(
binding: Binding,
resolver: BindingResolver<KnownBindings, InstanceType<Binding>>
): void
singleton<Binding>(
binding: Binding,
resolver: BindingResolver<
KnownBindings,
Binding extends AbstractConstructor<infer A>
? A
: Binding extends keyof KnownBindings
? KnownBindings[Binding]
: never
>
): void {
this.#container.conditionalBinding(
this.#condition,
binding as BindingKey,
resolver as BindingResolver<KnownBindings, any>,
true
)
}
}
Loading
Loading