以前書いた「PHPでのログのとり方を考える」では、error_log()関数を使ったログ取りを書きました。
もう2年も前になりますが・・

なんか昔の自分のソースコードを見ると、初々しいと言うか頑張ってると言うか汚いというか・・
まぁ、いろんな感慨にふけったりと・・

組み込み関数もシンプルに書けるのでいいんですが、もっとバリエーションをガツンと出していきたいな ナンテね。。
そこでstaticなクラスとして作成してみようと思って書いてみました。
できるだけシンプルに使えるように気をつけながら・・


ログを取るクラス - Log.php
<?php
/**
* ログを取るクラス
*/
class Log
{
  protected static $_on= true; // 出力切替
  protected static $_prefix = '';
  protected static $_ext = '.log';
  protected static $_path = '';
  const MAXSIZE = 1; // ローテートサイズ[MB]

  /**
   * ログ出力をオフに
   */
  public static function off() {
    self::$_on= false;
  }
  /**
   * ログ出力をオンに(デフォルト)
   */
  public static function on() {
    self::$_on= true;
  }
  /**
   * メッセージログ出力
   */
  public static function info($s, $type="info") {
    if (self::$_on) {
      $file = self::_getfile($type);
      self::_rotate($type);

      // 日付付加
      $s = rtrim($s);
      if (preg_match('/\n|\r/', $s)) {
        $s = self::_dateline() . PHP_EOL
          . preg_replace('/(?:\r\n|\r|\n){1,}/', PHP_EOL, $s);
      } else {
        $s = "[" . self::_getdate() . "] " . self::_caller() . "\t" . $s;
      }
      if ($type === "dbg") {
        $s .= PHP_EOL
          . print_r(error_get_last(), true);
      }
      return file_put_contents($file, rtrim($s) . PHP_EOL, FILE_APPEND | LOCK_EX);
    }
  }
  public static function setpath($path) {
    self::$_path = realpath($path);
  }
  /**
   * エラーログ出力
   */
  public static function err($s) {
    return self::info($s, "err");
  }
  public static function setprefix($prefix) {
    self::$_prefix = $prefix;
  }
  protected static function _getdate($short=false) {
    date_default_timezone_set('Asia/Tokyo');
    return date($short ? 'Y-m-d' : 'Y/m/d H:i:s');
  }
  protected static function _dateline() {
    return str_pad(self::_getdate(), 56, "-", STR_PAD_BOTH);
  }
  protected static function _getfile($type, $rotate=false) {
    $file = rtrim(self::$_path, "/\\")
      . DIRECTORY_SEPARATOR
      . self::$_prefix
      . $type
      . ($rotate ? self::_getdate(true) : "")
      . self::$_ext;
    return $file;
  }
  /**
   * デバッグログ出力
   */
  public static function d($s='') {
    return self::info(print_r(debug_backtrace(), true), "dbg");
  }
  protected static function _rotate($type) {
    $file = self::_getfile($type);
    if (is_writable($file) && filesize($file) > 1024 * 1024 * self::MAXSIZE) {
      rename($file, self::_getfile($type, true));
    }
  }
  protected static function _caller() {
    global $argv;

    if (!empty($argv[0])) $caller = $argv[0];
    else if (isset($_SERVER['SCRIPT_NAME'])) $caller = basename($_SERVER['SCRIPT_NAME']);
    else $caller = 'Unknown';

    return $caller;
  }
}


実行スクリプト - test.php
<?php

// クラス定義読み出し
require_once '/path/to/Log.php';

// 保存先指定
Log::setpath("/path/to");

// メッセージログ出力
Log::info("保存したいメッセージを書く");


メッセージログ出力結果 - /path/to/info.log
[2012/01/14 23:42:02] test.php 保存したいメッセージを書く


ま、こんな感じでしょう。。
staticなクラスメソッドなのでインスタンス化しないで使える手軽さがあります。

保存するメッセージのところに、アクセスしてきた「ホスト名」「ユーザエージェント」「リファラー」等を入れると、アクセスログとして使えますね。

これまた以前に「PHPでIPアドレスを取得」で書いたものですが、realhost()関数を再利用できます。エコです。

Log::info(implode("\t", array(realhost(realaddr()), $_SERVER['HTTP_USER_AGENT'], $_SERVER['HTTP_REFERER'])));


こうやればできそうです。

統計や解析の材料に使えるのが、メッセージログといったところでしょうか。


では次、エラーログ出力の場合です。
処理の途中でエラーが出た場合に、どんなエラーが出たかを書いておいて後で確認する為に使います。
エラーの種類によっては処理を終了させる必要があるので、そんな時は終わらせる直前にどういったエラーが出たか判るように書いておきます。

実行スクリプト - test.php
<?php

// クラス定義読み出し
require_once '/path/to/Log.php';

// 保存先指定
Log::setpath("/path/to");

// エラーログ出力
Log::err("エラーメッセージを書く");


エラーログ出力結果 - /path/to/err.log
[2012/01/14 23:48:36] test.php エラーメッセージを書く


本来の使い方はifブロックに書くことになります。

if (条件) { // 成功したら
   // 処理継続
} else { // 失敗なら
  Log::err("○○条件エラー");
  exit; // 終了
}


エラーログはどんな使い方をするかと考えてみると、例えば「ログインで何度入力してもエラーが出る」というクレームが来た場合に備えて、どんな間違った入力がされたかを保存しておきます。
そうすれば「こういった間違った入力がされてる場合はエラーになります」と説明ができます。
同様のトラブルが多い場合は入力エラーメッセージで指摘したり、FAQを強化したりしてブラッシュアップできるわけです。


最後にデバッグですが、これは開発時に使います。
主に処理中の変数内容確認です。
割と怪しげな部分にあちこち書きまくるような使い方をすると思います。(原始的ではありますが)


実行スクリプト - test.php
<?php

// クラス定義読み出し
require_once '/path/to/Log.php';

// 保存先指定
Log::setpath("/path/to");

// デバッグログ出力
$foo = 1;
Log::d($foo);


デバッグログ出力結果 - /path/to/dbg.log
------------------2012/01/15 00:16:33-------------------
Array
(
  [0] => Array
    (
      [file] => C:\xampp\htdocs\test.php
      [line] => 11
      [function] => d
      [class] => Log
      [type] => ::
      [args] => Array
        (
          [0] => 1
        )
    )
)


[args]要素が渡した変数内容になります。
debug_backtrace()関数を使っているので、ネストの深い関数内から呼び出してもトレースしてくれると思います。

また、Log::d($val)をあちこちに書きまくったらその都度消すかコメントにしないといけませんが、開発中はいちいち消すのが面倒です。
なのでオフ機能を付けました。
以下のようにすればログ出力を抑えられます。


$foo = 1;
Log::d($foo); // ログ出力される

Log::off(); // オフに

Log::d($foo); // ログ出力されない
Log::d($foo); // ログ出力されない
Log::d($foo); // ログ出力されない

Log::on(); // オンに

Log::d($foo); // ログ出力される


その他、複数のシステムが混在する場合に対応できるようにログファイル名にプレフィックスを付加することができるようにしました。

Log::setprefix('MySystemA-');

Log::info("foo"); // MySystemA-info.log
Log::err("foo"); // MySystemA-err.log
Log::d("foo"); // MySystemA-dbg.log

Log::setprefix('MySystemB-');

Log::info("foo"); // MySystemB-info.log
Log::err("foo"); // MySystemB-err.log
Log::d("foo"); // MySystemB-dbg.log


あとはログ容量が膨れ上がった場合に備えて、ファイルサイズが定数値MAXSIZE(MB)を超えたら切り分けるようにしています。

今回作ったLogクラスは今後使っていく中で、よりよくなるようブラッシュアップしていけたらいいかなと思っています。
差し当たりLog::d()からは画面表示もさせた方がいいかも知れませんね。


前回は「コントローラ名」と「アクション名」を使ってアクションに処理が渡される部分を掘り下げました。

それではZend FrameworkのMVC設計でアプリケーション開発を実践的に行う第一歩として、トップページを作ることを考えましょう。

とは言っても既にこれまで使ってきた「コントローラ名」と「アクション名」がindexのものがあります。
このindex/indexはデフォルトのものなので、もっとアプリケーションらしい名前に変えるべきでしょう。
いかにもアプリケーションのトップページですよ的な名前が分りやすくていいですね。

「コントローラ名」=appli
「アクション名」  =top


これでどうでしょう?

で、これだけだとただ名前を変えただけです。
これにトップページの役割りとして、コントローラ名やアクション名が存在しない場合にエラー表示にしないで、トップページを表示させるようにしてみましょう。
できるだけエラー画面を出さない方がなんだか安定感のあるサイトだと印象付けられるんじゃないかという狙いです。(個人的意見)

名前を変えるにはフロントコントローラのオブジェクトが持つsetDefaultControllerName()メソッドおよびsetDefaultAction()メソッドに指定します。
たったこれだけです。

変更すると当然のことながらIndexControllerではなくAppliControllerクラスをデフォルトで呼ぶようになるのでこれを用意します。

以上がデフォルトの「コントローラ名」と「アクション名」を変えるやり方です。


次は、存在しない「コントローラ名」でリクエストが来た時に、デフォルト(appli)へ飛ばす仕組みを作ります。
これのやり方もフロントコントローラでuseDefaultControllerAlwaysパラメータを有効に設定するだけで済みます。
簡単ですね。


では最後に、存在しない「アクション名」でリクエストが来た時に、デフォルト(top)へ飛ばす仕組みを作ります。
これを実現させるにはアクションコントローラにPHPのマジックメソッド__call()を実装します。

それでは書いていきましょう。
フロントコントローラにはデフォルトの「コントローラ名」と「アクション名」を指定して、存在しないコントローラ名でリクエストが来たらデフォルトのコントローラを使う設定にします。

フロントコントローラ - index.php
<?php

// Zendパスを通す
set_include_path('C:\xampp');

// 必要なクラス定義を読み込む
require_once 'Zend/Controller/Front.php';

// アクションコントローラへ丸投げ
$front = Zend_Controller_Front::getInstance();
$front->setControllerDirectory('../application/controllers');
$front->setDefaultControllerName('appli');
$front->setDefaultAction('top');
$front->setParam('useDefaultControllerAlways', true);
$front->dispatch();


上記のように、たった3行を加えるだけで済みました。

フロントコントローラは基本的に書き変えるべきでないと以前書きましたが、アプリケーションがどれに変わったとしても共通な部分としてくくり出せるものはフロントコントローラに書くようにしていくといいと思います。
コード量が増えたとしてもたかが知れてるのでキチンと整理して書いていけば問題ないでしょう。

次はアクションコントローラに、存在しないアクションがリクエストされて来た場合にデフォルトのアクションを使う仕組みを追加します。

アクションコントローラ - AppliController.php
<?php
require_once 'Zend/Controller/Action.php';

class AppliController extends Zend_Controller_Action
{
  public function topAction() {

  }
  public function __call($name, $args) {

    if (substr($name, -6) === 'Action') {
      return $this->_redirect('/');
    }

    throw new Exception('Method not found: ' . $name);
  }
}


実装していないfooというアクション名でリクエストが来た場合に、fooAction()メソッドを呼ぼうとします。
ところがメソッドが無いので代わりにマジックメソッド__call()が呼ばれる流れになります。
__call()ではアクションを呼ぼうとして無かった場合のみリダイレクトしています。
_redirect('/')と指定すると、ベースURLへのリダイレクトとなります。
なので「コントローラ名」および「アクション名」が省略されたリクエストとなり、デフォルト値(つまりトップページ)を呼ぶことになります。

ビュースクリプト - appli/top.phtml
<html>
<body>
  ここがトップページだよん!
</body>
</html>


ファイルを配置できたら、ベースURLやトップページ、存在しないページURLをリクエストしてみましょう。

http://localhost/
http://localhost/appli
http://localhost/appli/top
http://localhost/appli/foo
http://localhost/appli.html
http://localhost/foo
http://localhost/foo/bar


どんなリクエストでもエラー画面にならずにトップページが表示できれば成功です!!

以前「Zend Frameworkを使う[3]」で作成したエラーコントローラには、コントローラが見つからない場合やアクションが見つからない場合の処理も書いていました。
今回の設定をすることにより、コントローラやアクションが見つからなくてもエラーへは行かなくなり、その部分は必要なくなるでしょう。

しかし、ZFで開発されたどんなアプリケーションにも対応できるよう残しておきましょう。
作っていけば行くほど、資産にしていきましょう。


前回はページ作成に関わる命名規則とページへのアクセス、それに伴い「コントローラ名」「アクション名」の存在をつまびらかにしました。

命名規則は、内部で処理を自動化するための大いなるカラクリとなります。


1.「コントローラ名」が分れば、命名規則によりアクションコントローラのファイル名が特定できます。

2.そしてファイルを読み込みます。

3.そこにはファイル名と同名のクラス名がある規則なので、クラスのインスタンスが生成できます。

4.「アクション名」が分れば、命名規則によりインスタンスのメソッド(アクションメソッド)をコールできます。


上記の流れはリクエストを受け取った「フロントコントローラ」でdispatch()メソッドをコールした時に自動的に行われる内部処理で、その結果「アクション」へ処理を引き渡しているわけです。

リクエストされたURIから、呼び出すべきアクション先を決定することを『ルーティング』、アクションメソッドをコールすることを『ディスパッチ』と言います。


一応、MVCイメージをつかめるように簡単な画像を作ってみました。

そろそろホンキ出す-Zend Framework MVC図

全リクエストを「フロントコントローラ」(設定ではindex.php)に集め、
「アクションコントローラ」へ丸投げし、
必要に応じてデータ入出力を「モデル」に依頼し、
必要に応じてページ描画を「ビュー」に依頼し、
レスポンスを返します。


なんとなくMVC互いの役割分担、連携の一端が見えてきたような気がします。