コントローラ

コントローラ

HTTPコントローラは、ルートハンドラーを専用のファイル内で整理するための抽象化レイヤーを提供します。ルートファイル内ですべてのリクエスト処理ロジックを表現する代わりに、コントローラクラスに移動します。

コントローラは、./app/controllersディレクトリ内に格納され、各コントローラをプレーンなJavaScriptクラスとして表現します。次のコマンドを実行して新しいコントローラを作成できます。

参照も: コントローラ作成コマンド

node ace make:controller users

新しく作成されたコントローラは、class宣言でscaffoldされ、手動でメソッドを作成することもできます。この例では、indexメソッドを作成し、ユーザーの配列を返します。

import type { HttpContext } from '@adonisjs/core/http'
export default class UsersController {
async index(ctx: HttpContext) {
return [
{
id: 1,
username: 'virk',
},
{
id: 2,
username: 'romain',
},
]
}
}

最後に、このコントローラをルートにバインドしましょう。#controllersエイリアスを使用してコントローラをインポートします。エイリアスは、Node.jsのサブパスインポート機能を使用して定義されます。

start/routes.ts
import router from '@adonisjs/core/services/router'
const UsersController = () => import('#controllers/users_controller')
router.get('users', [UsersController, 'index'])

お気づきかもしれませんが、コントローラクラスのインスタンスを作成せずに、直接ルートに渡しています。これにより、AdonisJSが次のことができます。

  • 各リクエストのたびにコントローラの新しいインスタンスを作成します。
  • また、IoCコンテナを使用してクラスを構築します。これにより、自動的な依存性の注入を活用できます。

非推奨

必要な場合、コントローラのインスタンスを手動で作成し、メソッドを実行することもできます。ただし、IoCコンテナの利点を失い、冗長なコードが増えるため、おすすめしません。

// 🫤 いやいや
router.get('users', (ctx) => {
return new UsersController().index(ctx)
})

また、関数を使用してコントローラを遅延ロードしていることにも注意してください。

HMRを使用している場合は、コントローラを遅延ロードする必要があります。

コードベースが成長するにつれて、アプリケーションの起動時間に影響を与えることがわかるでしょう。その一因は、すべてのコントローラをルートファイル内でインポートしているためです。

コントローラはHTTPリクエストを処理するため、モデル、バリデータ、またはサードパーティのパッケージなど、他のモジュールをインポートすることがよくあります。その結果、ルートファイルはコードベース全体をインポートする中心地となります。

遅延ロードは、インポートステートメントを関数の後ろに移動し、動的インポートを使用するだけの簡単な方法です。

ESLintプラグインを使用して、標準的なコントローラのインポートを強制し、自動的に遅延ダイナミックインポートに変換することができます。

シングルアクションコントローラ

AdonisJSでは、シングルアクションコントローラを定義する方法が用意されています。これは、機能を明確に名前付けられたクラスにまとめる効果的な方法です。これを実現するには、コントローラ内にhandleメソッドを定義する必要があります。

import type { HttpContext } from '@adonisjs/core/http'
export default class RegisterNewsletterSubscriptionController {
async handle({}: HttpContext) {
// ...
}
}

次に、次のようにコントローラをルートに参照できます。

router.post('newsletter/subscriptions', [RegisterNewsletterSubscriptionController])

マジックストリングの使用

コントローラを遅延ロードする別の方法は、コントローラとそのメソッドを文字列として参照することです。これはマジックストリングと呼ばれます。文字列自体には意味がなく、ルーターがコントローラを参照し、内部でインポートするために使用します。

次の例では、ルートファイル内にインポートステートメントがなく、コントローラのインポートパス+メソッドを文字列としてルートにバインドしています。

import router from '@adonisjs/core/services/router'
router.get('users', '#controllers/users_controller.index')

マジックストリングの唯一の欠点は、タイプセーフではないことです。インポートパスにタイプミスをすると、エディターからフィードバックが得られません。

一方、マジックストリングは、インポートステートメントのおかげで、ルートファイル内の視覚的な雑音をすべてクリーンアップすることができます。

マジックストリングの使用は主観的であり、個人的に使用するか、チームで使用するかは自由です。

依存性の注入

コントローラクラスは、IoCコンテナを使用してインスタンス化されるため、コントローラのコンストラクターまたはコントローラメソッド内で依存関係をタイプヒントできます。

UserServiceクラスがある場合、次のようにコントローラ内でそのインスタンスを注入できます。

export default class UserService {
async all() {
// データベースからユーザーを返す
}
}
import { inject } from '@adonisjs/core'
import UserService from '#services/user_service'
@inject()
export default class UsersController {
constructor(protected userService: UserService) {}
index() {
return this.userService.all()
}
}

メソッドインジェクション

メソッドインジェクションを使用して、コントローラメソッド内でUserServiceのインスタンスを直接注入することもできます。この場合、メソッド名に@injectデコレーターを適用する必要があります。

コントローラメソッドに渡される最初のパラメータは常にHttpContextです。したがって、2番目のパラメータとしてUserServiceをタイプヒントする必要があります。

import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import UserService from '#services/user_service'
export default class UsersController {
@inject()
index(ctx: HttpContext, userService: UserService) {
return userService.all()
}
}

依存関係のツリー

依存関係の自動解決は、コントローラに限定されるわけではありません。コントローラ内でインジェクトされたクラスは、依存関係をタイプヒントでき、IoCコンテナが依存関係のツリーを自動的に構築します。

たとえば、UserServiceクラスを変更して、HttpContextのインスタンスをコンストラクターの依存関係として受け入れるようにします。

import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
@inject()
export default class UserService {
constructor(protected ctx: HttpContext) {}
async all() {
console.log(this.ctx.auth.user)
// データベースからユーザーを返す
}
}

この変更後、UserServiceは自動的にHttpContextクラスのインスタンスを受け取ります。また、コントローラには変更は必要ありません。

リソース駆動型コントローラ

従来のRESTfulアプリケーションでは、コントローラは単一のリソースを管理するために設計されるべきです。リソースは通常、ユーザーリソース投稿リソースのようなアプリケーション内のエンティティです。

投稿リソースの例を取り上げ、そのCRUD操作を処理するエンドポイントを定義してみましょう。まず、最初にコントローラを作成します。

--resourceフラグを使用して、リソースに対するコントローラを作成できます。次のメソッドが含まれたコントローラが作成されます。

node ace make:controller posts --resource
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
/**
* すべての投稿のリストまたはページネーションを返す
*/
async index({}: HttpContext) {}
/**
* 新しい投稿を作成するためのフォームを表示する
*
* APIサーバーを作成している場合は不要です。
*/
async create({}: HttpContext) {}
/**
* 投稿を作成するためのフォームの送信を処理する
*/
async store({ request }: HttpContext) {}
/**
* IDによって単一の投稿を表示する
*/
async show({ params }: HttpContext) {}
/**
* IDによって既存の投稿を編集するためのフォームを表示する
*
* APIサーバーを作成している場合は不要です。
*/
async edit({ params }: HttpContext) {}
/**
* 特定の投稿をIDで更新するためのフォームの送信を処理する
*/
async update({ params, request }: HttpContext) {}
/**
* 特定の投稿をIDで削除するためのフォームの送信を処理する
*/
async destroy({ params }: HttpContext) {}
}

次に、router.resourceメソッドを使用してPostsControllerをリソースフルなルートにバインドしましょう。メソッドは、リソース名を第1引数として、コントローラの参照を第2引数として受け入れます。

import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')
router.resource('posts', PostsController)

resourceメソッドによって登録されるルートのリストは、node ace list:routesコマンドを実行することで表示できます。

ネストされたリソース

ドット.表記法を使用して、親リソースと子リソースの名前を指定することで、ネストされたリソースを作成できます。

次の例では、commentsリソースをpostsリソースの下にネストしたルートを作成しています。

router.resource('posts.comments', CommentsController)

シャローリソース

ネストされたリソースを使用する場合、子リソースのルートは常に親リソース名とそのIDで接頭辞が付けられます。たとえば:

  • /posts/:post_id/commentsルートは、指定された投稿のすべてのコメントのリストを表示します。
  • そして、/posts/:post_id/comments/:idルートは、IDによって単一のコメントを表示します。

2番目のルートの/posts/:post_idの存在は無関係であり、IDによってコメントを参照できます。

シャローリソースは、URL構造をフラットに保ちながら(可能な限り)、そのルートを登録します。今回は、posts.commentsをシャローリソースとして登録しましょう。

router.shallowResource('posts.comments', CommentsController)

リソースルートの名前付け

router.resourceメソッドで作成されるルートは、リソース名とコントローラアクションの後に名前が付けられます。まず、リソース名をスネークケースに変換し、ドット.セパレーターを使用してアクション名を連結します。

リソースアクション名ルート名
postsindexposts.index
userPhotosindexuser_photos.index
group-attributesshowgroup_attributes.index

resource.asメソッドを使用して、すべてのルートのプレフィックス名を変更できます。次の例では、group_attributes.indexルート名をattributes.indexに変更しています。

router.resource('group-attributes', GroupAttributesController).as('attributes')

resource.asメソッドに指定されたプレフィックスは、スネークケースに変換されます。必要な場合は、変換をオフにすることもできます。

router.resource('group-attributes', GroupAttributesController).as('groupAttributes', false)

API専用ルートの登録

APIサーバーを作成する場合、リソースの作成と更新のフォームはフロントエンドクライアントやモバイルアプリによってレンダリングされます。そのため、これらのエンドポイントのルートを作成することは冗長です。

resource.apiOnlyメソッドを使用して、createeditのルートを削除することができます。その結果、5つのルートのみが作成されます。

router.resource('posts', PostsController).apiOnly()

特定のルートのみの登録

特定のルートのみを登録する場合は、resource.onlyまたはresource.exceptメソッドを使用できます。

resource.onlyメソッドは、アクション名の配列を受け入れ、それ以外のすべてのルートを削除します。次の例では、indexstoredestroyアクションのルートのみが登録されます。

router
.resource('posts', PostsController)
.only(['index', 'store', 'destroy'])

resource.exceptメソッドは、onlyメソッドの逆で、指定されたルート以外のすべてのルートを削除します。

router
.resource('posts', PostsController)
.except(['destroy'])

リソースパラメータの名前変更

router.resourceメソッドによって生成されるルートは、パラメータ名としてidを使用します。たとえば、単一の投稿を表示するためのGET /posts/:idや投稿を削除するためのDELETE /post/:idなどです。

resource.paramsメソッドを使用して、パラメータ名をidから別の名前に変更できます。

router.resource('posts', PostsController).params({
posts: 'post',
})

上記の変更により、次のルートが生成されます(一部のみ表示)。

HTTPメソッドルートコントローラメソッド
GET/posts/:postshow
GET/posts/:post/editedit
PUT/posts/:postupdate
DELETE/posts/:postdestroy

ネストされたリソースを使用する場合も、パラメータ名を変更できます。

router.resource('posts.comments', PostsController).params({
posts: 'post',
comments: 'comment',
})

リソースルートにミドルウェアを割り当てる

リソースによって登録されるルートにミドルウェアを割り当てるには、resource.useメソッドを使用します。このメソッドは、アクション名の配列とそれに割り当てるミドルウェアを受け入れます。例えば:

import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router
.resource('posts')
.use(
['create', 'store', 'update', 'destroy'],
middleware.auth()
)

ワイルドカード(*)キーワードを使用して、すべてのルートにミドルウェアを割り当てることもできます。

router
.resource('posts')
.use('*', middleware.auth())

最後に、.useメソッドを複数回呼び出して複数のミドルウェアを割り当てることもできます。例えば:

router
.resource('posts')
.use(
['create', 'store', 'update', 'destroy'],
middleware.auth()
)
.use(
['update', 'destroy'],
middleware.someMiddleware()
)