ActiveRecordにユーザーの入力を直接突っ込むのはやめよう

  • 9
    Like
  • 0
    Comment
More than 1 year has passed since last update.

YiiのActiveRecordを使って、やはりオマエラのMVCは間違っている系のことを書きたくてやってしまいました。

前置き

YiiはRuby on Rails(RoR)の影響を受けたフレームワークです。RoR同様、MVCパターンの設計をサポートしています。Giiを使ったCRUDのスキャフォルディングでは、

  • ビューをHTMLテンプレートファイル
  • モデルをActiveRecordとし
  • それをControllerクラスが繋いでいる

という、3つのファイル/フォルダがセットになった形になります。

app
  controllers
    HogeController
  models
    Hoge
  views
    hoge
      create.php
      index.php
      view.php
      update.php

さてここで主張したいのは、この3要素のセットというのはあくまで、機能を最大化してコード量は最少になるよう、可能な工夫を積み重ねた結果であるということです。

つまり、基礎を理解している人向けに、本来学習すべき内容をすっ飛ばして素早く開発できるようにした雛形であるということ。初学者はGiiの結果をそのまま使わず、Yiiのモデルの基礎から手書きすることで、MVCを正しく理解できるようになります。

ActiveRecordとは

YiiのActiveRecordは、いっぽうの側面としてORMの特性を持ちます。が、その前に、直系の親としてModelを継承したクラスであるということが重要です。そもそも、HTMLの入力フォームをMVCでうまくプログラミングするということと、DBとの入出力とは、直接関係ありません。Modelとは、ユーザーの都合で変化する値を保持する、ビジネスロジックの本質を表すオブジェクトと言うのが正しい解釈です。

RoR系のMVCがそれぞれに対応する3つのクラスでできているように見えるのは、言ってしまえば、チュートリアルを早く済ませるためのまやかしです。あるいは、データベースに皮をかぶせただけで済むような、単純なデータ管理ツールのためのものかもしれません。ともかく、現実にはそれだけで作れてしまうようなアプリケーションは滅多にありません。

Modelとは

MVCのModelクラスとは、ユーザーの入力(もしくはユーザーへのフィードバック値)を格納する変数を持つオブジェクトです。Yiiアプリケーションの実装コードでは、Modelだけに唯一、ユーザーの自由入力値や外部からの値を持つことを許可すべきです。プログラムを書けば何でもできると思われるかもしれませんが、ユーザーの入力はシステムに危害を及ぼす不適切なものかもしれません。

人が使う何らかのアプリケーションでは、どうしてもユーザー入力を受け入れる必要があります。そうしなければ機能が成立しません。どうせ受け入れなければならないのなら、リスクは最小化したいですね。そこで、Modelだけが、ユーザーの都合に合わせる変数を持つと徹底するようにします。他の部分にユーザー入力はいっさい漏らさないように作ることで、セキュリティ上のリスクを抑えられます。

なので、Modelには入出力をフィルタリングする機能が不可欠です。

Model::load() メソッドは、Model::setAttributes() とは異なり、検証可能な値でなければ受け入れを拒否するようにできています。Model::rules() を実装することで、検証とは何かを定義します。Model::validate()rules() にしたがって、受け入れた値を検証します。

Webアプリケーションであれば、HTMLのフォーム入力と対応するModelは、フォームモデルとして多くのフレームワークで採用されているアイデアです。もちろん、フォームでないAjaxリクエストのようなものを保持してもかまいません。

app
  controllers
    HogeController
  forms
    HogeForm  < これを使う
  models
    Hoge < 編集ビューとコントローラーからは使わない
  views
    hoge
      create.php
      index.php
      view.php
      update.php

単にPOSTを受け付けるだけの段階では、フォームとコントローラーはこのようになります。

HogeController.php
class HogeController extends Controller
{
    public function actionCreate()
    {
        // 元のコード $model = new Hoge();
        $model = new HogeForm(); // フォームモデル

        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'id' => $model->id]);
        } else {
            return $this->render('create', [
                'model' => $model,
            ]);
        }
    }
}
HogeForm.php
class HogeForm extends Model
{
    public $fuga;
    public $piyo;

    public function rules()
    {
        return [
            [['fuga', 'piyo'], 'required'],
            [['fuga'], 'string'],
            [['piyo'], 'string', 'max' => 255]
        ];
    }

    public function save()
    {
        if (!$this->validate()) {
            return false;
        }
        // TODO ビジネスロジックを実装すること
        return true;
    }
}

まだ肝心の実装がありませんが、このアプリケーションは動作します。追ってなにかHogeを使ったビジネスロジックを実装すべきですが、その前に、DBへの影響なしに軽くUXを確認することができます。

主にユーザー入力を保存することが主目的である場合は、メソッド名を save() とすることをお勧めします。後に説明するActiveRecord直結型との互換性が高くなるためです。もちろん、保存でない場合は適切なメソッド名を付けてください。

scenarioを乱用しない

Modelには scenario プロパティがあります。同じフォームモデルだけど、状況に応じてバリデーションルールの一部が少しだけ変わる(たとえばユーザーの権限によって指定できるフィールドが異なるなど)状況を表すのに使います。

ActiveRecordではない純粋なModelでビジネスロジックを実装する設計では、この scenario プロパティの乱用はお勧めしません。同じDBテーブルを操作するからといって、まったく異なる業務にひとつのクラス設計を再利用するのは間違っています。

たとえば、棚卸しと入荷と出荷は、同じ在庫を操作する業務かもしれませんが、まったく異なる画面と権限を持ちます。業務の数だけ、画面の数だけ、それぞれモデルを作るのが健全です。

app
  forms
    TanaoroshiStockForm
    NyuukaStockForm
    ShukkaStockForm
  models
    Stock

そうしないと...

    public function rules()
    {
        return [
            [['title', 'body'], 'required', 'on' => 'create'],
            [['body'], 'string'],
            [['title'], 'string', 'max' => 255, 'on' => 'create'],
            [['body'], 'required', 'on' => 'update'],
            [['title'], 'string', 'max' => 200, 'on' => 'update'],
        ];
    }

たった2シナリオでこれですよ。

3要素MVCが破綻するシナリオ

それでもフォームモデルを信じられない人は、ちょっとECサービスの開発をイメージしてください。

ユーザーが商品を購入したとき、在庫が減って売上が増え、ユーザーの購買履歴にそのデータが追加されます。これによって、1トランザクションでいくつのDBテーブルへの更新が発生するでしょうか?

また、商品を購入するユーザーが入力した値は、商品コードと数量だけです。そんなテーブルは存在するでしょうか?

モデルはテーブルのレコードを表すものだとしか理解していないと、簡単なデータメンテツールを超えた業務の実装では、いくらMVCを指向していても、ファットコントローラーを避けられません。

コントローラーの責務は次の2点で、それ以上のことを行ってはいけません。

  1. コンピューターシステムの都合(Webではつまり、HTTPプロトコルやJSONフォーマット、DBなどの前後処理)をカプセル化すること
  2. 機械のフォーマットで与えられたデータを受けそれを翻訳、ビジネスロジックに責務を負うオブジェクトに委譲すること

コントローラーでHTTPの取り回しが長くなることは、悪いことだとは思いません。そうではなく、一歩でも業務の意味の詳細に踏み込むのが良くないのです。何をどの順番で実行するのかもまた、業務の仕様を表すコードです。コントローラーが直接ORMを操作すると、かならず、処理順序を表現せざるをえなくなります。

(Yiiのコンポーネントにはイベントがあるからといって、ビジネスロジックをイベントコールバックのチェーンで実装するのは、もっとナンセンスです。イベントで行うのは、仕様の本質ではなく、ログや通知、ストレージへの転送など、正常系のテストに影響しないような副次的なことに留めるべきでしょう)

コントローラーに load()save() 以外を書くと負けなのです。ECのコントローラーでは購入フォームの buy() を呼び、その中に、各種テーブルのレコードを操作する知識を実装するのがスマートな実装です。

補足:
データベースのことはモデルでやるんだ、だからコントローラーでトランザクションというのはおかしい、という考え方もあります。しかし自分は、ビジネスロジックの仕様と対応するコードが、トランザクションをコミットするかロールバックするかという制御を持つことには、少々不自然さを感じます。

データベースのことはすべてモデルで実装するべきという感覚は、もしかすると、「モデルはテーブルのレコードを表すものだ」の延長の発想かもしれません。

往々にして、ビジネスロジックはSQLを意識して作らざるをえない場合もあります。しかし、できればモデルは「業務」の意味だけを説明するものとして書きたいと思います。自分が極力ORMを使う理由はまさにここです。

もし動けばいいだけなら、わざわざプロパティにセットしてsave()などせず、クエリビルダでINSERT文を書けるだけで十分ではありませんか。裏を返せば、コンピューターシステムとして必要な処理はできるかぎり「モデル」の外に置きたい、まあHTTPのコントローラーでないにしても、何か「コントローラー的なもの=DB抽象層」が責務を負うべきだと思います。

ではGiiは何を生成しているのか

YiiのActiveRecordとは、ここまでで述べてきた純粋なModelと、ORMとしてのActiveRecordのハイブリッドです。ORMとしてだけ使ってもいいけれど、フォームモデルとしての責務も兼務できる、責務重複クラスです。おっと、すぐに責務重複は悪とか言わない。

たしかに、不純に見えます。が、YiiはOOPフレームワークであると同時に、EoD(Ease of Development)フレームワークでもあります。もしも、本当にユーザー入力をそのまま、最低限のバリデーションだけしてDBに格納できればいいのなら、これほど楽なものはありません。時にそれは、データ設計のプロトタイピングで使い捨てコードとして、あるいは、本当に自分だけが利用する管理用のバックドアとして、非常に役に立ちます。

自分は勝手に ActiveRecord直結型 と呼んでいます。直結型を適切なときに使えるのは、十分に経験を積んだ人です。迂闊に使うと、待っているのは scenario 条件の膨張とファットコントローラーです。

コメントを受け付けるブログをイメージしてください。ActiveRecordであるCommentモデルは文字種のみチェックし、ユーザーが使うCommentFormではNGワードを禁止するバリデーションも行うかもしれません。このシステム、管理者は裏から任意にデータメンテできる必要がありませんか。その場合、直結型で何か問題あるでしょうか?

Giiはまた、CRUDコード生成中に検索用のフォームモデルを生成できます。このフォームもまたユーザー入力用のModelです。

※ 生成された検索用のフォームが間接的にActiveRecordを継承する形になっていますが、それはやはり単にコード量を節約するためです。どのフィールドででも検索できるようにする必要がなければ、本当は純粋なModelに書き換えた方が良いでしょう。

結論

ActiveRecordにユーザーの入力を直接突っ込むのはやめましょう。いろいろと間違ったMVCの罠に足を突っ込んでしまいますよ。

アプリケーション独自の業務を実装するときは、Yiiプロジェクトの雛形を見て、ログインフォームとお問い合わせフォームを参考にしてください。フォームとして独立したModelの良い例となっています。Giiで生成したコードは参考として脇に置いて、まずはそれら良い例を真似て新規のクラスを書いてみてください。

慣れてきたら、CRUD生成でプロトタイプを作ってDBアクセスを確認し、そこから徐々にActiveRecordとコントローラーを切り離していきましょう。つねにUIの動作を確認できる状態を維持できます。

そのとき、フォームモデルが主に保存を行うのなら、メイン機能に save() と名づけておけば、コントローラーでクラスインスタンスが変わってもメソッドの呼び出し方を変えずに済む、というわけです。