読者です 読者をやめる 読者になる 読者になる

FuelPHPを用いたPHPフレームワークのカスタマイズ

FuelPHP PHP バックエンド技術

初めまして、“なにわの天才プログラマ”です。
アプリ事業部でリードエンジニアをやっています。
詳しい自己紹介はMEMBERSページをご覧いただくとして…
今回私は「FuelPHPを用いたPHPフレームワークのカスタマイズ」について書きたいと思います。

フレームワークを使う理由

フレームワークは使い方次第でうまくメリットを出せない場合も多々ありますが

  • 汎用的なシステムを作って横展開をしようという思想があった
  • コーディング規約をフレームワークの規約で補いたかった
  • O/Rマッパーを使うことでオブジェクトキャッシュの仕組みをうまく構築したかった

など導入に繋がる要因はたくさんあったんですが、
結局のところ一人で一生作っていくものではないので

  • 人数が増えたとしても生産性やコードの可読性が落ちにくい
  • 急遽プログラムのメンテナンスする場合にやりやすい
  • 当時流行っていたCodeIgniterなどに近いもので初期導入のしやすさ

を大前提とし、初期開発速度の向上や運用時属人性の排除を主目的として実際は開発しました。

なぜFuelPHPなのか?

ちょうど2012年の10月ぐらいから『開発効率を上げるためにフレームワークを作りたい』という話が出始め、周りではCodeIgniterで作っているという話をよく聞いていたんですが、
CodeIgniterのライセンス問題がありFuelPHPの話を耳にし始めた時期でした。
上記のライセンス問題を無視して使おうとする動きもあれば、バージョンによって制約を逃れたり、CakePHPに戻ったり、Symfonyで開発したり多様なアプローチが可能でしたが、状況といろんなフレームワークを調査してみた上のFuelPHPを選択しました。

理由としましては、以下のとおりです。

  • CodeIgniterに似ていた&Kohanaより日本語ドキュメントが充実していた
  • FuelPHPが普及していき扱える人が多くなりそうだった
  • coreのクラス系のロジックを元のロジックを崩すことなく書きやすかった(後述)
  • O/Rマッパーの仕組みが手軽で導入しやすかった

これらの点から、FuelPHPは想定していたフレームワーク像に近く、実装に手を入れるコスト面が少なかった点があげられます。
また、公式ドキュメントやその他コミュニティの情報により、
ドキュメント作成やノウハウ事例も増えてきており、学習コストも削減できると判断できたため、
比較的導入がしやすく拡張性についても、既存の仕組みに沿った形で拡張がしやすいと判断し、
それが導入する決め手となりました。

FuelPHPを選定した後は、
主にエラー処理とデータベース処理周りをスッキリさせたかったので

  • 1.ベースコントローラの拡張(エラー処理をキャッチしやすく)
  • 2.O/Rマッパーの仕組みを拡張及び変更
  • 3.O/Rマッパーのプログラムを自動生成
  • 4.O/Rマッパーによるレスポンスの体系化
  • 5.マスタ管理ツールの自動生成

を拡張項目として検討し実装しました。

コントローラの拡張(主にエラー処理)

『独自エラー処理系をどう扱うか』という点において、
これから作成するコンテンツに特化した部分を差別化するために独自のエラー処理を実装しました。

主に実装した項目は3点です。

  • アプリケーションの意図したエラー
  • コンテンツが見つからないなど404(not found)系の制御
  • 課金などの重要な処理系を受け付けるコントローラーでのエラー制御

この仕組みにより、エラー制御に共通処理などを入れやすく、既存の処理に拡張を加えた形を提供できるようになりました。

また、スマホデバイスとの連携箇所に関してはJSONでデータ連携が必要で、
体系化や統一をすることが開発効率の向上につながる可能性が高かったため、
以下の4点を意識してベースのロジックを組み立てました。

  • レスポンスの体系化(エラーを含む)
  • 通信形式の共通化によりドキュメントにまとめやすい
  • 体系化することによる共通処理レスポンスにてメンテナンス性向上
  • 影響範囲が処理を統一することで明確化

こうすることで、スポットで処理ロジックを見てもらったり改修する場合に、影響範囲の共有と変更点、また追加する場合も例に習うことができ学習コストを減らすことのきっかけとなりました。

たとえば、下記のようなケースでも、

すべてのコントローラに対して拡張したベースコントローラーを継承させ、

class Controller_XXXX Xextentds Application_Controller(拡張コントローラ)
class Application_Controller extends Controller_Hybrid

として使うことにより元のメソッドbefore()前処理 after()後処理

をうまく使いながら特殊な対応も乗り切ることができました。

O/Rマッパーの仕組みを拡張

O/Rマッパーについては、標準の仕組みでmigrationすることをやめました。

理由としては、たとえばAlterTableなどスキーマ仕様を変更する場合にすべてFuelPHPで用意されたコマンドラインから行うと、それに慣れていないエンジニアに学習コストがかかってしまうためです。
標準の仕組みのmigrationはFuelPHPに特化した機能のため、デフォルトで実現できない仕組みがあった場合には、
新しい仕組みなどを入れる際に再度の学習コストやメンテナンスコストが発生してしまいます。
また運用ミスが起こった際には対応時に特殊な対処法や注意点などを踏まえる必要もあり、その場合の想定と検証をする時間がかかることもあり、
今回はフレームワークへの依存をしない仕組みを選択しました。

ですので、スキーマ変更などはSQLベースの運用にしデータベース
ーブルを先に生成してから、
そのテーブルに合わせてmodelクラスの生成を行う仕組みを作成しました。
実際にmodelクラスを作る場合はテーブル定義を最新にしコマンドを一つ叩くだけです。 図にすると、以下のような関係で自動生成されます。



こちらは
model/base/master/item.php
model/master/item.php
の2ファイルが存在し、baseディレクトリのclass側にテーブル定義やテーブルに直接関わる属性が入ります。
また複合primaryKeyへの対応をするためにpropertyを追加などしたかったので
自動で作ったメリットは共通化に大きく貢献してくれました。
PHPにはPropelと呼ばれるO/Rマッパーがあるんですが、そちらを参考に、
コネクション管理周りの思想をFuelPHPの標準O/Rマッパーに対して拡張しました。 その際、主に以下の点に気をつけて実装しました。 さらに、負荷分散などを考えるとDBコネクション管理を明確にしたかったので、以下のようにコネクションを渡せる実装としました。
これにより任意のタイミングでデータベースの接続先を変更できるようになりました。
$user_object = new User();
$user_attribute_object = new User_Attribute();
$con = \Fuel\Core\Database_Connection::instance();
try
{
    $con->start_transaction();
    // =============================
    // ▼以下一緒に紹介
    // 独自実装method(重複レコード系へのinsert処理を追加)
    // $user_object->insert_ignore(); 構文にinsert ignoreを追加してくれる
    // $user_object->on_duplicate_key_update(); 構文にon duplicate key updateを追加してくれ
    // =============================
    $user_object->save($con);
    $user_attribute->set('user_id', $user_object->get('id'));
    $user_attribute->save($con);
    $con->commit_transaction();
}
catch (\Exception $e)
{
    $con->rollback_transaction();
    throw $e;
}
select時にはデフォルトでインスタンスキャッシュをしてくれるのですが、
トランザクション中にどうしても外したい場合があったのと複合primaryKeyの参照などpkへの参照系を別に追加しました。
// ObjectInstanceを返す
Model_User::from_pk($pk);
Model_User::from_pk(array($multi_pk1, $multi_pk2), $con = null, $options = array());
// ObjectInstanceのarrayを返す
Model_User::from_pks(array($pk1, $pk2), $con = null, $options = array());
$conを渡した場合かつtransaction中の場合はキャッシュされたデータを見ない実装としました。
$optionsにはApplicationQuery::for_update(); <= array(‘for_update’ => TRUE)
select … for updateによるレコードロックの仕組みを追加
$optionにApplicationQuery::calc_found_rows(); <= array(‘calc_found_rows’ => TRUE);
SQL_CALC_FOUND_ROWSをサポート
selectした後に$query->found_rows($con);
を発行することでトータル件数を取得(MySQL特化なので意外と知らない人がいたり)
innoDBの場合count処理が遅いなどの問題が昔あったのですが、
それを解決するためにこの構文を使ったりしていたのですが、かなり便利なのでサポート。

from_pkメソッドAPC/Memcached/その他キャッシュ(主にKVS)へ前述のmodelクラスを、
primaryKeyから一意に識別されたレコードインスタンスをキャッシュして参照/変更をできるようにしました。
protected static $_cache_type = self::CACHE_TYPE_MEMCACHED;
とO/Rマッパーのmodelクラスに記述することで自動でキャッシュするロジックを追加するだけです。
当然saveのあとはキャッシュをクリアしたりトランザクション中はキャッシュをみない仕組みなども合わせて実装しています。 slaveへの書き込みなども過去の運用の経験上見たことが多々あるので
データベースの定義に’writable’ => FALSEと設定することで
save/deleteロジックでExceptionを吐く仕組みなども取り入れることにより
  • modelのデータキャッシュすることによる処理及びレスポンスの向上へ大きく貢献
  • クエリの発行のパターン化により可視性の向上
へ繋がりました。

O/Rマッパーによるレスポンスの統一とメンテナンスツールの自動生成

以下のような構造で各データのobjectインスタンスのレスポンスを統一しました。
// /controller/user.php
public function action_info()
{
    $user_objects = Model_User::from_pks(array(1,2,3));
    $payload = array();
    $payload['user_objects'] = Model_User::batch_adjust_array($user_objects,                Model_User::ADJUST_TYPE_NORMAL);
    $this-&gt;_assign($payload);
    $this-&gt;_display();
}
// /model/user.php
public static function batch_adjust_array(array $objects, $type = null, $options = array())
{
    $result_array = array();
    foreach ($objects as $object)
    {
        if ($object instanceof self)
        {
            $result_array[] = $object-&gt;to_adjust_array($type, $options);
        }
    }

    return $result_array;
}

/**
* Json向けに返す
*
* @static
* @return mixed array
*/
public function to_adjust_array($type = self::ADJUST_TYPE_NORMAL, $options = array())
{
    $output_array = array();
    switch ($type)
    {
        case self::ADJUST_TYPE_NORMAL:
        default:
            $output_array['id'] = $this-&gt;get('id');
            $output_array['name'] = $this-&gt;get('name');
            // ポイント値を取得(ソーシャルゲームの場合はJoinしたくない思想)
            $output_array['point'] = (string)Model_User_Point::from_pk($this-&gt;get('id'))-&gt;get('point');
            break;
    }

    return $output_array;
}
ソーシャルゲーム開発でControllerの中で煩雑に作ってしまうと、
レスポンスの内容が共通化されていないため一部を直すと修正箇所が複数となり、
いわゆるカオスな感じになりますが、この書き方でpayloadのパターンを統一することにより、
スマホデバイスや特定の情報を返すといった体系化された形へ柔軟に対応が可能となっています。
メンテナンス時の確認工数と修正工数が大幅に削減されました。

最後に

その他もっと修正変更はかけているのですが今回紹介したカスタマイズ方法では、
元のFuelPHPのコードが汚染されてないのが特徴です。
このように、bootstrap.phpにコア拡張と置換えができるところがFuelPHPを選定する一番大きな決め手ともなりました。(coreのバージョンアップ耐えやすいメリット)
Autoloader::add_classes(array(
    // Add classes you want to override here
    // Example: 'View' =&gt; APPPATH.'classes/view.php',
    'ApplicationServerErrorException' =&gt; APPPATH.'classes/exceptions.php',
    'ApplicationNotFoundException' =&gt; APPPATH.'classes/exceptions.php',
    'PurchaseErrorException' =&gt; APPPATH.'classes/exceptions.php',

    // コア拡張&amp;amp;置き換え
    'Database_Query_Builder_Select' =&gt; APPPATH.'classes/core/select.php',
    'Database_Query_Builder_Insert' =&gt; APPPATH.'classes/core/insert.php',
    'Database_Query_Builder_Update' =&gt; APPPATH.'classes/core/update.php',
    'Database_Query_Builder_Delete' =&gt; APPPATH.'classes/core/delete.php',
    'PhpErrorException' =&gt; APPPATH.'classes/core/error.php',
    'Error' =&gt; APPPATH.'classes/core/error.php',

    // taskのベース
    'Fuel\\Tasks\\Base' =&gt; APPPATH.'tasks/base.php',
));
参考URL:
http://press.nekoget.com/fuelphp_doc/general/extending_core.html
高速化に対するアプローチ(インスタンスキャッシュの有効利用)なども少しテクニックとして入れることにより、
メモリは多少多く使いますが最近のメモリの搭載量をうまく利用してメンテンス性と共に高速化へつなげることと、modelの自動生成から

input -> process -> output

の流れが見通しやすく初期開発速度の向上、
また運用時属人性の排除も同じような書き方となることから同時に解決がされました。 今回は、社内的に開発体制の統合の話などが出ていたことから、プログラムメインのような話となりましたが、
次回は、運用テクニックやインフラとの繋がりについてなども、お話させて頂ければと思っています。