CodeIgniterのちまちまネタ5種詰め合わせ

皆様、いかがお過ごしであろうか? 誕生石はRuby、名前はGo、しかしメイン言語はPHPな野地である。PHPerだからって叩かないで下さい。
個人的にはJavaScriptを書くことも増えてきて、最近やっとTypeScriptやWebpack、Vueなんかに触れ始めて楽しいのだが、今回はPHPのフレームワークであるCodeIgniterネタである。
前前々回でも話題に出したCodeIgniterだが、ぼちぼちネタが溜まってきたのでたまにはQiitaではなくこちらに書いていきたいと思う。
表題通りちびちびしたネタが中心になるかと思うが、「あぁ、へぇ、ふぅ~ん……」程度に参考にして頂ければ幸いである。
- CSRF脆弱性を防御しながらAjaxでPOST通信を行う
- キャメルケースでプロジェクトを進める時の注意点
- Contorollerのindex()メソッドに引数を渡す
- よく使うForm_validationの追加バリデーションルール
- 拡張したModelを抽象的に呼び出す
- まとめ
CSRF脆弱性を防御しながらAjaxでPOST通信を行う
フォームを扱うのであればほぼ必須と言っても過言ではないCSRF脆弱性対策だが、CodeIgniterではconfig/config.phpにある$config['csrf_protection']をTRUEにし、<form>の開始タグ出力をフォームヘルパーのform_open()にするだけで対策が行える。
仕組みを少しだけひも解いてみると、こうして出力された<form>のすぐ後にtype="hidden"の<input>タグがname="config.phpのconfg['csrf_token_name']"で出力されており、この値も一緒にPOST送信されている事でtokenによる脆弱性対策が行われていることが分かる。
ただし、この方式では自動出力された<input>のvalueをJavaScriptへと渡すのが少々面倒(name属性を頼りにquerySelectorを使えばいけなくもないが、name属性の値はCodeIgniter=バックエンドのみ知る情報なので、仮にconfig.phpの情報が変更されたらフロントエンドのコードも変更しなくてはならない)なので、そのままJavaScriptの変数としてViewに出力してしまうのがオススメだ。
<head>タグの中等に<script>タグを仕込んでそのまま変数として宣言すればJavaScriptの変数として扱える。
//以下Viewファイルの途中
//略
<head>
//略
<script>
var tokenKey = '<?php echo $this->security->get_csrf_token_name(); ?>';
var tokenValue = '<?php echo $this->security->get_csrf_hash(); ?>';
</script>
//略
</head>
//略
もしTypeScriptを使用しているプロジェクトでコンパイル前に未定義変数だと怒られたら、
declare const tokenKey: string; declare const tokenValue: string;
と書いたhoge.d.tsを用意してtsconfig.jsonと同階層に用意してあげよう。
キャメルケースでプロジェクトを進める時の注意点
CodeIgniterは基本的にスネークケースを推奨しているフレームワークであり、本来キャメルケースで開発をすべきではない。
ただし、プロジェクトや社内のコーディング規則によってはキャメルケースで開発せざるを得ない時もあるだろう。
大体のケースはキャメルケースでも問題なく動くハズだが、個人的に経験した中で明確に気を付けなければならないケースは以下の2つある。
form_validationライブラリでControllerのメソッドをルールとして使う場合、プレフィックスとして「callback_」が付くのでスネークケースとキャメルケースが混在する
CodeIgniterのForm_validationライブラリは非常にお世話になるライブラリだが、デフォルトで用意されているルール以外(典型的にはカタカナの判別やDBへのアクセスを伴うもの)を使用する場合はControllerに用意したメソッドを呼び出す方法と無名関数を使う方法の二種類が用意されている。
どちらの方法にも長所があり、メソッドを利用する場合は、既にControllerが持っている情報やロードされたモデルなどを使用する時に有効なのだが、この方法はルールを追加する時にcallback_という文字列をメソッド名の前に付けなければならない。
//Controllerの中にhogeMethodというpublicメソッドがあったとして
$this->form_validation->set_rules(array(
'field' => 'fuga',
'label' => 'フガ',
'rules' => 'required|max_length[128]|callback_hogeMethod'
));
美意識の問題だけに感じるかもしれないが、ふとした瞬間にcallbackHodeMethodのように書いてしまうことも考えられる。
自作Libraryをloadする際には全て小文字にしないと正常にロードできない
キャメルケースでModel等をロードした場合はそのままの名前(sampleModel.phpの場合は$this->sampleModel->method()のように)で$thisから参照できる。
しかし、Libraryの場合は$this->load->library('hogeLibrary')というようにロードしたら$this->hogeLibrary->method()のように使えるかと思いきや、そんなクラスは無いと言われてしまう。
CodeIgniterの内部を見てみると、クラスファイルをincludeする前に
//$classはlibrary()の第一引数で渡した文字列 $class = ucfirst($class);
という処理が行われているので、クラス自体のincludeは正常に行われているものの、$thisのプロパティとしてクラスを仕込む前は
//$classはlibrary()の第一引数で渡した文字列 //object_nameは$this->の後に使われる名前 $object_name = strtolower($class);
というコードで処理されているため、上記の例であれば全て小文字で$this->hogelibrary->method()とする必要がある。これはスネークケースでは発生しない問題のため、キャメルケースの場合のみ注意が必要だ。
ちなみに、キャメルケースのフロントエンドフレームワークと併用する場合は
最近流行っているJavaScriptのフレームワーク、少なくともAngular、React、Vueらフレームワークは全てキャメルケースを推奨しているようなので、特にAPIが返すJSONデータ等にスネークケースが混ざるのはよろしくない(気がする)。
フロントエンドの人たちに怒られ or 気持ち悪がられないためにも変数名等はキャメルケースで返したいところだが、バックエンド側では規約に従いたい、という場合は語形変換ヘルパーのcamelize()関数を使ってみることをお勧めする。キャメルケースからスネークケース? フロント側に任せるか自作せよ。
//このようにロードし、
$this->load->helper('inflector');
//こんな感じで呼び出してあげれば$strがキャメルケースになる
$str = camelize($str);
Contorollerのindex()メソッドに引数を渡す
CodeIgniterにおける基本中の基本、「example.com/index.php/hoge/fuga/piyoはHode.phpControllerのfuga($str)メソッドが引数$strに'piyo'が入った状態で実行される」というルーティング機能だが、example.com/index.php/page/1のようなURLでid = 1のページを表示したい、みたいなケースは意外と困ることになる。
エンジニアは気にしない場合が多い(野地調べ)が、基本クライアントはURL階層が深いのを嫌う傾向にある(あくまで野地調べ)ため、Controller名の後にすぐ引数を渡したい状況は結構頻繁にあるだろう。
メソッドを表す第二セグメントを省略した場合、自動的に呼び出されるのはindex()メソッドだが、素直にルーティング規則へ従った場合、index()メソッドへ引数を渡すのであればexample.com/index.php/page/inidex/1のようなURLにせざるを得ない。
これを解決するには_remap()メソッドを使うのが確実だ。
まず、ブラウザから直接アクセスするPublicメソッドがindex()のみの場合は以下の脳筋コードで万事解決する。
/**
* URL書き換え用メソッド
* 何が来ようとindexへアクセスさせる
* @param string $unique
*/
public function _remap($param)
{
$this->index($param);
}
ただし、index()以外のメソッドにもアクセスする、または今後増設する可能性がある場合はもうちょっと知性を働かせたルーティングロジックが必要になる。
当然、なにがなんでも第二セグメントを引数として解釈してしまうと他のメソッドへアクセスできなくなってしまうので、第二セグメントに渡ってきた文字列を見て判定が必要なのだが、以下のケースは数値であったらindex()メソッドの引数としてindex()を呼び出す例である。
/**
* URL書き換え用メソッド
* example.com/index.php/hoge/○○○の位置に数字と解釈できるものが入ってきたらindexメソッドを叩く
* @param string $method
* @param array $param
* @return mixed
*/
public function _remap($method, $param)
{
if ($method === 'index' && ! isset($param[0]))
{
//indexメソッドを呼び出そうとしているが、引数が指定されていない場合はTOPへリダイレクトする
redirect(site_url());
}
elseif (ctype_digit($method))
{
$this->index($method);
}
else
{
if (method_exists($this, $method))
{
return call_user_func_array(array($this, $method), $param);
}
show_404();
}
}
よく使うForm_validationの追加バリデーションルール
個人的に、CodeIgniterが標準で用意してくれているライブラリの中で最もお世話になっているのがForm_validationライブラリだ。
既に用意されているルールのカバー範囲は広く、必須や最大・最小文字数制限はもちろんアルファベットや数値制限、再入力チェックやメールアドレス・IPチェック等まさに至れり尽くせりである。
しかし、当然プロジェクトによってはそれだけでは足りずにカスタムルールを作成することも多い。
ここでは自分が使用する頻度の多かったカスタムルールの例を挙げていこう。
ファイルが存在するかとうか調べる
画像URL等を入力させる時、本当にそのファイルが存在するのか調べるルールである。
file_get_contentsを使用するので外部サイトの全てに適用できるわけではないのだが、内部にアップロードしたファイルに限定される場合や、file_get_contentsで取得できないコンテンツを無視できる場合は有用かと思われる。
array(
'isExistFile',
function (string $path)
{
//そもそも$pathが空だったらTRUE判定(requiredとの差別化)
if ($path === '')
{
return TRUE;
}
//file_get_contentsのエラーを抑える
$context = stream_context_create(array(
'http' => array('ignore_errors' => TRUE)
));
//パスの存在確認
file_get_contents($path, FALSE, $context);
preg_match('/HTTP\/1\.[0|1|x] ([0-9]{3})/', $http_response_header[0], $matches);
if ((int)$matches[1] !== 200)
{
return FALSE;
}
return TRUE;
}
);
全角カナのみに制限する
厳密にはカスタムルールではなくregex_matchを使用したルールなのだが、日本語プロジェクトではヨミガナを入力させるフォームが頻出するので紹介。
このルールは全角カナのみを許可するのだが、レンタルサーバー等への移植を考えた際、phpを動かすlinux環境でで半角カナを扱うのは怖いという理由で半角カナを許可した案件は未だに未経験なので、もし必要であれば拡張して使用して欲しい。
'rules' => 'regex_match[/^[ァ-ヶー]+$/u]'
現在編集しているユーザーが既に登録しているメールアドレス以外の登録済メールアドレスと被らないメールアドレスかを判定する
DBの特定のテーブル中、特定のフィールド中でユニークであるかどうかはis_unique[table.field]を使えば手軽にできるのだが、既に登録されているデータの内、既に登録されているメールアドレスのみは除外、つまりユーザー情報等を新規追加するのではなく更新するケースでのバリデーションはひと手間加えた検証ルールが必要だ。
CodeIgniterの構造上、バリデーションはControllerで扱うことになるのだが、DBを参照する処理はやはりModelに作成するのが妥当だろう。
幸い、CodeIgniterではそのコードが置いてあるのがControllerであろうがconfigファイルであろうがCodeIgniterの資産にアクセスできるget_instance()という関数があるので、これを使用して対応するModelをロードして検証ルールを作成しよう。
//以下、$_POSTにテーブルのプライマリキーであるidが一緒に送られている前提
array(
'isUniqueMail',
function($mail)
{
//モデルのロード
$CI =& get_instance();
$CI->load->model('userModel');
//idが一緒に送られてきている場合はメソッドに渡す
$id = $CI->input->post('id') ? (int)$CI->input->post('id') : 0;
//モデルに投げる
return $CI->user_model->is_unique_mail((string)$mail, $id);
}
);
//こちらはUser_modelクラスのis_unique_mail()メソッド
/**
* 渡されたメールアドレスを使用しているユーザーが一人でもいればFALSEを返す
* 逆にそのメールアドレスがユニーク(未使用)であればTRUEを返す
* ただし、削除済のユーザーは検索対象に含まない(actフィールドが0の場合は除外する)
* $idに渡されたidと一致するレコードも除外する
* @param string $mail
* @param int $id
* @return bool
*/
public function is_unique_mail($mail, $id)
{
//SQLを投げる
$query = $this->db->query("
SELECT id
FROM user
WHERE act = 1
AND id != ?
AND mail = ?
LIMIT 1
",array($id, $mail));
//結果配列が空だったらTRUE, 中身があったらFALSEを返す
return $query->row_array() ? FALSE : TRUE;
}
拡張したModelを抽象的に呼び出す
自分はよくDB操作をするModelのためにCI_Modelを継承したMY_Modelクラスへ様々なCRUD操作のためのメソッドを用意し、実際に使うModelに継承させて使うことがある。
しかし、都道府県データをprefectureテーブルから入手する等、Prefecture_model.php等を用意するまでもないのだが、取得のためにMY_Modelに用意したメソッドを使いたい場合がある。
おとなしく新しいクラスを作っておけばいいのかもしれないが、そんなケースが4、5個も重なると正直modelsディレクトリが見辛くてしょうがないので、Starndard_model.phpといった抽象的なModelを作って兼任させるという手がある。
やり方は、MY_Modelを継承させたStarndard_model.phpをmodelsディレクトリに作成し、Starndard_model.phpにテーブル名やカラム名等、MY_Modelのメソッドを実行するのに必要な情報を補完するプロパティとそのセッターメソッドを作成する。
そして実際に使用するControllerで
//MY_Modelにはidで単一レコードが入手できるget()メソッド、Standard_modelにはtalbe名をセットするset_table()メソッドがあったとする
$this->load->model(standard_model);
//admin情報の入手
$admin_model = clone $this->standard_model;
$admin_model->set_table('admin');
$this->admin = $admin_model->get($id);
//都道府県の入手
$pref_model = clone $this->standard_model;
$pref_model->set_table('prefecture');
$this->pref = $pref_model->get($id);
のようにcloneを使うことによって$this->starndard_modelを使いまわすことができるのだ。
まとめ
ちまちまネタの数々、いかがだっただろうか。間違っているところを発見したらこっそり教えて欲しいが、楽しんでいただけたらなによりである。
CodeIgniterは薄いフレームワークだが、その分自由で楽しい。
多人数 or 大規模な開発には向かないが、自由なぶん新しい発見があるのはエンジニアとして面白い特徴だろう。
いずれLaravelやRuby on Railsあたりには手を出してみたいところだが、ひとまずはSQLを生でガリガリ書くようなCodeIgniterで修行を積んでいきたいものである。

[…] CodeIgniterのちまちまネタ5種詰め合わせ […]