どうも、ぬまです。
前回からZendFrameworkに注力し始めたのですが、なかなか思い通りにはいきません。
どこかのサイトで日本でZendFrameworkよりCakePHPが好まれているのは、ZFがCakeより自由度が高いためかえって敬遠されている、との見解が書かれていました。そのサイトの著者は日本ではルールがたくさんある野球の方が自由度の高いサッカーよりも好まれることを例に挙げていました。なるほど。
確かにZFはフレームワークというよりはライブラリと言った方かピンとくるような位置付けのものとなっていますね。Cakeのようにガチガチにルール化することで、実現の可否を明確にしてしまって生まれる効率化も確かにあります。フレームワークは所詮はツールなのでそれほど深く掘り下げずに留めるのは賢い選択肢の1つというのも頷ける。
『縛られたほうがイイ』なんて思ってしまうのは日本の国民性なんですかねえ。日本以外の話はよく知らないのでなんとも言えませんが、確かに日本人は野球が好きですね。私は野球はあんまり好きじゃないですが、ふと気づくとかなり詳細なルールまで体に染みついているのに驚いたりします。(スリーバントやタッチアップ、考えて見ればどれもややこしいルールです)
反面、企業相手のソリューション開発なんかやっている立場から言わせてもらうと、様々な企業からヘビィなローカルカスタマイズが課題になること多かったりもします。そういった視点からみると、フレームワークへ求められているものとしては自由度が高く応用が効いた方が良いかと思ったのですが。何にしてもなかなか難しいもんです。
今回はもう少し中身について。Zend_Logについてやってみました。
ZendFrameworkのデフォルトのプロジェクトでログを利用するのは至って簡単。
application.iniに以下を記述するだけです。ZFのプログラマ向けリファレンスガイド参照
resources.log.stream.writerName = "Stream" resources.log.stream.writerParams.stream = "(出力先)" resources.log.stream.filterName = "Priority" resources.log.stream.filterParams.priority = 7 ;// DEBUGまで全部 resources.log.stream.formatterName = "Simple" resources.log.stream.formatterParams.format = "%timestamp%[%priorityName%][%user%] %message%" PHP_EOL
これだけで特にプログラミングせずにログを記述できます。
呼び出し側ではphpのコントローラークラス等で以下のように書きます。
$bootstrap = $this->getInvokeArg("bootstrap"); if ($bootstrap->hasResource("Log")) { $log = $bootstrap->getResource("Log"); $log->setTimestampFormat("y/m/d-h:i:s"); $log->setEventItem("user", "hiranuma"); $log->info("参考情報"); $log->emerg("緊急事態"); }
ちなみに、resources.log.stream.formatterParams.format で指定した %user% を Zend_Log::setEventItem() で設定しています。ログインしているアカウントの情報を埋め込んだりできますね。
Zend_Logオブジェクトを呼び出す処理ですが少し冗長ですね。デフォルトのErrorControllerクラスから持ってきたのですが。今後のお勉強の課題にしときます。
出力したログはこちら。
11/03/03-08:33:31[INFO][hiranuma] 参考情報 11/03/03-08:33:31[EMERG][hiranuma] 緊急事態
Zend_Log_Filter を設定すればエラーログ出力のプライオリティを変更できるし、デフォルトのログとしては十分です。
Zend_Log_Writer の派生クラスを変更(application.ini の .writerName)すれば出力先も変更できます。XML形式やシスログ形式、メール送信はもちろん、Fire Bugにまで出力できるんですね。今度やってみよ。
上記以外にも直接Zend_Log クラスのインスタンスを生成する方法もありますが、それは特に難しくもないので省略します。
このWriterのバリエーションとしてテキストファイル出力は Zend_Log_Writer_Stream を指定するのですが、残念ながらローテーション機能はありませんでした。
てなわけで、せっかくなんで作ってみました。ちょっと時間掛かりましたが…。
仕様としては以下の通りです。
- Zend_Log_Writer_Stream クラスを継承
- コンストラクタでのファイルオープンのタイミングでローテート処理を実施
- INIファイルから保持する履歴ファイル数を指定できる
- INIファイルで指定すれば、ファイルサイズ超過のタイミングで xx.1 xx.2 といった具合にローテート
- Zend_Date形式の日付指定を行えば時間経過のタイミングでのローテートも可能
- 時間経過ポリシーとファイルサイズポリシーの双方の指定もできる(ファイル名は 『xxxx-(日付).(数値)』となる)
- ファイルロック等の排他制御には非対応(これも今後の課題ですね)
※諸々の都合で一部の処理を割愛しました。もったいぶってすみません。
// 親クラスのインクルード require_once 'Zend/Log/Writer/Stream.php'; /** * ローテーション付きストリームログ出力クラス * * Zend_Log_Writer_Stream の派生クラス。ローテーションを自動で実施。 */ class Zend_Log_Writer_RotationStream extends Zend_Log_Writer_Stream { const SUFFIX_SEP_IDX = "."; // インデックス用サフィックスセパレータ const SUFFIX_SEP_DAT = "-"; // 日時用サフィックスセパレータ const DEFAULT_MAX_BACKUP = "30"; // デフォルトのファイルバックアップ数 const DEFAULT_FORMAT = "YMMdd"; // デフォルトのサフィックスフォーマット const DS = DIRECTORY_SEPARATOR; // パス区切りの省略 /** * コンストラクタ * * @param array|string|resource $stream Streamクラスに準じる * @param string|null $mode Streamクラスに準じる * @param array $configPolicy ポリシー定義配列 * @return void * @throws Zend_Log_Exception */ public function __construct($stream, $mode = null, $configPolicy = array()) { // デフォルト設定 if (null === $mode) { $mode = 'a'; } // ポリシー定義配列の初期化 if (empty($configPolicy)) { $configPolicy = self::configPolicy(); } if (is_resource($stream)) { if (get_resource_type($stream) != 'stream') { require_once 'Zend/Log/Exception.php'; throw new Zend_Log_Exception('Resource is not a stream'); } if ($mode != 'a') { require_once 'Zend/Log/Exception.php'; throw new Zend_Log_Exception('Mode cannot be changed on existing streams'); } $this->_stream = $stream; } else { if (is_array($stream) && isset($stream['stream'])) { $stream = $stream['stream']; } // ローテート処理 $this->_rotate($stream, $configPolicy); if (! $this->_stream = @fopen($stream, $mode, false)) { require_once 'Zend/Log/Exception.php'; $msg = "\"$stream\" cannot be opened with mode \"$mode\""; throw new Zend_Log_Exception($msg); } } $this->_formatter = new Zend_Log_Formatter_Simple(); } /** * ローテーションポリシー定義のテンプレートを返す * * @param none * @return array ローテーションポリシーの定義のテンプレート */ static public function configPolicy() { $arrConfigPolicy = array( 'maxBackupIndex' => null, // バックアップ数 'maxSize' => null, // 最大バイト数 'datetimeFormat' => null, // 日時ローテーション時のフォーマット ); return $arrConfigPolicy; } /** * ファクトリーメソッド * * @param array|Zend_Config $config Streamクラスに準じる(ただし項目は追加) * @return Zend_Log_Writer_Stream Streamクラスに準じる */ static public function factory($config) { $config = self::_parseConfig($config); $config = array_merge(array( 'stream' => null, 'mode' => null, ), $config); // ポリシー定義の初期化 $policies = self::configPolicy(); $configPolicy = array_merge($policies, $config); foreach (array_keys($configPolicy) as $key) { if (array_key_exists($key, $policies) == false) { unset($configPolicy[$key]); } } return new self( $config['stream'], $config['mode'], $configPolicy ); } /** * ファイルサイズの数値化 * * @param string $string サイズの文字列表記 * @return integer サイズ数値 */ protected function _sizeToString($string) { // 不要な文字を除く $trimming = preg_replace("/[^0-9KM]/u", "", strtoupper($string)); // 抽出 if (preg_match("/^([1-9]\d*)([KM])?$/u", $trimming, $m) == false) { // 合致しない値です return null; } $integer = $m[1]; if (isset($m[2])) { switch ($m[2]) { case "K" : $integer *= 1024; break; case "M" : $integer *= 1024 * 1024; break; default : break; } } return $integer; } /** * 日付サフィックスを生成 * * @param string $format Zend_Date用フォーマット * @param integer|null $tp タイムスタンプ * @return string 日付文字列 */ protected function _dateSuffix($format, $tp = null) { if (is_null($tp)) { $tp = time(); } $objDate = new Zend_Date($tp); $date_str = $objDate->get($format); // ファイルにふさわしくない文字は除く $suffix = str_replace( array(" ", self::SUFFIX_SEP_DAT, self::DS), array("_", "_", "_"), $date_str ); return $suffix; } /** * ログファイルのローテーション処理 * * @param string $stream ファイルパス * @param array $config ローテーションポリシー定義 * @return void * @throws Zend_Log_Exception */ protected function _rotate($stream, $config) { // 最大バックアップ数 $maxBackupIndex = is_null($config["maxBackupIndex"])? self::DEFAULT_MAX_BACKUP : $config["maxBackupIndex"]; // サイズローテート時の基準値 $maxSize = $this->_sizeToString($config["maxSize"]); // 日付フォーマット(サイズ指定と双方とも空白ならデフォルト値を利用) $format = (is_null($config["datetimeFormat"]) && is_null($maxSize))? self::DEFAULT_FORMAT : $config["datetimeFormat"]; // フォーマットのチェック if (!is_null($format) && $this->_dateSuffix($format) == "") { require_once 'Zend/Log/Exception.php'; throw new Zend_Log_Exception('サフィックスフォーマットが不正です'); } if (file_exists($stream) == false) { return; // ローテーション不要 } if (!is_writable($stream)) { require_once 'Zend/Log/Exception.php'; throw new Zend_Log_Exception('ストリームパスが不正です'); } $dir = dirname($stream); if (!is_readable($dir) || !is_writable($dir)) { require_once 'Zend/Log/Exception.php'; throw new Zend_Log_Exception('対象ディレクトリが不正です'); } $base = basename($stream); // 該当のファイルリストを取得 $files = array(); $fmt = "/^%s(?:%s([^%s]+))?(?:%s([1-9]\d*))?$/u"; $ptn = sprintf($fmt, preg_quote($base), preg_quote(self::SUFFIX_SEP_DAT), self::SUFFIX_SEP_IDX, preg_quote(self::SUFFIX_SEP_IDX) ); foreach (scandir($dir) as $file) { if (preg_match($ptn, $file, $m) == true) { $key1st = (isset($m[1]) && $m[1] != "")? $m[1] : $base; $key2nd = (isset($m[2]) && $m[2] != "")? $m[2] : "0"; $files[$key1st][$key2nd] = realpath($dir . self::DS . $file); } } // 日付でのローテーションを実施するか? /* .....割愛..... */ // ファイルサイズでのローテーションを実施するか? /* .....割愛..... */ // ファイルを処理別に整理 /* .....割愛..... */ // 古いものから順に削除 /* .....割愛..... */ // 順次リネーム /* .....割愛..... */ } }
上記のファイルをBootstrap.phpあたりでインクルードして、application.iniにファイルに設定を書きます。stream以外は未設定時にはデフォルト(30バックアップ、日付YMMdd、サイズなし)で動作します。
resources.log.stream.writerName = "RotationStream" resources.log.stream.writerParams.stream = "(出力先)" resources.log.stream.writerParams.maxBackupIndex= 10 resources.log.stream.writerParams.datetimeFormat= YYMMdd resources.log.stream.writerParams.maxSize= 1M
以上です。
さて、今度は何を作ろうかな。