CodeIgniter のモデルの利用

手作業でのモデル作成

モデルの作成のために特別なクラスを継承する必要はありません。データベース接続のインスタンスさえあれば準備はできています。

use \CodeIgniter\Database\ConnectionInterface;

class UserModel
{
        protected $db;

        public function __construct(ConnectionInterface &$db)
        {
                $this->db =& $db;
        }
}

CodeIgniter のモデル

CodeIgniter はモデルクラスを提供しており、機能はわずかながら良いものです。次のものです:

  • 自動データベース接続
  • 基本的な CRUD のメソッド
  • モデルでのバリデーション
  • そのほか

このクラスはモデルづくりの基盤を提供します。アプリケーションのモデルレイヤ作成を手早く行えるようになります。

モデルの作成

CodeIgniter のモデルを利用するには、新しいモデルクラスでシンプルに CodeIgniter\Model を継承します:

class UserModel extends \CodeIgniter\Model
{

}

このからっぽのクラスはデータベース接続への便利なアクセスと、クエリビルダ、そのほか多くの便利メソッドを提供しています。

データベースへの接続

クラスが最初にインスタンス化されるとき、コンストラクタにデータベース接続がなにも渡されなければ、設定ファイルにあるデフォルトデータベースグループへの接続を自動的に行います。どのグループを使うかはモデルごとに基本設定を行えます。DBGroup プロパティをクラスに追加します。 これによってモデルの $this->db で適切な接続を利用することを確かなものにしてくれます。

class UserModel extends \CodeIgniter\Model
{
        protected $DBGroup = 'group_name';
}

"group_name" の箇所はデータベース設定ファイルのグループ定義の名前に置き換えてください。

モデルの設定

モデルクラスはわずかながら設定オプションを持ち、シームレスにクラスメソッドを調整できます。まず最初の2つはすべての CRUD メソッドで使用されるもので、どのテーブルを使用しどのレコードが要求されているかを発見するためのものです:

class UserModel extends \CodeIgniter\Model
{
        protected $table      = 'users';
        protected $primaryKey = 'id';

        protected $returnType = 'array';
        protected $useSoftDeletes = true;

        protected $allowedFields = ['name', 'email'];

        protected $useTimestamps = false;

        protected $validationRules    = [];
        protected $validationMessages = [];
        protected $skipValidation     = false;
}

$table

このモデルで主に使用するデータベーステーブルを設定します。これはビルトインの CRUD メソッドでのみ利用されます。あなた独自のクエリはこのテーブルだけに制限されるものではありません。

$primaryKey

このテーブルのレコードを一意に特定するカラム名です。これは必ずしもデータベースで主キーに設定されているものに合わせる必要はありませんが、find() のようなメソッドで値を特定するのに利用されます。

$returnType

モデルの CRUD メソッドはあなたの作業を一歩進め、結果オブジェクトではなく結果データを自動的に返します。この設定では返すデータの型を定義します。使える値は 'array'、'object'、または完全なクラス名称で、getCustomResultObject() メソッドで使われるものです。

$useSoftDeletes

もし true なら、すべての delete* メソッド呼び出しは実際に行を削除するのではなく、フラグをデータベースに設定するだけになります。This can preserve data when it might be referenced elsewhere, or can maintain a "recycle bin" of objects that can be restored, or even simply preserve it as part of a security trail. もし true なら、find* メソッドに先立って withDeleted() を呼ばない限り、find* メソッドは削除されていない行だけを返します。

この設定には INT または TINYINT のフィールド deleted をテーブルに用意しておく必要があります。

$allowedFields

この配列は save、insert、update メソッドで操作することのできるフィールド名です。これらに設定していないフィールドの操作は破棄されます。これはフォームからの入力値を全部モデルに投げてしまったときに発生する、潜在的な mass assignment 脆弱性からの防御に役立ちます[訳注: mass assignment という名の脆弱性です。定訳なし]。

$useTimestamps

この boolean 値は、insert または update で毎回自動的に現在日時を設定するかどうかを決定します。もし true なら、$dateFormat で指定したフォーマットの現在時刻を設定します。この設定には 'created_at' カラムと 'updated_at' カラムを適切な型でテーブルに持っておくことが必要です。

$dateFormat

この値は $useTimestamps と一緒に動かすもので、データベースに入れる日付値の形を正しく認識させるものです。デフォルトでは DATETIME 値になりますが、有効なオプションは datetime、date、または int(PHPのtimestamp)です。

$validationRules

Contains either an array of validation rules as described in How to save your rules or a string containing the name of a validation group, as described in the same section. より詳しくは後述を参照ください。

$validationMessages

Contains an array of custom error messages that should be used during validation, as described in Setting Custom Error Messages. より詳しくは後述を参照ください。

$skipValidation

すべての insertsupdates でバリデーションをスキップするかどうか。デフォルトは false で、毎回バリデーションを実施することを意味します。この設定は主に skipValidation() メソッドで使用されますが、 true に変更するとこのモデルは決してバリデーションされなくなります。

データ操作

データの find

いくつかのメソッドはテーブルの基本的な CRUD 操作を提供します。find()、insert()、update()、delete() その他です。

find()

第1引数に渡された値が主キーと一致する単一行を返します:

$user = $userModel->find($user_id);

返される値は $returnType で指定した型になります。

主キーの値には1つだけではなく複数の値を配列で渡すことができ、複数の値が返ります:

$users = $userModel->find([1,2,3]);

findWhere()

ひとつまたは複数の検索条件を指定することができます。一致したすべての行が返ります:

// 単純な where の利用
$users = $userModel->findWhere('role_id >', '10');

// 配列で where を使う
$users = $userModel->findWhere([
        'status'  => 'active',
        'deleted' => 0
]);

findAll()

Returns all results:

$users = $userModel->findAll();

このメソッドに先立って呼び出された Query Builder コマンドを割り込ませる形でこのクエリを編集します:

$users = $userModel->where('active', 1)
                   ->findAll();

limit と offset をそれぞれ第1引数、第2引数に渡せます:

$users = $userModel->findAll($limit, $offset);

first()

結果セットの1行目を返します。クエリビルダとの組み合わせで使用するのが良いです。

$user = $userModel->where('deleted', 0)
                  ->first();

withDeleted()

もし $useSoftDeletes が true なら、find* メソッドは 'deleted = 1' の行を返しません。一時的にこれを変更したいなら、withDeleted() メソッドを find* メソッドに先立って使用することができます。

// 削除されていない行だけを取得します (deleted = 0)
$activeUsers = $userModel->findAll();

// すべての行を取得します
$allUsers = $userModel->withDeleted()
                      ->findAll();

onlyDeleted()

withDeleted() が削除行と削除されていない行の両方を返すところを、このメソッドは次回の find* メソッドで論理削除行だけを返すようにします:

$deletedUsers = $userModel->onlyDeleted()
                                                  ->findAll();

データの保存

insert()

このメソッドの唯一の引数として、連想配列を渡すとデータベースに新しい行を作成します。配列のキーは $table のカラム名と一致していなければなりません。配列の値はそのキーに対する値として保存されます:

$data = [
        'username' => 'ダース',
        'email'    => 'd.vader@theempire.com'
];

$userModel->insert($data);

update()

データベースにすでにあるレコードを更新します。第1引数の $primaryKey は更新するレコードを指します。 第2引数として連想配列をこのメソッドに渡します。配列のキーは $table のカラム名と一致していなければなりません。配列の値はそのキーに対する値として保存されます:

$data = [
        'username' => 'ダース',
        'email'    => 'd.vader@theempire.com'
];

$userModel->update($id, $data);

save()

これは insert() メソッドと update() メソッドのラッパーで、自動的に作成または更新を操作します。 $primaryKey 値にマッチする配列キーがあるかどうかで判定します:

// モデルのプロパティとして定義します
$primaryKey = 'id';

// insert() として動作します
$data = [
        'username' => 'ダース',
        'email'    => 'd.vader@theempire.com'
];

$userModel->save($data);

// 主キー 'id' があるので更新として動作します。
$data = [
        'id'       => 3,
        'username' => 'ダース',
        'email'    => 'd.vader@theempire.com'
];
$userModel->save($data);

また、save メソッドはカスタムクラス結果オブジェクトと一緒によりシンプルに動作します。ただのオブジェクトとしてではなく認識すると、public と protected の値をかき集めて配列に入れ、適切に insert または update メソッドに渡します。これはエンティティクラスとして非常にわかりやすい方法です。エンティティクラスはシンプルなクラスで、ユーザやブログ投稿、仕事などのようなオブジェクトの型として、単一のインスタンスを表現します。このクラスはオブジェクト自体にまつわるビジネスロジックを管理する責任を持ち、要素の形式を正しく保つなどをします。それらはデータベースに保存する方法についてどのような関心も持つべきではありません。最も単純な形式として、次のようになります:

namespace App\Entities;

class Job
{
        protected $id;
        protected $name;
        protected $description;

        public function __get($key)
        {
                if (isset($this->$key))
                {
                        return $this->$key;
                }
        }

        public function __set($key, $value)
        {
                if (isset($this->$key))
                {
                        $this->$key = $value;
                }
        }
}

非常にシンプルなモデルとして次のように操作します:

class JobModel extends \CodeIgniter\Model
{
        protected $table = 'jobs';
        protected $returnType = '\App\Entities\Job';
        protected $allowedFields = [
                'name', 'description'
        ];
}

このモデルは jobs テーブルのデータを操作し、App\Entities\Job のインスタンスとして結果を返します。 データベースのレコードとして永続化させる必要があるとき、独自のメソッドを書くか、もしくはモデルの save() メソッドを使いクラスを解析させ、public と private のプロパティを集め、データベースにそれを保存します:

// Job のインスタンスを取得します
$job = $model->find(15);

// なんらかの変更を加えます
$job->name = "Foobar";

// 変更を保存します
$model->save($job);

データの削除

delete()

主キーの値を第1引数として取り、モデルのテーブルの一致するレコードを削除します:

$userModel->delete(12);

モデルの $useSoftDeletes が true の場合は、削除対象の行は 'deleted = 1' に更新されます。第2引数に true を渡せば物理削除を強制できます。

deleteWhere()

モデルのテーブルの複数行を削除します。その検索条件は最初の2つの引数に渡します。

// 単純な where
$userMdoel->deleteWhere('status', 'inactive');

// 複雑な where
$userModel->deleteWhere([
        'status' => 'inactive',
        'warn_lvl >=' => 50
]);

モデルの $useSoftDeletes が true の場合は、削除対象の行は 'deleted = 1' に更新されます。第3引数に true を渡せば物理削除を強制できます。

purgeDeleted()

'deleted = 1' で論理削除された行を物理削除で一掃します。:

$userModel->purgeDeleted();

データのバリデーション

多くの人にとって、モデルでのバリデーションは重複コードをなくし、データを単一基準で維持するのに好ましい方法です。モデルクラスは insert()update()save() メソッドでデータベースに保存するのに先立ちデータを自動的にバリデーションする方法を提供します。

最初のステップは、$validationRules プロパティに適用ルールを埋めることです。もし独自のエラーメッセージを使いたいなら、$validationMessages 配列に入れてください:

class UserModel extends Model
{
        protected $validationRules = [
                'username' => 'required|alpha_numeric_space|min_length[3]',
                'email'    => 'required|valid_email|is_unique[users.email]',
                'password' => 'required|min_length[8]',
                'pass_confirm' => 'required_with[password]|matches[password]'
        ];

        protected $validationMessages = [
                'email' => [
                        'is_unique' => 'ごめんなさい。そのメールアドレスはすでに使用されています。別のものにしてください。'
                ]
        ];
}

さて、これで insert()update()save() メソッドはどこで呼ばれようともバリデーションされます。バリデーションに失敗したら、モデルは boolean で false を返します。getErrors() メソッドでバリデーションエラーを取得することができます:

if ($model->save($data) === false)
{
        return view('updateUser', ['errors' => $model->getErrors()];
}

このメソッドはフィールド名とそれに紐づくエラーの配列を返します。フォームの上部にまとめて表示するか、それぞれ個別に表示することができます:

<?php if (!empty($errors)) : ?>
        <div class="alert alert-danger">
        <?php foreach ($errors as $field => $error) : ?>
                <p><?= $error ?></p>
        <?php endforeach ?>
        </div>
<?php endif ?>

バリデーション設定ファイルでルールとエラーメッセージを整理するほうがよいなら、$validationRules には作成したバリデーションルールグループ名を単に設定してください:

class UserModel extends Model
{
        protected $validationRules = 'users';
}

フィールドの保護

mass assignment 攻撃から保護する助けとして、モデルクラスは $allowedFields プロパティに insert または update するフィールド名をすべてリストアップすることを 要求 します。これらからはみ出るデータは、データベースに入れる前に削除されます。タイムスタンプや主キーが変わらないことを確信できる、すばらしいことです。

protected $allowedFields = ['name', 'email', 'address'];

ときには、それらの要素を変更する必要がでてくるでしょう。テストやマイグレーション、値の埋め込みの際にはよく出てきます。その際には、保護を on/off できます:

$model->protect(false)
          ->insert($data)
          ->protect(true);

クエリビルダの操作

必要なときにはいつでも、モデルのデータベースのクエリビルダの共有インスタンスにアクセスすることができます:

$builder = $userModel->builder();

このビルダはモデルの $table でセットアップ済みです。

また、クエリビルダメソッドおよびモデルの CRUD メソッドを同じメソッドチェーンにつなげることができ、エレガントに使えます:

$users = $userModel->where('status', 'active')
                                        ->orderBy('last_login', 'asc')
                                        ->findAll();

注釈

モデルのデータベース接続もシームレスに使用できます:

$user_name = $userModel->escape($name);

実行時の返値型の変更

find*() メソッドの返値型として $returnType を指定することができます。しかしながら、異なる型で取得したいときもあるでしょう。モデルが提供するメソッドでは、まさにそのようにすることができます。

注釈

これらのメソッドは次回の find*() メソッド呼び出しにのみ影響します。その後、デフォルトの値にリセットされます。

asArray()

次回の find*() メソッド呼び出しの返値を連想配列にします:

$users = $userModel->asArray()->findWhere('status', 'active');

asObject()

次回の find*() メソッド呼び出しの返値を標準オブジェクトまたは独自クラスのインスタンスにします:

// 標準オブジェクトとして返します
$users = $userModel->asObject()->findWhere('status', 'active');

// 独自クラスのインスタンスとして返します
$users = $userModel->asObject('User')->findWhere('status', 'active');

大量データの処理

ときどき、大量データを処理する必要があり、メモリ不足のリスクをはらむこともあるでしょう。 これをシンプルにするため、chunk() メソッドでより小さな塊でデータを取得し、これに対処できます。第1引数はひとつの塊で取得する行数です。第2引数は各行を処理するクロージャです。

cron ジョブやデータエクスポート、その他のの大きなタスクに活用できます。

$userModel->chunk(100, function ($data)
{
        // なにかします
        // $data は単一行です
});

URL での ID 不明瞭化

リソースの ID を URL に表示するかわりに(例: /users/123)、モデルは ID を不明瞭にするシンプルな方法を提供します。これは、攻撃者が URL の ID を単純にインクリメントして何か悪いことを企むことへの、いくらかの保護になります。

これは正確にはセキュリティの対応にはなりませんが、異なる単純なレイヤにおける保護になります。標的型の攻撃者にとっては実際の ID を極めてたやすく判別できます。

そのデータはデータベースには保存せずに、シンプルに ID を偽装します。URL を作るとき、encodeID() メソッドを使ってハッシュした ID を取得します。

// 次のようなものを作成します: http://exmample.com/users/MTIz
$url = '/users/'. $model->encodeID($user->id);

コントローラでそのアイテムを取得するときは、find() メソッドの代わりに findByHashedID() メソッドを使用します。

public function show($hashedID)
{
        $user = $this->model->findByHashedID($hashedID);
}

ハッシュをデコードする場合、decodeID() メソッドを使用します。

$hash = $model->encodeID(123);
$check = $model->decodeID($hash);

注釈

"ハッシュ ID" と呼んでいますが、実際にはハッシュしていません。ID を短い、ユニークな、一意に特定するものにエンコードするものの用語として多くの集団で共通して用いられています。