CodeIgniter」タグアーカイブ

ci-phpunit-testが動かない理由がphp7ccだった話

Composerで導入できる kenjis/ci-phpunit-test は Codeigniter 3.x で快適にPHPUnitによるテストが書ける便利なツールです。

その ci-phpunit-test にはクラスのメソッド、PHP標準の関数、定数などを書き換える事が出来る、モンキーパッチ機能が有るのですが。なぜか動かなくなり悪戦苦闘していました。

原因はComposerでグローバルにインストールしたphp7ccでした。

sstalle/php7cc はそのPHPアプリケーションが PHP 7.0 で動作するうえで問題が無いかを静的解析するツールなのですが、どちらも nikic/php-parser というPHPで書かれたPHPパーサに依存しており、違うバージョンが入ってしまうことが原因のようです。

nikic/php-parser v1.2.1 の composer.json の記述(抜粋)
nikic/php-parser: ~1.4,

kenjis/ci-phpunit-test v0.15.0 の composer.json の記述(抜粋)
nikic/php-parser: ^2.1|^3.0

とりあえずグローバルに入っている古いバージョンをremoveする事で、問題なく動くようになりました。

なお、php7cc の代わりになりそうな物としては wimg/PHPCompatibility があります。php7cc でカバーしていた範囲をすべてチェック出来るかはまだ分からないのですが、チェックのターゲットとして、PHP 5.6 以前のバージョンであったり、PHP 7.1 にも対応してるようです。

既存のPHP 4系WebアプリをCodeIgniter 3.xに乗せ換えた話

PHP 4.xで書かれた歴史有るWebアプリをフルスクラッチせずにPHP 5.6&CodeIgniter 3.xで動くように乗せ換えた話です。具体的なコードというよりも、こうやって進めた、という話になります。

この記事はCodeIgniter Advent Calendar 2016の11日目の記事になります。

現状

  • フレームワーク利用無し。
  • DBとしてPostgreSQL 8.2以前が組み合わされている。
  • ORマッパーとかも無い。
  • register_globals = On で動く前提で作られている。
  • かなりの数がある画面のほとんどのページが数個程度のPHPファイルに纏められている。GETやPOSTの値に機能やページの指定も含まれていて、if文でページを切り替えている。
  • 画面表示はCodeigniterでいうHTMLヘルパーのような、フォームやテーブルを楽に作るためのクラスが用意されていて、その関数を呼んでいる。
  • テストコードなんて無い。
  • ソースコードの文字コードもDBの文字コードもEUC-JP。

なかなか辛そうな感じですが、下手にフレームワークを使われているより、フルスクラッチ的なPHPの方がCodeigniterへの移植相性は良い(と思った)ので移行は出来るだろうと判断しました。

全コードをUTF-8に書き換え

今後の事を考えて文字コードはUTF-8に統一します。EUC-JPのコードをnkfでサクッと書き換えます。htmlspecialcharsしている所を検索し、見つけた1つ1つをUTF-8に対応した形で書き換えます。

PostgreSQL 9.x系への入れ替え

既存のDBの内容はすべてダンプで出力します。ダンプの中身をテキストエディタで開き、文字コードに関連している記述をすべてUTF-8準拠に書き換えます。そしてnkfで実際にUTF-8へと書き換え、PostgreSQL 9.4へ流し込みます。ここは意外と問題になりませんでした。

ここで処理上の問題になるのはキャストの仕様変更になります。

キャストを追加してバージョン互換性を保つには
http://lets.postgresql.jp/documents/tutorial/cast/

PostgreSQL 8.3 では型変換のチェックが厳密になるよう仕様変更されました。特に文字列型への暗黙の型変換が無くなったため、以前のバージョンで動作していたアプリケーションであっても 8.3 上ではエラーが発生するかもしれません。

これがもろに該当しSQL実行時にエラーになるのですが、今はまだ修正しません。

PHP 5.6への入れ替え

先のOS入れ替えの時点でremiレポジトリからPHP 5.6を導入しています、ので、アプリが正常に動かない状態ではありますが、PHP 5.6で動作する環境となっています。

試しに動かしてみる

全然動きません。ここからひたすら修正していく作業になります。

静的解析と実際に動かして出るエラーをひたすら潰していく

Eclipse+PDTで作業しています。世間的にはPhpStormな潮流となっていますが、こういう元々動かないようなコードをPhpStormに放り込むとエラー数がすごい事になるので、PDTが静的解析する分が丁度よかったりします(PhpStormでも解析する範囲は調整できるのですが)。これは体感的な物なので実際はどちらでもいいでしょう。ただ、静的解析の無いテキストエディタでのコーディングはお勧めできません。

ここでIDEによる静的解析でエラーになっている物、ワーニングになっている物をひたすら修正していきます。例えば、元のコードでは連想配列を使用する際にほとんどの添え字がシングルクォーテーションで囲まれていなかったりしました。連想配列は相当な数が使用されているので、これが一番作業としてつらかったですね……。

register_globalsに関してはオンにしてはいけない、というかPHP5.6ではそもそも使用不可なので、各ファイルの先頭に全てのGETとPOSTを変数に展開するコードを一時しのぎで書いています。

foreach ($_POST as $post_name => $post_value) {
    if (isset($$post_name)) {
        continue;
    }
    $$post_name = $post_value;
}
foreach ($_GET as $get_name => $get_value) {
    if (isset($$get_name)) {
        continue;
    }
    $$get_name = $get_value;
}

PostgreSQLのキャストで問題になるエラーも、実際に動かしてみながらエラーが出る個所を潰していきます。一か所エラーが見つかれば、そのカラムを扱うところすべてが怪しいので、コード全体をカラム名で検索し該当箇所をまとめて修正します。

とりあえずPHP 5.6とPostgreSQL 9.4で動くようになった

実はまだバグが残っていたりしますが、機能として重要な部分は正常に動作する状態ですので、他は見つけ次第随時潰していく、という対応を取りました。何かしらフレームワークが使われていたらフレームワークまで含んだコード量やフレームワークのメンテナンス状態、それを入れ替えるための仕様変更部分もろもろの理由で諦めていたかもしれませんが、フルスクラッチなのでひたすらバグを潰していくだけで移行が出来ました。

CodeIgniterへの入れ替え

やっとCodeIgniterの話になります。既にPHP 5.6で動く状態ですので処理の移植は結構楽です。ちなみにここからIDEをPhpStormに切り替えています。

コントローラの作成

既存のアプリはGETとPOSTの値だけで複数のページを実装していましたが、素直に

/分類A/insert
/分類A/edit/xxx
/分類A/detail/xxx
/分類B etc…

というような形で動くコントローラに作り替えました。分類部分がコントローラのクラス、insertやedit、detailの部分がコントローラ内のメソッドになります。既存のWebアプリとはここでURLの互換性が一旦なくなります。コントローラでルーティングした後は、すぐにモデルに渡してしまう形にします。

モデルの作成

GETとPOSTで振り分けていた処理ブロックの中身をモデル、モデル内のメソッドに分割していきます。つまり元の処理をひたすらコピペをしていくような作業になります。また、register_globalsや先のregister_globals一時しのぎ処理で作られた変数については、$this->input->get() や $this->input->post() から取得するようにします。

SQL実行部分に関しては、組み立てたSQLをPHPのPostgreSQL関数ではなく、CodeIgniterのSQL実行関数 $this->db->query() に渡すようにします。SQLのエスケープ処理もCodeIgniterの標準エスケープ関数 $this->db->escape() に切り替えました。実行結果は元の実装が連想配列前提でしたので、 $query->result_array() で取り出します。DBアクセスに独自の仕組みではなくSQL文スクラッチのような形でも特に問題にならないのは、CodeIgniterへの移植のしやすさとして大きい部分だと感じます。

共通処理的なコードは適宜、メソッドでまとめるなり、(CodeIgniterでいう)ライブラリを追加するなりしました。

ビューの作成

今回はTwigを使用しました。CodeIgniterで使える形のTwigは @kenji_s 様の「CodeIgniter Simple and Secure Twig」が利用できます。
http://blog.a-way-out.net/blog/2015/05/25/codeigniter-twig/

元々の実装に有ったHTMLヘルパー的クラスは用意せず、シンプルにTwigを使ったテンプレートを作成していきました。

デザイン的にはUIフレームワークとして Bootstrap 3.x を利用するようにしました。そのため各UI部品、Webアプリ全体のデザインについてはあまり時間をかけずに現代的な物になりました。元々のWebアプリではフレームでメニューを表示していましたが、ヘッダーにメニューバー(navbar)を設ける形に作り替えました。

URL互換性レイヤー

なんてたいそうな名前を付けていますが、元のWebアプリとのURL互換性を取るためのレイヤーをCodeIgniterのコントローラとして作成します。

例としてURLが
/?function=機能A&page=detail&code=xxx
のようなものが来たら、GETとPOSTの値で判断し、
/機能A/detail/xxx
のURLにリダイレクトさせます。

public function functionA() {
    $get_arr = $this->input->get();
    // 機能A
    if (!empty($get_arr['functionA']) && !empty($get_arr['detail']) && !empty($get_arr['code'])) {
        redirect($url_base . '/FunctionA/detail/' . $get_arr['code'] . '/');
    }
}

この互換性コントローラの呼び出しは
application/config/routes.php
のルーティング設定に定義します。URLには都合よく旧アプリにしか含まれない文字列が有りましたので、

$route['文字列A:any'] = 'compatiblelayer/functionA';
$route['文字列B:any'] = 'compatiblelayer/functionB';

のように振り分けます。

基本的には新しいURLを使ってほしいですが、既存の資料なりブックマークなりで古いURLを持っていたしても、これを用意しておく事でURL互換性が保たれます。

所感

文量にも表れていますが、既存のコードをPHP 5.6で動かした場合にバグとなる部分を修正する作業が一番重いです。ただ、それを乗り越えると既存のフルスクラッチコードは簡単にCodeIgniterのコントローラ、モデル、ビューに分割していく事が可能でした。またDBアクセス部分に関しても、旧来のSQL組み立てロジック、PHPが標準で備える関数での実行、結果を連想配列として扱う処理がそのままに近い形で移行できます。どちらもCodeIgniterが独自の作法を極力持たない、フルスクラッチに近い感覚で書けるという良さが移植においても現れた物かと思います。太古のPHPアプリが有れば、その近代化の選択肢としてCodeIgniter 3.x 、いかがでしょうか?

CodeigniterのDB接続不可をハンドリングして再接続する

目的

  • CodeigniterのDB接続不可時に処理をエラーにせずなんとか再接続したい。

前提・用途

  • DBに繋がらないからといってクラッシュされては困るようなクリティカルな処理が有る。
  • Amazon RDSのマルチAZ等のように、冗長化されてはいるが数秒~2分程度のダウンタイムは発生するような構成であり、それに耐えたい。
  • Web APIを提供しているなど、瞬間的に大量のアクセスが発生しDBのmax_connectionを使い切るような状況が起こり得る。が、普段のアクセスは少ない。
  • Codeigniter標準のDBフェイルオーバー機能が使えるほど冗長化された構成ではない(=フェイルオーバー用のDBが無い)。
  • この記事はMySQLにmysqliを使って接続する前提になっています。

方法

Connecting to your Database — CodeIgniter 3.0.6 documentation
http://www.codeigniter.com/user_guide/database/connecting.html

$this->db->reconnect(); を使えば行けそうに思えますが、そもそも最初からDBに接続できなかった場合、reconnect() メソッドはただただfalseを返すような実装になっています。MySQLに上手く接続できているかどうかは、$this->db 、または$this->load->database(‘hoge’, true);が返すオブジェクトのプロパティ、conn_id を見る事で確認可能です。mysqliを使って接続している場合、このプロパティにはmysqliのオブジェクトが格納されており、接続できなかった場合は false が格納されます。

function reconnectDb()
{ 
    if ($this->db->conn_id !== false) {
         return;
     }
     log_message('error', 'DB接続失敗。再接続開始');
    // スクリプト実行可能時間を延ばしておく
     ini_set('max_execution_time', '180');
     for ($retry_count = 0; $retry_count < 8; $retry_count ++) {
         sleep(15);
         $this->load->database(); 
        if ($this->db->conn_id !== false) {
             log_message('debug', 'DB再接続処理OK');
             return; 
        } 
        log_message('debug', 'DB再接続処理NG');
     } 
}  

$this->db->conn_idがfalseであった場合はスクリプトの実行可能時間を延ばしたうえで、再度databaseのロードを試みます。上手く接続できた場合はfalseではなくmysqliのオブジェクトが入っており、DBを使う処理の続行が可能になります。上記のような処理を前述のクリティカルな場所に仕込んでおきます。DBがダウンしていたり、DB側のコネクション制限を使い切っている場合でも再接続の機会を得る事が出来ます。

注意点

  • あくまでDBへの再接続チャンスを得るための処理なので万能ではありません。上記例の場合、15秒ごとの8回のリトライに失敗すればDBには接続できないままです。
  • かなりの時間を待たせる処理になるので、クリティカルな処理、バッチ的な処理、API的な処理でのみ保険的に利用するべきでしょう。
  • バッチ的な処理など、それなりに時間がかかる処理の中で何度もDBにアクセスするような場合は、最初に接続確認を行っても途中でDBがダウンして接続出来なくなるような事も考えられます。$this->db->recconect() メソッドで conn_id プロパティを更新したうえで再度、再接続処理を実行する事もできるはずですが、運悪くDB処理の真っ最中にダウンしたような場合に対しては無力です。
  • CodeigniterのDB接続設定(database.php)にて db_debug を trueにしている場合、DB周りでのError・Warningはそのまま処理の即時停止となり、この再接続処理にたどり着きません。db_debug を false にしている環境でのみ動作します。
  • 上記と同様にディスプレイにPHPのエラーを表示する設定も、DB接続失敗時にWarningが発生しているため非推奨です。

用法、用量を守ってご参考程度に。

Gist lf-uraku-yuki/ci3-db-connect-retry.php