PHP の error_log に syslog を指定した際に他のログと混ざらないようにする

PHP のランタイムのエラーログですが、ファイルを指定するより syslog 経由にしたほうが何かと楽です。
ファイルの場合は、ユーザ権限で実行している cli でのエラーと httpd でのエラーの両方を記録するために、
パーミッションをもしかしたら 777 とかにする必要があるかもしれませんが、
syslog の場合は特にそんなこと気にする必要がありません。

php.ini で下記の設定をするだけなのですが、これだけだとちょっと都合が悪いです。

sudo vim /etc/php.ini

error_log = syslog


というのも送信されるログのファシリティは user なので、例えば CentOS 6 だと /var/log/messages に出力されます。
他のログと混ざるので都合が悪いです。

じゃあ、ということで user.* を全部受けると、例えば yum でのインストールログも出力されるのでこれまた都合が悪いです。

で、本題ですがファシリティが user かつタグ (プログラム名) が httpd (mod_php) または php (cli) の場合だけ専用のログに出力するようにします。
一見めんどくさそうですが、CentOS 6 デフォルトの rsyslog だと、比較的簡単に設定できます。

sudo vim /etc/rsyslog.d/php.conf

$Umask 0000
$RepeatedMsgReduction off

$FileCreateMode 0644
$template php_log,"/var/log/php.log"

if ($syslogfacility-text == 'user') and \
   (($programname == 'httpd') or ($programname == 'php')) then \
       -?php_log

sudo /etc/init.d/rsyslog restart

で、syslog 経由なので、ログローテートも /etc/logrotate.d/syslog に /var/log/php.log を書いておけば OK です。

Zend_Oauth_Consumer でプロキシサーバを経由する方法

$config = array(
    'siteUrl'        => 'http://example.com/oauth',
    'consumerKey'    => 'xxxxx',
    'consumerSecret' => 'xxxxx',
    'callbackUrl'    => 'http://example.com/callback',
    'authorizeUrl'   => 'https://example.com/oauth/authorize',
);

$client = new Zend_Http_Client(null, array(
    'adapter'    => 'Zend_Http_Client_Adapter_Proxy',
    'proxy_host' => 'proxyサーバのホスト名',
    'proxy_port' => proxyサーバのポート番号,
));

$consumer = new Zend_Oauth_Consumer($config);
$consumer->setHttpClient($client);

Zend_Service_Twitter でプロキシサーバを経由する方法

$client = new Zend_Http_Client(null, array(
    'adapter'    => 'Zend_Http_Client_Adapter_Proxy',
    'proxy_host' => 'proxyサーバのホスト名',
    'proxy_port' => proxyサーバのポート番号,
));

$twitter = new Zend_Service_Twitter(array(
    'accessToken' => $access_token, // Zend_Oauth_Token_Access
));
$twitter->setLocalHttpClient($client);

Zend Studio を使って CLI をデバッグする

export QUERY_STRING="start_debug=1&debug_host=192.168.1.2&debug_port=10137&debug_stop=1&no_remote=1&debug_session_id=12345"

  をサーバに設定して、CLI プログラムを動かす
  debug_host: Zend Studio が動いている機械のホスト名/IPアドレス
  debug_port: Zend Studio のポート番号
  debug_session_id: 任意

PHP の cURL 実際にリクエストされたヘッダを確認する方法

  PHP で cURL 拡張モジュールを利用しているときに、デバック用に実際にリクエストしているヘッダの内容を見たいときがあります。
  その場合は、以下のように CURLOPT_VERBOSE と CURLOPT_STDERR を使って、ファイルに出力することが出来ます。

// 保存するファイル
$fp = fopen('/tmp/curl.log', 'a');
// 詳細な情報を出力する
curl_setopt($ch, CURLOPT_VERBOSE, true);
// STDERR の代わりにエラーを出力するファイルポインタ
curl_setopt($ch, CURLOPT_STDERR, $fp);


  例として、Google へのアクセスを見てみます。

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'http://www.google.com/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

// 保存するファイル
$fp = fopen('/tmp/curl.log', 'a');
// 詳細な情報を出力する
curl_setopt($ch, CURLOPT_VERBOSE, true);
// STDERR の代わりにエラーを出力するファイルポインタ
curl_setopt($ch, CURLOPT_STDERR, $fp);

$ret = curl_exec($ch);

fclose($fp);
curl_close($ch);


  これを実行すると、/tmp/curl.log には以下のように出力されます。

* About to connect() to www.google.com port 80 (#0)
*   Trying 66.249.89.147... * connected
* Connected to www.google.com (66.249.89.147) port 80 (#0)
> GET / HTTP/1.1
Host: www.google.com
Accept: */*

< HTTP/1.1 302 Found
< Location: http://www.google.co.jp/
< Cache-Control: private
< Content-Type: text/html; charset=UTF-8
< Set-Cookie: PREF=ID=c09b07d3b330fa72:TM=1234774504:LM=1234774504:S=Omlx1u3jcFB3egKo; expires=Wed, 16-Feb-2011 08:55:04 GMT; path=/; domain=.google.com
< Date: Mon, 16 Feb 2009 08:55:04 GMT
< Server: gws
< Content-Length: 221
* Connection #0 to host www.google.com left intact

Zend Studio Toolbar のダウンロード

  My Zend にログインして、下記ページを開く。
  http://www.zend.com/en/products/studio/downloads
  Studio Browser Toolbars を選択して、ダウンロード

Zend Studio Web Debugger のアップグレード

  Zend Studio 6.1.0 にアップグレードしたら、Web Debugger が古いとかいわれた。
  
  My Zend にログインして、下記ページを開く。
  http://www.zend.com/en/products/studio/downloads
  Studio Web Debugger を選択して、ダウンロード (Linux x86 (32 bit) (TAR.GZ) | 2.45 MB | 5.2.14)。
  
  /usr/local/Zend/Platform/lib/Debugger-5.2.14 として展開して、

$ mv 4_3_x_comp php-4.3.x
$ mv 4_4_x_comp php-4.4.x
$ mv 5_0_x_comp php-5.0.x
$ mv 5_1_x_comp php-5.1.x
$ mv 5_2_x_comp php-5.2.x

  で、ディレクトリをリネームする。

  • 設定

  /usr/local/Zend/Core/etc/php.ini を変更する

zend_extension_manager.debug_server=/usr/local/Zend/Platform/lib/Debugger-5.2.14

  • 確認

  Apache を restart して、php -i または phpinfo() で Zend Debugger v5.2.14 になっていれば OK。

APC と jQuery を利用してファイルアップロードの進行状況を表示する

  ネタ的には特別新しいものではないですが、気になっていたので試してみました。
  PHP 5.2.0 以降、APC 3.0.13 が必要です。

1. APC の設定

  普通に APC をインストールして、以下のように apc.rfc1867 を有効にするだけです。

apc.rfc1867 = On

2. アップロードフォームの準備

  普通のアップロードフォームです。
  ただし、"APC_UPLOAD_PROGRESS" という name 属性を持った hidden フィールドを用意します。値は、何でもいいのですが、ここでは "progress_key" という風にしています (本当はランダムにするほうがいいと思います)。

<form id="upload" action="upload.php" method="post" enctype="multipart/form-data">
<p>
<input type="hidden" name="APC_UPLOAD_PROGRESS" value="progress_key" />
<input type="file" name="file" />
<input id="submit_button" type="submit" value="アップロード" />
</p>
</form>

3. アップロードファイルの処理

  上記アップロードフォームの upload.php ですが、これは通常のファイルアップロード時の処理を書いてください。
  今回はテストなので何もしませ

<?php
// 通常のファイルアップロードの処理
?>

4. アップロードの進行状況を返す処理

  次のファイルを progress.php とします。

<?php
// progress.php
header('Content-type: application/json; charset=UTF-8');
$status = apc_fetch('upload_progress_key');
echo json_encode($status);
exit;
?>


  2 で指定した "APC_UPLOAD_PROGRESS" の値の先頭に upload_ をつけたものを apc_fetch() の引数に指定します。upload_ は設定で変更することが出来ます (apc.rfc1867_prefix)。
  apc_fetch() で取得できる値は次のようになります。

total          アップロードされるファイルのサイズ                              
current        現時点までに受信したファイルのサイズ                            
rate           アップロード速度 (byte/second)           アップロード完了時のみ
filename       ファイル名                                                      
name           <input type="file" /> の name 属性                              
temp_filename 一時ファイル名                           アップロード完了時のみ
cancel_upload アップロードがキャンセルされたかどうか   アップロード完了時のみ
done           アップロードが完了したかどうか                                  
start_time     アップロード開始日時の UNIX TIME                                
  • cancel_upload: 0 = キャンセルされていない、1 = キャンセルされた
  • done: 0 = 未完了、1 = 完了

  これらの値を JSON で出力しています。

5. jQuery を使って進行状況を取得する

  jQuery Form Plugin を使って、フォームを POST し、1 秒毎に getJSON で進行状況を取得します。
  また、getJSON でリクエストに現在日時を渡しているのは、ブラウザのキャッシュを利用しないようにするためです。

var timer = null;

var progress = function() {
    // progress.php を呼び出して進行状況を取得する
    $.getJSON('progress.php', { 'd': new Date().getTime() }, function(json) {
        // 進行状況を % で表示する
        $('#status').html(parseInt(json.current / json.total * 100) + '%');
    });
};

$(function() {
    $('#upload').submit(function() {
        timer = setInterval('progress()', 1000);
        // フォームを POST する
        $(this).ajaxSubmit(function() {
            clearInterval(timer);
            progress();
        });
        return false;
    });
});

6. デモ

  本当は、デモを用意したかったのですが、ここのレンタルサーバでは APC が使えないようでしたのでアップロード状況をキャプッチャしました。
  基本的には、上記の処理を行なっているだけですが、進行状況の表示の部分は Progress Bar Plugin を利用して、プログレスバーで表示しています。



  このデモで使用したファイル一式を以下に置いておきます。

参考


- PHP V5.2 の新機能、第 5 回: ファイル・アップロードの進行状況を追跡する方法
  http://www.ibm.com/developerworks/jp/opensource/library/os-php-v525/

- upload meter for PHP with APC and Json
  http://progphp.com/progress.phps

- PHP: APC 関数 - Manual
  http://php.net/apc

- jQuery Form Plugin
  http://malsup.com/jquery/form/

- Progress Bar Plugin
  http://digitalbush.com/projects/progress-bar-plugin

PEAR::setErrorHandling でエラー処理を行なう

  PEAR パッケージのエラー処理ですが、よくあるパターンとしては以下のような書き方をすると思います。

$result = $mdb->query('SELECT * FROM table');
if (PEAR::isError($result)) {
    die($result->getMessage());
}


  エラー処理の数が少ない場合はこれでいいのですが、多くなってくると可読性が悪くなります。

  この場合、PEAR::setErrorHandling() を使うことで以下のように簡潔に書くことができます。

PEAR::setErrorHandling(PEAR_ERROR_DIE);

$mdb2->query('SELECT * FROM table');
// PEAR:isError() を使わなくていい


  この書き方は、マニュアルによると「PEAR_Error の生成時に、エラーメッセージが出力され、スクリプトが終了します。」ということになります。

  エラーが発生したら、ログを出力して終了したいという場合は、以下のようにコールバックを利用することができます。

PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError');

function handleError($error)
{
    require_once 'Log.php';
    $log = &Log::singleton('file', 'error.log');
    $log->err($error->getMessage());
    exit('エラー');
}

$mdb2->query('SELECT * FROM table');


  PEAR パッケージをよく使う場合には便利です。

- PEAR::setErrorHandling()
  http://pear.php.net/manual/ja/core.pear.pear.seterrorhandling.php

PEAR::Pager で Digg っぽいページャを作る

  UI Patterns紹介されているように Digg のページャは見た目にも綺麗だし、わかりやすいと思います。
  以下の画像が Digg のページャです。

  Digg Pager

  そこで、PEAR::Pager を使って Digg っぽいページャを作ってみました。

- スクリーンショット
  PEAR::Pager Digg Pager

- デモページ
  http://pocari.org/demo/digg-pager/

  完全に一緒というわけではありませんが、これくらいならば結構簡単に作ることができました。

  ソースは以下のようになります。

<?php
require_once 'Pager/Pager.php';

$params = array(
    'mode'                  => 'sliding',
    'perPage'               => 10,
    'delta'                 => 5,
    'separator'             => '',
    'curPageLinkClassName'  => 'current',
//  上記はこれと同じ意味
//  'curPageSpanPre'        => '<span class="current">',
//  'curPageSpanPost'       => '</span>',
    'prevImg'               => '&#171; Previous',
    'nextImg'               => 'Next &#187;',
    'firstPagePre'          => '',
    'firstPagePost'         => '',
    'lastPagePre'           => '',
    'lastPagePost'          => '',
    'spacesBeforeSeparator' => 0,
    'spacesAfterSeparator'  => 0,
    'totalItems'            => 2180,
    'altFirst'              => 'Go to page 1',
    'altPrev'               => 'Go to Previous Page',
    'altNext'               => 'Go to Next Page',
    'altLast'               => 'Go to Last Page',
    'altPage'               => 'Go to page',
);

$pager =& Pager::factory($params);
$links = $pager->getLinks();
$page_range = $pager->getPageRangeByPageId();
$page_range = range($page_range[0], $page_range[1]);

$link = '';
if ($links['pages'] != '') {
    // 前のページ
    if ($links['back'] != '') {
        // クラスを付ける
        $link .= str_replace('<a href', '<a class="nextprev" href', $links['back']);
    } else {
        $link .= '<span class="nextprev">' . $pager->getOption('prevImg') . '</span>';
    }
    // 最初のページ
    if ($links['first'] != '' && !in_array(1, $page_range)) {
        $link .= $links['first'] . '<span>....</span>';
    }
    // ページ
    $link .= $links['pages'];
    // 最後のページ
    if ($links['last'] != '' && !in_array($pager->numPages(), $page_range)) {
        $link .= '<span>....</span>' . $links['last'];
    }
    // 次のページ
    if ($links['next'] != '') {
        // クラスを付ける
        $link .= str_replace('<a href', '<a class="nextprev" href', $links['next']);
    } else {
        $link .= '<span class="nextprev">' . $pager->getOption('nextImg') . '</span>';
    }
}
?>
<!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" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <link rel="stylesheet" type="text/css" href="style.css" />
  <title>PEAR::Pager で Digg っぽいページャを作るデモ</title>
</head>

<body>

<div class="pager">
<?php echo $link; ?>
</div>

</body>
</html>


  CSS は以下のようになります (Digg のものとほとんど同じです)。

div.pager {
    margin-top: 20px;
    font-size: 85%;
}

div.pager a,
div.pager span {
    display: block;
    float: left;
    margin-right: 0.1em;
    padding: 0.2em 0.5em;
}

div.pager a {
    color: #105cb6;
    background-color: #fff;
    border: 1px solid #9aafe5;
    text-decoration: none;
}

div.pager a:hover {
    color: #003;
    background-color: #fff;
    border: 1px solid #2e6ab1;
}

div.pager span.current {
    color: #fff;
    background-color: #2e6ab1;
    border: 1px solid #2e6ab1;
    font-weight: bold;
}

div.pager span.nextprev {
    border: 1px solid #ddd;
    color: #999;
    background-color: #fff;
}

div.pager a.nextprev {
    font-weight: bold;
}