[CakePHP] コントローラでの共通処理にはトレイトではなくコンポーネントを使おう

この記事は Qiita の CakePHP3 Advent Calendar 2016 の5日目として投稿したものです。

皆さんは CakePHP3 におけるコンポーネントの存在意義について、どのようなお考えをお持ちでしょうか。

以前の CakePHP であれば、複数のコントローラに共通する処理がある場合、それをコンポーネントにまとめるというのはごく普通のことでした。しかし、 PHP5.4 でトレイトが登場したことによって状況は変わってきました。

特に PHP5.5.9 以上を対象にする CakePHP3 においては、コンポーネントに共通処理を書くことにあまりメリットを見出せていない方も少なくないかもしれません。コンポーネントの代わりにトレイトを使用している方もいらっしゃるかもしれませんね。

CakePHP3 のソースコードでもトレイトはふんだんに使用されています。また、 Cookbook にもトレイトの使用を推奨するかのような例が示されています。例えば Email の章の再利用可能なメールの作成の節には MailerAwareTrait を使用する例として、 2016年12月4日現在、次のようなコードが掲載されています。

UsersController1.php

また、ビューセルの章のセルの呼び出しの節にも、 CellTrait を使用する例として次のコードが掲載されています。

DashboardsController.php

ですが、これらのコードには落とし穴があるのです。 Cookbook には

例えば、ウェルカムメールを送信したいのであれば、 以下のようにするとよいでしょう。

と書いてはありますが、本当は少しもよくないのです。

次のコードをご覧ください。普段、あまり意識されてはいないかもしれませんが、トレイトを使うというのは、こうするのとだいたい同じなのです。

UsersController2.php

もはやお気付きでしょう。コントローラで MailerAwareTrait を使用すると、その public メソッドである getMailer() は UsersController のアクションになってしまうのです。

したがって、次のような URL にアクセスされてしまうと、シグネチャに違反しない限り、悪意のあるユーザーにあらゆるクラスのコンストラクタを呼ぶことを許してしまいます。

/users/getMailer/ClassName

この問題を回避するための一つの案は、トレイトのメソッドの可視性を private に変更することです。コントローラでは public ではないメソッドはアクションにはなりません*1

UsersController3.php

しかし、この方法は決して万全ではありません。 なぜなら、今後の CakePHP のバージョンアップで MailerAwareTrait に新たな public メソッドが追加されないとは言い切れないからです*2

あるいは、これがビジネスロジックをまとめるために独自に作成したトレイトであれば、皆さんの同僚に無分別なプログラマがいて、コントローラで使用しているそのトレイトに、別のブランチでちゃっかり public メソッドを追加しているかもしれません。

この無分別なコミットを止めることは、コードレビューでもしない限り不可能です。ユニットテストは何の役にも立ちません。なぜなら、皆さんのコントローラのテストに、同僚が適当に命名したメソッドが呼べないことを確認するテストケースなんて書いてあるはずがないのです。したがって、同僚のコミットは確実にテストを通るはずです。

問題を解決するための最善の方法は、コントローラからはトレイトを使わないようにすることです。本来であれば MailerAwareTrait の例では次のような MailerComponent があればよかった話なのです。

MailerComponent1.php
UsersController4.php

もっとも、 MailerAwareTrait に書かれているコードをコピペするのは気が引けるでしょうから、実際には次のようなクラスを作って対応することになるでしょう。

MailerComponent2.php

結局トレイトを使っているわけですが、コンポーネントでトレイトを使うのは、コントローラでそうするほどには危険ではありません。コントローラとは異なり、コンポーネントのメソッドを任意に呼び出すことはできないからです。

もっとも、ほどには、という言い方をしたからには常に安全というわけでもありません。トレイトを使用するという選択は、それだけで皆さんのアプリケーションを少なからず壊れやすいものにするはずです。

トレイトが破壊するもの――それはカプセル化です。

恐ろしいことに、トレイトというのはそれを使用するクラスのあらゆる private メンバにアクセスすることが可能です。皆さんがクラスの内部でのみ使用するつもりで private として定義していたはずのメソッドさえも、実はトレイトは呼ぶことが許されているのです。

CapsuleComponent.php
CapsuleCrusherTrait.php

こんなことは親クラスにだって許されません。しかし、親父にもぶたれたことがなくても、トレイトには殴られるのです*3

もしも、これと同じことを無分別な同僚にやられた日には「トレイトから private メソッドを呼ぶなんてありえない」と憤慨したくなるでしょうが、同僚は同僚で「private メソッドを呼ばれたくないのにトレイトを使うなんてありえない」と思っているかもしれません。

そして、ある意味では同僚の言い分は間違ってはいないのです。なぜなら、トレイトを実装する必要があるのは private/protected なメンバにどうしてもアクセスしたい時だからなのです。

実際、そうしたメンバにアクセスする必要がないのであれば、 MailerComponent のようにコンポジションとして実装した方がずっと安全です。使用するクラスの private メンバにまでアクセスできるトレイトは密結合の典型です。

その点、コンポジションを採用すれば、それを使用するクラスとの間は疎結合になります。対照的に同僚との間には諍いも起きず、仲は睦まじく密結合になり、社内の雰囲気だってきっとよくなるに違いありません。

確かにトレイトは非常に強力な機能です。しかし、このように非常に危険な側面も併せ持っています。特にコントローラから使用するのは極力避けるべきです。コントローラでの共通処理はコンポーネントに書きましょう。 CakePHP3 になってもそれは変わりません。

謝辞

この記事の内容は CakePHP 公式 Slack 日本語チャンネルに私が投稿した内容を、改めて文書としてまとめたものです。当時 Cookbook 上に掲載されているコントローラでのトレイトの使用例として、私が把握していたのは CellTrait だけでしたが、この内容を Slack に投稿した際に同チャンネル内の @tenkoma さんより MailerAwareTrait についても同様の記載があることを教えていただきました。そして、今回、改めて記事をまとめるにあたり、コードの単純さから MailerAwareTrait の方を説明に採用させていただくことにしました。 @tenkoma さんにはこの場を借りてお礼申し上げます。

脚注

  1. ^ 他にもコアの Controller クラスで定義されているメソッド、および同クラスで使用されているトレイトで定義されているメソッドについては、仮にそれらをオーバーライドしてもアクションにはなりません。一方で AppController で新たなメソッドを定義、または新たなメソッドを持つトレイトを使用した場合については、これらが public メソッドであればすべてアクションになります。
  2. ^ あくまで理屈の上の話です。この問題に関してはすでに私の方からチームに報告済みですので、実際に public メソッドがこれから追加されることはありません。既存の getMailer() についても 3.4 で何らかの修正を行うかもしれません。
  3. ^ ブライトだったかもしれません。
Share

“[CakePHP] コントローラでの共通処理にはトレイトではなくコンポーネントを使おう” への 1 件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。