Quantcast
Channel: アイキューヴのスタッフ・ブログ » ZendFramework

ZendFramework –例えば、ログ出力

$
0
0

どうも、ぬまです。

前回から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

以上です。
さて、今度は何を作ろうかな。

人気ブログランキングへ


ZendFramework –我流でSmartyを使う

$
0
0

どうも、ぬまです。

引き続き、ZendFrameworkを。
ZendFrameworkのリファレンスガイドを初め、あちこちのサイトでZendFrameworkでのSmarty導入方法が紹介されています。
ですがどれも少し踏み込みが足りず、layoutに対応していなかったり、ビューヘルパーを使えなかったりします。てなわけで、以下を満たすようなSmarty実装をしてみました。

  • ビューヘルパーを利用できるようにする
  • layoutに対応する

例によってここでの内容をお試しになる際にはご自身の責任でお願いします。ちなみにZendFrameworkのバージョンは1.11.3で、PHPは5.3.5、Smartyは3.0.7を利用しています。
また、ディレクトリ構成はZendFrameworkリファレンスの「推奨されるプロジェクト・ディレクトリ構造」に沿っていることを前提にしています。

まずZend_View_Smartyクラス。リファレンスであるようにZend_View_Interfaceのインプリメントでなく、Zend_View_Abstractの継承で実現しています。基本はSmartyを利用する方法 – [Zend Framework] ぺんたん infoを参考にさせていただきました。
ただ、バージョンが合わなかったせいかそのままの実装ではlayoutが正常に動作しなかったため、setScriptPath() と addScriptPath() の辺りを複数のテンプレートパス指定に対応できるように修正しています。


require_once 'Smarty-3.0.7/libs/Smarty.class.php';

/**
 * Smarty利用のためのViewクラス
 *
 * SmartyタグをViewで利用可能にする
 *
 * @category   Zend
 * @package    View
 * @subpackage Smarty
 * @copyright  Copyright(C) 2011 IQUEVE Co.,Ltd.
 * @license    Undecided
 * @version    Release: @package_version@
 * @link       http://www.iqueve.co.jp
 * @since      none
 * @deprecated none
 */
class Zend_View_Smarty extends Zend_View_Abstract
{
    /**
     * Smarty object
     * @var Smarty
     */
    public $_smarty;

    /**
     * コンストラクタ
     *
     * @param string $tmplPath
     * @param array $extraParams
     * @return void
     */
    public function __construct($tmplPath = null, $extraParams = array())
    {
        $this->_smarty = new Smarty;

        if (null !== $tmplPath) {
            $this->setScriptPath($tmplPath);
        }

        foreach ($extraParams as $key => $value) {
            $this->_smarty->$key = $value;
        }
    }

    /**
     * テンプレートエンジンオブジェクトを返します
     *
     * @return Smarty
     */
    public function getEngine()
    {
        return $this->_smarty;
    }

    /**
     * テンプレートへのパスを設定します
     *
     * @param string|array $path パスとして設定するディレクトリ
     * @return void
     */
    public function setScriptPath($path)
    {
        $checkPaths = (array) $path;
        foreach ($checkPaths as $p) {
            if (!is_readable($p)) {
                throw new Exception('無効なパスが指定されました');
            }
        }
        $this->_smarty->setTemplateDir($path);
        return $this;
    }

    /**
     * テンプレートへのパスを追加します
     *
     * @param string|array $path パスとして設定するディレクトリ
     * @return void
     */
    public function addScriptPath($path)
    {
        $checkPaths = (array) $path;
        foreach ($checkPaths as $p) {
            if (!is_readable($p)) {
                throw new Exception('無効なパスが指定されました');
            }
        }
        $this->_smarty->addTemplateDir($path);
        return $this;
    }

    /**
     * 現在のテンプレートディレクトリを取得します
     *
     * @return string
     */
    public function getScriptPath()
    {
        return current($this->_smarty->template_dir);
    }

    /**
     * 現在のテンプレートディレクトリ全てを取得します
     *
     * @return string
     */
    public function getScriptPaths()
    {
        return $this->_smarty->template_dir;
    }

    /**
     * setScriptPath へのエイリアス
     *
     * @param string $path
     * @param string $prefix Unused
     * @return void
     */
    public function setBasePath($path, $prefix = 'Zend_View')
    {
        return $this->setScriptPath($path);
    }

    /**
     * addScriptPath へのエイリアス
     *
     * @param string $path
     * @param string $prefix Unused
     * @return void
     */
    public function addBasePath($path, $prefix = 'Zend_View')
    {
        return $this->addScriptPath($path);
    }

    /**
     * 変数をテンプレートに代入します
     *
     * @param string $key 変数名
     * @param mixed $val 変数の値
     * @return void
     */
    public function __set($key, $val)
    {
        // レイアウト利用のための措置
        if(is_object($key)){
            return;
        }

        $this->_smarty->assign($key, $val);
    }

    /**
     * empty() や isset() のテストが動作するようにします
     *
     * @param string $key
     * @return boolean
     */
    public function __isset($key)
    {
        return (null !== $this->_smarty->get_template_vars($key));
    }

    /**
     * オブジェクトのプロパティに対して unset() が動作するようにします
     *
     * @param string $key
     * @return void
     */
    public function __unset($key)
    {
        $this->_smarty->clear_assign($key);
    }

    /**
     * 変数をテンプレートに代入します
     *
     * 指定したキーを指定した値に設定します。あるいは、
     * キー => 値 形式の配列で一括設定します
     *
     * @see __set()
     * @param string|array $spec 使用する代入方式 (キー、あるいは キー => 値 の配列)
     * @param mixed $value (オプション) 名前を指定して代入する場合は、ここで値を指定します
     * @return void
     */
    public function assign($spec, $value = null)
    {
        if (is_array($spec)) {
            $this->_smarty->assign($spec);
            return;
        }

        $this->_smarty->assign($spec, $value);
    }

    /**
     * 代入済みのすべての変数を削除します
     *
     * Zend_View に {@link assign()} やプロパティ
     * ({@link __get()}/{@link __set()}) で代入された変数をすべて削除します
     *
     * @return void
     */
    public function clearVars()
    {
        $this->_smarty->clear_all_assign();
    }

    /**
     * テンプレートを処理し、結果を出力します
     *
     * @param string $name 処理するテンプレート
     * @return string 出力結果
     */
    public function render($name)
    {
        $this->_smarty->assign('layout', Zend_Layout::getMvcInstance());
        return $this->_smarty->fetch($name);
    }

    /**
     * Zend_Layoutを利用のためのメソッド
     *
     * @return object Zend_Layoutインスタンス
     */
    public function layout()
    {
        return Zend_Layout::getMvcInstance();
    }

    /**
     * 空実装
     *
     * @return mixed
     */
    protected function _run()
    {
    }
}

次にlayout対応のため application.ini を編集します。
以下はSmartyクラスへ渡すオプションとして、templates_cのディレクトリパスの他に、EC-CUBEなど使われているような形式のデリミタも含めています。

[production]
;(/*====================省略====================*/)
;;
;; View(Smarty)
;;
view.scriptPath = APPLICATION_PATH "/views/scripts"
view.viewSuffix = "tpl"
view.smarty.compile_dir = APPLICATION_PATH "/../data/templates_c"
view.smarty.left_delimiter = "<!--{"
view.smarty.right_delimiter = "}-->"

;;
;; Layout(Smarty)
;;
resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts"
resources.layout.viewSuffix = "tpl"

そうしてBootstrap.phpで初期化処理を行います。

    /**
     * View(Smarty)の初期化メソッド
     *
     * @params none
     * @return none
     */
    protected function _initView()
    {
        // 設定取得
        $objOptions = new Zend_Config($this->getOptions());
        $arrSmartyConf = $objOptions->view->smarty->toArray();

        // SmartyカスタマイズのViewクラス呼び出し
        require_once "Zend_View_Smarty.php";
        $objView = new Zend_View_Smarty(
            $objOptions->view->scriptPath,
            $arrSmartyConf
        );

        // ViewRendererの生成
        $objViewRenderer = new Zend_Controller_Action_Helper_ViewRenderer();
        $objViewRenderer->setView($objView)
           ->setViewBasePathSpec($objOptions->view->scriptPath)
           ->setViewScriptPathSpec(":controller/:action.:suffix")
           ->setViewScriptPathNoControllerSpec(":action.:suffix")
           ->setViewSuffix($objOptions->view->viewSuffix);

        // ViewRendererを追加する
        Zend_Controller_Action_HelperBroker::addHelper($objViewRenderer);
    }

さて、これで完成。
APPLICATION_PATH . “/layouts/scripts/layout.tpl”にレイアウトのテンプレートを配置すればlayoutのビュースクリプトをSmarty形式で作成できます。

<!--{$layout->content}-->

「zf create project」 コマンドで生成されるerrorコントローラのビュースクリプトなんかですと、以下のように書き換えます。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Zend Framework Default Application</title>
</head>
<body>
  <h1>ZFでの例外が発生...</h1>
  <h2><!--{$message}--></h2>

  <!--{if isset($exception)}-->
  <h3>Exception information:</h3>
  <p>
      <b>Message:</b> <!--{$exception->getMessage()}-->
  </p>

  <h3>Stack trace:</h3>
  <pre><!--{$exception->getTraceAsString()}-->
  </pre>

  <h3>Request Parameters:</h3>
  <pre>
  <!--{foreach from=$request->getParams() key="key" item="val"}-->
  <!--{$key}--> => <!--{$val}-->
  <!--{/foreach}-->
  </pre>
  <!--{/if}-->

</body>
</html>

従来のビュースクリプトでは「<? echo $this->exception->getTraceAsString();?>」などとするところを「<!–{$exception->getTraceAsString()}–>」という具合に$thisを外してしまいます。
さて、ビューヘルパーの利用方法。従来のビュースクリプトであればいきなり「<? echo $this->baseUrl();?>」とすればいいのですが、

// ビューヘルパーをアサインする
$this->view->assign("objBaseUrl", $this->view->getHelper("baseUrl"));

という風にコントローラで予めヘルパーオブジェクトをアサインしておいてから、

<!--{$objBaseUrl->getBaseUrl()}-->

とコールしなくてはなりません。この辺りがやっぱりデフォルトのビュースクリプトに比べると不便ではありますね。
でもそもそもSmartyとヘルパーにイイトコどりってのもムシが良すぎる気がするのでこんなところで我慢すべきなんでしょうか。Smartyの関数や修飾子、フィルタなんかを活用すればヘルパーが無しでも十分に様々な処理が可能ですし。ヘルパーとSmarty機能の両立は今後の課題ということにしておきましょう。

あと、ここまで実装しても、zf コマンドでコントローラやアクションを作成した時に自動生成されるビュースクリプトは「.phtml」のままです。
この辺りも修正したいところではありますが、今日は時間もあまりないのでこれまでということで。

次回はZendFrameworkからは少し離れて別のことを調べます。

人気ブログランキングへ