UNPKG

21.9 kBMarkdownView Raw
1# AdonisJS Fold
2
3> Simplest, straightforward implementation for IoC container in JavaScript
4
5<br />
6
7[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url]
8
9## Why this project exists?
10
11Many existing implementations of IoC containers take the concept too far and start to feel more like Java. JavaScript inherently does not have all the bells and whistles; you need to have similar IoC container benefits as PHP or Java.
12
13Therefore, with this project, I live to the ethos of JavaScript and yet build a container that can help you create loosely coupled systems.
14
15I have explained the [reasons for using an IoC container](https://github.com/thetutlage/meta/discussions/4) in this post. It might be a great idea to read the post first ✌️
16
17> **Note**: AdonisJS fold is highly inspired by the Laravel IoC container. Thanks to Taylor for imaginging such a simple, yet powerful API.
18
19## Goals of the project
20
21- **Keep the code visually pleasing**. If you have used any other implementation of an IoC container, you will automatically find `@adonisjs/fold` easy to read and follow.
22- **Keep it simple**. JavaScript projects have a few reasons for using an IoC container, so do not build features that no one will ever use or understand.
23- **Build it for JavaScript and improve with TypeScript** - The implementation of `@adonisjs/fold` works with vanilla JavaScript. It's just you have to write less code when using TypeScript. Thanks to its decorators metadata API.
24
25## Usage
26
27Install the package from the npm packages registry.
28
29```sh
30npm i @adonisjs/fold
31```
32
33Once done, you can import the `Container` class from the package and create an instance of it. For the most part, you will use a single instance of the container.
34
35```ts
36import { Container } from '@adonisjs/fold'
37
38const container = new Container()
39```
40
41## Making classes
42
43You can construct an instance of a class by calling the `container.make` method. The method is asynchronous since it allows for lazy loading dependencies via factory functions (More on factory functions later).
44
45```ts
46class UserService {}
47
48const service = await container.make(UserService)
49assert(service instanceof UserService)
50```
51
52In the previous example, the `UserService` did not have any dependencies; therefore, it was straightforward for the container to make an instance of it.
53
54Now, let's look at an example where the `UserService` needs an instance of the Database class.
55
56```ts
57class Database {}
58
59class UserService {
60 static containerInjections = {
61 _constructor: {
62 dependencies: [Database],
63 },
64 }
65
66 constructor(db) {
67 this.db = db
68 }
69}
70
71const service = await container.make(UserService)
72assert(service.db instanceof Database)
73```
74
75The `static containerInjections` property is required by the container to know which values to inject when creating an instance of the class.
76
77This property can define the dependencies for the class methods (including the constructor). The dependencies are defined as an array. The dependencies are injected in the same order as they are defined inside the array.
78
79> **Do you remember?** I said that JavaScript is not as powerful as Java or PHP. This is a classic example of that. In other languages, you can use reflection to look up the classes to inject, whereas, in JavaScript, you have to tell the container explicitly.
80
81### TypeScript to the rescue
82
83Wait, you can use decorators with combination of TypeScript's [emitDecoratorMetaData](https://www.typescriptlang.org/tsconfig#emitDecoratorMetadata) option to perform reflection. You will also need to install [`reflect-metadata`](https://www.npmjs.com/package/reflect-metadata) in order for TypeScript to extract metadata from your classes.
84
85It is worth noting, TypeScript decorators are not as powerful as the reflection API in other languages. For example, in PHP, you can use interfaces for reflection. Whereas in TypeScript, you cannot.
86
87With that said, let's look at the previous example, but in TypeScript this time.
88
89```ts
90import { inject } from '@adonisjs/fold'
91
92class Database {}
93
94@inject()
95class UserService {
96 constructor(db: Database) {
97 this.db = db
98 }
99}
100
101const service = await container.make(UserService)
102assert(service.db instanceof Database)
103```
104
105The `@inject` decorator looks at the types of all the constructor parameters and defines the `static containerInjections` property behind the scenes.
106
107> **Note**: The decorator-based reflection can only work with concrete values, not with interfaces or types since they are removed during the runtime.
108
109## Making class with runtime values
110
111When calling the `container.make` method, you can pass runtime values that take precedence over the `containerInjections` array.
112
113In the following example, the `UserService` accepts an instance of the ongoing HTTP request as the 2nd param. Now, when making an instance of this class, you can pass that instance manually.
114
115```ts
116import { inject } from '@adonisjs/fold'
117import { Request } from '@adonisjs/core/src/Request'
118
119class Database {}
120
121@inject()
122class UserService {
123 constructor(db: Database, request: Request) {
124 this.db = db
125 this.request = request
126 }
127}
128```
129
130```ts
131createServer((req) => {
132 const runtimeValues = [undefined, req]
133
134 const service = await container.make(UserService, runtimeValues)
135 assert(service.request === req)
136})
137```
138
139In the above example:
140
141- The container will create an instance of the `Database` class since it is set to `undefined` inside the runtime values array.
142- However, for the second position (ie `request`), the container will use the `req` value.
143
144## Calling methods
145
146You can also call class methods to look up/inject dependencies automatically.
147
148In the following example, the `UserService.find` method needs an instance of the Database class. The `container.call` method will look at the `containerInjections` property to find the values to inject.
149
150```ts
151class Database {}
152
153class UserService {
154 static containerInjections = {
155 find: {
156 dependencies: [Database],
157 },
158 }
159
160 async find(db) {
161 await db.select('*').from('users')
162 }
163}
164
165const service = await container.make(UserService)
166await container.call(service, 'find')
167```
168
169The TypeScript projects can re-use the same `@inject` decorator.
170
171```ts
172class Database {}
173
174class UserService {
175 @inject()
176 async find(db: Database) {
177 await db.select('*').from('users')
178 }
179}
180
181const service = await container.make(UserService)
182await container.call(service, 'find')
183```
184
185The **runtime values** are also supported with the `container.call` method.
186
187## Container bindings
188
189Alongside making class instances, you can also register bindings inside the container. Bindings are simple key-value pairs.
190
191- The key can either be a `string`, a `symbol` or a `class constructor`.
192- The value is a factory function invoked when someone resolves the binding from the container.
193
194```ts
195const container = new Container()
196
197container.bind('db', () => {
198 return new Database()
199})
200
201const db = await container.make('db')
202assert(db instanceof Database)
203```
204
205Following is an example of binding the class constructor to the container and self constructing an instance of it using the factory function.
206
207```ts
208container.bind(Database, () => {
209 return new Database()
210})
211```
212
213### Factory function arguments
214
215The factory receives the following three arguments.
216
217- The `resolver` reference. Resolver is something container uses under the hood to resolve dependencies. The same instance is passed to the factory, so that you can resolve dependencies to construct the class.
218- An optional array of runtime values defined during the `container.make` call.
219
220```ts
221container.bind(Database, (resolver, runtimeValues) => {
222 return new Database()
223})
224```
225
226### When to use the factory functions?
227
228I am answering this question from a framework creator perspective. I never use the `@inject` decorator on my classes shipped as packages. Instead, I define their construction logic using factory functions and keep classes free from any knowledge of the container.
229
230So, if you create packages for AdonisJS, I highly recommend using factory functions. Leave the `@inject` decorator for the end user.
231
232## Binding singletons
233
234You 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.
235
236```ts
237container.singleton(Database, () => {
238 return new Database()
239})
240```
241
242## Binding values
243
244Along side the factory functions, you can also bind direct values to the container.
245
246```ts
247container.bindValue('router', router)
248```
249
250The values are given priority over the factory functions. So, if you register a value with the same name as the factory function binding, the value will be resolved from the container.
251
252The values can also be registered at the resolver level. In the following example, the `Request` binding only exists for an isolated instance of the resolver and not for the entire container.
253
254```ts
255const resolver = container.createResolver()
256resolver.bindValue(Request, req)
257
258await resolve.make(SomeClass)
259```
260
261## Aliases
262
263Container aliases allows defining aliases for an existing binding. The alias should be either a `string` or a `symbol`.
264
265```ts
266container.singleton(Database, () => {
267 return new Database()
268})
269
270container.alias('db', Database)
271
272/**
273 * Make using the alias
274 */
275const db = await container.make('db')
276assert.instanceOf(db, Database)
277```
278
279## Contextual bindings
280
281Contextual bindings allows you to register custom dependency resolvers on a given class for a specific dependency. You will be mostly using contextual bindings with driver based implementations.
282
283For example: You have a `UserService` and a `BlogService` and both of them needs an instance of the Drive disk to write and read files. You want the `UserService` to use the local disk driver and `BlogService` to use the s3 disk driver.
284
285> **Note**
286> Contextual bindings can be defined for class constructors and not for container bindngs
287
288```ts
289import { Disk } from '@adonisjs/core/driver'
290
291class UserService {
292 constructor(disk: Disk) {}
293}
294```
295
296```ts
297import { Disk } from '@adonisjs/core/driver'
298
299class BlogService {
300 constructor(disk: Disk) {}
301}
302```
303
304Now, let's use contextual bindings to tell the container that when `UserService` needs the `Disk` class, provide it the local driver disk.
305
306```ts
307container
308 .when(BlogService)
309 .asksFor(Disk)
310 .provide(() => drive.use('s3'))
311
312container
313 .when(UserService)
314 .asksFor(Disk)
315 .provide(() => drive.use('local'))
316```
317
318## Swapping implementations
319
320When using the container to resolve a tree of dependencies, quite often you will have no control over the construction of a class and therefore you will be not able to swap/fake its dependencies when writing tests.
321
322In the following example, the `UsersController` needs an instance of the `UserService` class.
323
324```ts
325@inject()
326class UsersController {
327 constructor(service: UserService) {}
328}
329```
330
331In the following test, we are making an HTTP request that will be handled by the `UsersController`. However, within the test, we have no control over the construction of the controller class.
332
333```ts
334test('get all users', async ({ client }) => {
335 // I WANTED TO FAKE USER SERVICE FIRST?
336 const response = await client.get('users')
337})
338```
339
340To make things simpler, you can tell the container to use a swapped implementation for a given class constructor as follows.
341
342```ts
343test('get all users', async ({ client }) => {
344 class MyFakedService extends UserService {}
345
346 /**
347 * From now on, the container will return an instance
348 * of `MyFakedService`.
349 */
350 container.swap(UserService, () => new MyFakedService())
351
352 const response = await client.get('users')
353})
354```
355
356## Observing container
357
358You can pass an instance of the [EventEmitter](https://nodejs.org/dist/latest-v18.x/docs/api/events.html#class-eventemitter) or [emittery](https://github.com/sindresorhus/emittery) to listen for events as container resolves dependencies.
359
360```ts
361import { EventEmitter } from 'node:events'
362const emitter = new EventEmitter()
363
364emitter.on('container:resolved', ({ value, binding }) => {
365 // value is the resolved value
366 // binding name can be a mix of string, class constructor, or a symbol.
367})
368
369const container = new Container({ emitter })
370```
371
372## Container hooks
373
374You can use container hooks when you want to modify a resolved value before it is returned from the `make` method.
375
376- The hook is called everytime a binding is resolved from the container.
377- It is called only once for the singleton bindings.
378- The hook gets called everytime you construct an instance of a class by passing the class constructor directly.
379
380> **Note**: The hook callback can also be an async function
381
382```ts
383container.resolving(Validator, (validator) => {
384 validate.rule('email', function () {})
385})
386```
387
388## Container providers
389
390Container providers are static functions that can live on a class to resolve the dependencies for the class constructor or a given class method.
391
392Once, you define the `containerProvider` on the class, the IoC container will rely on it for resolving dependencies and will not use the default provider.
393
394```ts
395import { ContainerResolver } from '@adonisjs/fold'
396import { ContainerProvider } from '@adonisjs/fold/types'
397
398class UsersController {
399 static containerProvider: ContainerProvider = (
400 binding,
401 property,
402 resolver,
403 defaultProvider,
404 runtimeValues
405 ) => {
406 console.log(binding === UserService)
407 console.log(this === UserService)
408 return defaultProvider(binding, property, resolver, runtimeValues)
409 }
410}
411```
412
413### Why would I use custom providers?
414
415Custom providers can be handy when creating an instance of the class is not enough to construct it properly.
416
417Let's take an example of [AdonisJS route model binding](https://github.com/adonisjs/route-model-binding). With route model binding, you can query the database using models based on the value of a route parameter and inject the model instance inside the controller.
418
419```ts
420import User from '#models/User'
421import { bind } from '@adonisjs/route-model-binding'
422
423class UsersController {
424 @bind()
425 public show(_, user: User) {}
426}
427```
428
429Now, if you use the `@inject` decorator to resolve the `User` model, then the container will only create an instance of User and give it back to you.
430
431However, in this case, we want more than just creating an instance of the model. We want to look up the database and create an instance with the row values.
432
433This is where the `@bind` decorator comes into the picture. To perform database lookups, it registers a custom provider on the `UsersController` class.
434
435## Binding types
436
437If 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.
438
439Defining types will ensure the `bind`, `singleton` and `bindValue` method accepts only the known bindings and assert their types as well.
440
441```ts
442class Route {}
443class Databse {}
444
445type ContainerBindings = {
446 route: Route
447 db: Database
448}
449
450const container = new Container<ContainerBindings>()
451
452// Fully typed
453container.bind('route', () => new Route())
454container.bind('db', () => new Database())
455
456// Fully typed - db: Database
457const db = await container.make('db')
458```
459
460## Common errors
461
462### Cannot inject "xxxxx" in "[class: xxxxx]". The value cannot be constructed
463
464The error occurs, when you are trying to inject a value that cannot be constructed. A common source of issue is within TypeScript project, when using an `interface` or a `type` for dependency injection.
465
466In the following example, the `User` is a TypeScript type and there is no way for the container to construct a runtime value from this type (types are removed after transpiling the TypeScript code).
467
468Therefore, the container will raise an exception saying `Cannot inject "[Function: Object]" in "[class: UsersController]". The value cannot be constructed`.
469
470```ts
471type User = {
472 username: string
473 age: number
474 email: string
475}
476
477@inject()
478class UsersController {
479 constructor(user: User)
480}
481```
482
483## Module expressions
484
485In AdonisJS, we allow binding methods in form of string based module expression. For example:
486
487**Instead of importing and using a controller as follows**
488
489```ts
490import UsersController from '#controllers/users'
491
492Route.get('users', (ctx) => {
493 return new UsersController().index(ctx)
494})
495```
496
497**You can bind the controller method as follows**
498
499```ts
500Route.get('users', '#controllers/users.index')
501```
502
503**Why do we do this?**
504There are a couple of reasons for using module expressions.
505
506- Performance: Lazy loading controllers ensures that we keep the application boot process quick. Otherwise, we will pull all the controllers and their imports within a single routes file and that will surely impact the boot time of the application.
507- Visual clutter: Imagine importing all the controllers within a single routes file (or maybe 2-3 different route files) and then instantiating them manually. This surely brings some visual clutter in your codebase (agree visual clutter is subjective).
508
509So given we use **module expressions** widely in the AdonisJS ecosystem. We have abstracted the logic of parsing string based expressions into dedicated helpers to re-use and ease.
510
511### Assumptions
512
513There is a strong assumption that every module references using module expression will have a `export default` exporting a class.
514
515```ts
516// Valid module for module expression
517export default class UsersController {}
518```
519
520### toCallable
521
522The `toCallable` method returns a function that internally parses the module string expression and returns a function that you can invoke like any other JavaScript function.
523
524```ts
525import { moduleExpression } from '@adonisjs/fold'
526
527const fn = moduleExpression('#controllers/users.index', import.meta.url).toCallable()
528
529// Later call it
530const container = new Container()
531const resolver = container.createResolver()
532await fn(resolver, [ctx])
533```
534
535You can also pass the container instance at the time of creating the callable function.
536
537```ts
538const container = new Container()
539const fn = moduleExpression('#controllers/users.index', import.meta.url).toCallable(container)
540
541// Later call it
542await fn([ctx])
543```
544
545### toHandleMethod
546
547The `toHandleMethod` method returns an object with the `handle` method. To the main difference between `toCallable` and `toHandleMethod` is their return output
548
549- `toHandleMethod` returns `{ handle: fn }`
550- `toCallable` returns `fn`
551
552```ts
553import { moduleExpression } from '@adonisjs/fold'
554
555const handler = moduleExpression('#controllers/users.index', import.meta.url).toHandleMethod()
556
557// Later call it
558const container = new Container()
559const resolver = container.createResolver()
560await handler.handle(resolver, [ctx])
561```
562
563You can also pass the container instance at the time of creating the handle method.
564
565```ts
566const container = new Container()
567
568const handler = moduleExpression('#controllers/users.index', import.meta.url).toHandleMethod(
569 container
570)
571
572// Later call it
573await handler.handle([ctx])
574```
575
576### Bechmarks
577
578Following are benchmarks to see the performance loss that happens when using module expressions.
579
580**Benchmarks were performed on Apple M1 iMac, 16GB**
581
582![](./benchmarks.png)
583
584- `handler`: Calling the handle method on the output of `toHandleMethod`.
585- `callable`: Calling the function returned by the `toCallable` method.
586- `native`: Using dynamic imports to lazily import the module. This variation does not use any helpers from this package.
587- `inline`: When no lazy loading was performed. The module was importing inline using the `import` keyword.
588
589## Module importer
590
591The module importer is similar to module expression. However, instead of defining the import path as a string, you have to define a function that imports the module.
592
593```ts
594import { moduleImporter } from '@adonisjs/fold'
595
596const fn = moduleImporter(
597 () => import('#middleware/auth')
598 'handle'
599).toCallable()
600
601// Later call it
602const container = new Container()
603const resolver = container.createResolver()
604await fn(resolver, [ctx])
605```
606
607Create handle method object
608
609```ts
610import { moduleImporter } from '@adonisjs/fold'
611
612const handler = moduleImporter(
613 () => import('#middleware/auth')
614 'handle'
615).toHandleMethod()
616
617// Later call it
618const container = new Container()
619const resolver = container.createResolver()
620await handler.handle(resolver, [ctx])
621```
622
623## Module caller
624
625The module caller is similar to module importer. However, instead of lazy loading a class, you pass the class constructor to this method.
626
627```ts
628import { moduleCaller } from '@adonisjs/fold'
629
630class AuthMiddleware {
631 handle() {}
632}
633
634const fn = moduleCaller(AuthMiddleware, 'handle').toCallable()
635
636// Later call it
637const container = new Container()
638const resolver = container.createResolver()
639await fn(resolver, [ctx])
640```
641
642Create handle method object
643
644```ts
645import { moduleCaller } from '@adonisjs/fold'
646
647class AuthMiddleware {
648 handle() {}
649}
650
651const handler = moduleImporter(AuthMiddleware, 'handle').toHandleMethod()
652
653// Later call it
654const container = new Container()
655const resolver = container.createResolver()
656await handler.handle(resolver, [ctx])
657```
658
659[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/fold/checks.yml?style=for-the-badge
660[gh-workflow-url]: https://github.com/adonisjs/fold/actions/workflows/checks.yml 'Github action'
661[npm-image]: https://img.shields.io/npm/v/@adonisjs/fold/latest.svg?style=for-the-badge&logo=npm
662[npm-url]: https://www.npmjs.com/package/@adonisjs/fold/v/latest 'npm'
663[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
664[license-url]: LICENSE.md
665[license-image]: https://img.shields.io/github/license/adonisjs/fold?style=for-the-badge