gmmktime()の挙動の違い2008年03月01日 00時43分08秒

ここしばらくgelatoがマイブームなのでこにょこにょといじっていたりするのだが、JSAスクリプトからPOSTした場合とadminページからPOSTした場合で時差がでることに気がついた。

自宅やら会社の実験サーバやら開発用PCやら何箇所かgelatoを入れているのだが、このうちあるホストだけ時差が発生する。 調べてみると時差の源は、gelatoが新規エントリを追加する際のタイムスタンプを「gmmktime」で生成している部分らしかった。

このあたりはまたgelatoネタとして別エントリにする予定なのだが、なんで違いが出たり出なかったりするんだろうかといろいろ試したところ、PHP4とPHP5で挙動が違う場合があることが判明した。(ひょっとして有名なネタかしら?)

問題のgelatoのコードでは

gmmktime()
と、パラメータなしで使用している。ここがミソ。

こんな検証コードを書いてみた。

<?php
    $current_time = mktime();
    $d = array();
    foreach( split(',', 'H,i,s,m,d,Y') as $key ) {
        $d[ $key ] = date( $key, $current_time );
    }
    foreach( array(
        array(
            'name' => 'mktime()',
            'value' => $current_time
        ),
        array(
            'name' => 'gmmktime()',
            'value' => gmmktime()
        ),
        array(
            'name' => 'gmmktime(mktime())',
            'value' => gmmktime(
                $d['H'], $d['i'], $d['s'], $d['m'], $d['d'], $d['Y']
            )
        )
    ) as $conf ) {
        $date = date('Y-m-d H:i:s', $conf['value']);
        echo "{$conf['name']} --> $date\n";
    }
?>
mktime()で基準のタイムスタンプを取得し、
  • パラメータなしのgmmktime()
  • 基準タイムスタンプを元にしたgmmktime()
を出力しているだけだ。

これをPHP5で実行するとこんな感じ。

-sh-2.05b$ php test.php
mktime() --> 2008-03-01 00:22:57
gmmktime() --> 2008-03-01 00:22:57
gmmktime(mktime()) --> 2008-03-01 09:22:57
同じコードをPHP4で実行するとこんな感じ。
-sh-2.05b$ php4 test.php
mktime() --> 2008-03-01 00:23:01
gmmktime() --> 2008-03-01 09:23:01
gmmktime(mktime()) --> 2008-03-01 09:23:01

これを見ると、パラメータなしでgmmktimeを実行すると、PHP4では現在のローカル時刻が与えられたものとして動作し、PHP5ではmktime()と同じ動作をすることがわかった。

これ、マニュアルには特に何も書いてないので、もうちょっと細かいバージョンの違いがでるのかもしれない。

mktime()はパラメータなしの場合に「現在時刻を与えられたもの」として振舞っているので、どっちかっていうとPHP4の挙動のほうが正しい感じはするんだけどね。

しかし、普段はZend_Dateばっかり使っているのであんまり役に立たない発見な気が。

expandimage.jsがSafari for Winに冷たい件2008年03月02日 05時01分54秒

このブログのサイドバーに「最近のブックマーク」というのを貼っている。自分のtumblrのlinkのみをjson apiで抽出して並べる、というものだ。

で、だいたいtumblrにlinkをpostするときは、何についてのページなのかを忘れないように引用をつけたりしていて、「最近のブックマーク」ではその部分の表示をトグル切り替えするようにしている。

さて、本題。

なんとなくのノリで、「最近のブックマーク」をちょっと調整していたのだが、Safari for Winではトグル切り替えがまったく動作しない。

「最近のブックマーク」の独立版であるこちらのページは問題なくトグル切り替えができているし、他のブラウザでも特に問題がない。

トグル切り替え部分のイベント登録には、prototype.jsのEvent.observe()を使っているのだが、そこに登録しているイベントハンドラで、UAがSafariだった場合にalert()するデバッグコードを仕込んだところ、これも実行されない。どうやらEvent.observeでのハンドラ登録がうまくいっていないようだ。

手持ちのSafari for WinはDebug メニューを有効にしてあるので、JavaScript Consoleを表示したところ、

TypeError: Value undefined (result of expression this.attachEvent) is not object.
http://s.asablo.jp/js/expandimage.js  Line: 49

というエラーがでていた。おまえが原因か!

この「expandimage.js」、アサブロの画像表示周りのスクリプトらしい(自分ではあまりアサブロに画像登録をしていない。外部に置いてそっち参照させるので)のだが、この49行目前後を見てみると、

48: HTMLElement.prototype.addEventListener = function(name,func,dir) {
49:  this.attachEvent(name, func);
50: }
 
ってなコードになってる。このHTMLElement.prototypeのメソッド定義は
if (! (document.body && document.body.insertAdjacentHTML) || asablo.noHTMLElement) {

という条件を満たした場合に実行される模様。んで、これらの条件を満たした場合は、
  • HTMLElementオブジェクトが存在しない場合は定義する
  • HTMLElement.prototype.insertAdjacentHTMLの定義
  • HTMLElement.prototype.addEventListenerの定義
を行うと。

...ちょっとひどくないか?

上記の処理の対象となるのは

  • document.body.insertAdjacentHTMLがないブラウザ(=Firefox、Safari)
  • asablo.noHTMLElementがtrueなブラウザ(=IE)
のどちらかの条件に当てはまる場合で、どちらにも当てはまらないのはOperaのみなんですけど。

幸いというか、IEでのinsertAdjacentHTMLの定義はDOMとは無関係なダミーのHTMLElementに対して行われるため問題が起きず、FirefoxでのaddEventListenerの定義は他のDOM要素に(なぜか)影響を与えていないのだが、SafariはがっつりとaddEventListenerの上書きの影響をこうむっちゃっている

こういう処理はもうちょっと丁寧に状況分けしないとまずいでしょ。

よく小知恵が回るよなぁ!2008年03月08日 04時30分56秒

ここ2日くらいで6つほど英語のトラバスパム(全部薬売り)がきたのだが、なんとこのうち5つがGoogle グループだった。

こういうディスカッションフォーラムをスパム元にするって発想はなかったなぁ。ほんとよく思いつくもんだ。

Delegateクラス2008年03月11日 03時00分03秒

zf入門も書かずにコネタクラスです。ごめんなさい。今回はZend_Logのサンプルもちょっとだけ載せてるので勘弁してください。Zend_Logは説明してないけど。

string | array が気持ち悪いので

PHPで動的な関数・メソッド呼び出しを行う場合、関数名を示す文字列を使うか、配列にオブジェクト(or クラス名)とメソッド名を格納して渡す。マニュアル中で「callback」という呼称で呼ばれている擬似的な型なやつだ。

これ、短いスコープで使う場合はまぁそれでもいいかと思えるんだけど、大きいスコープの変数に格納した場合、本当に適切かどうかの判断がすぐにつかなくて気持ち悪い。$callbackとかって変数にint放り込んでも許容されるので実行時にチェックするのって面倒くさいじゃん。is_callback()とかあるわけじゃなし。

だもんで、.NETのDelegateのような感じで、あらかじめラッピングしちゃえば、関数やメソッドが実在するかはともかくとして「呼び出し可能なオブジェクト」として保持しておけるだろうと思って、こんなクラスを作ってみた。まんま「Delegate」というクラス。使いどこ、少なそうなんですが

<?php
class Delegate {
  /**
   * @static
   *
   * 指定の引数がコールバック形式かを判別する
   *
   * @param mixed $callback コールバック形式かを判別する引数
   * @return bool $callbackがコールバック形式の場合はtrue、それ以外はfalse
   */
  public static function isCallback($callback) {
    // 文字列か配列のみ許容
    if( is_string($callback) || is_array( $callback ) ) {
      if( is_array($callback) ) {
        if(
          // 配列長が2ではないか
          count($callback) != 2 ||
          // 第一要素がオブジェクトまたは文字列ではないか
          ( ! is_object($callback[0]) && ! is_string($callback[0]) ) ||
          // 第二要素が文字列ではない場合は
          ! is_string($callback[1])
        ) {
          // コールバックではない
          return false;
        }
      }
      return true;
    } else {
      // 文字列・配列以外はコールバックではない
      return false;
    }
  }

  /**
   * @access protected
   *
   * コールバックオブジェクト
   *
   * @var string|array
   */
  protected $_callback;

  /**
   * Delegateの新しいインスタンスを初期化する
   *
   * @param mixed $obj コールバック関数名、クラス名またはオブジェクトインスタンス
   * @param  string|null $method $objがクラス名かオブジェクトインスタンスの場合はメソッド名
   */
  public function __construct($obj, $method = null) {
    $callback = ( is_string($obj) && empty($method) ) ?
      // $objが文字列で$methodがnullの場合は関数名指定
      $obj :
      // それ以外はメソッド指定
      array( $obj, "$method" );

    // コールバック形式かをチェック
    if( ! self::isCallback($callback) ) {
      throw new Exception( '引数がコールバック形式ではありません' );
    }

    $this->_callback = $callback;
  }

  /**
   * コールバック呼び出しを実行する
   *
   * @param [mixed parameter [, mixed ...]] 可変長パラメータ
   * @return mixed コールバックの実行結果
   */
  public function invoke() {
    return $this->invokeArray(func_get_args());
  }

  /**
   * パラメータを配列で指定してコールバック呼び出しを実行する
   *
   * @param array $params コールバック関数・メソッドに指定する引数の配列
   * @return mixed コールバックの実行結果
   */
  public function invokeArray(array $params) {
    return call_user_func_array($this->_callback, $params);
  }
}


ま、元の発想のまま「Delegate」なんて名前にしてるけど、別に「Callback」クラスとか「MethodInvoker」とかでもいいです。

ドキュメントコメントが適当なのは勘弁してください。ちゃんと調べよう、そのうち。

「Delegate」とはいいつつも

当然ながら、.NETのDelegateクラスとはちみっと違う。まぁ、あちらは明示的にDelegate/MulticastDelegateから派生型作れるわけではなく、構文からコンパイラが勝手に派生させるっていう特殊な型なんだけども。

コールバックの引数でバインドするわけではないので、invoke()するときの引数に関してなんの保証もないので、ランタイムエラーになりやすいかも。

使い方

コンストラクタの引数に、「callback」型を指定する。

例えば関数の場合ならその関数名の文字列。クラスメソッド(スタティックメソッド)だったら「array( 'ClassName', 'methodName' )」のように文字列を2つ、インスタンスメソッドなら「array( $obj, 'methodName' )」みたいな感じ。

んで、格納したコールバックを呼び出す場合はinvoke()メソッドを使う。引数は可変長で受け取るので、元の関数(またはメソッド)に与える引数と同じような引数を与えてやる。元の関数(メソッド)が値を返すならinvoke()の戻りで受け取れる。

サンプル

以下はZend_Logの各種ログ出力メソッドをDelegateに収めてループで呼び出しするサンプル。

<?php
require_once 'Delegate.php';
require_once 'Zend/Log.php';
require_once 'Zend/Log/Writer/Stream.php';

$log = new Zend_Log( new Zend_Log_Writer_Stream('php://output') );

$list = array(
  // Zend_Log::emerg()を実行するDelegate
  array( 'name' => 'EMERG', 'callback' => new Delegate( $log, 'emerg' ) ),

  // Zend_Log::err()を実行するDelegate
  array( 'name' => 'ERR',   'callback' => new Delegate( $log, 'err' ) ),

  // Zend_Log::warn()を実行するDelegate
  array( 'name' => 'WARN',  'callback' => new Delegate( $log, 'warn' ) ),

  // Zend_Log::debug()を実行するDelegate
  array( 'name' => 'DEGUB', 'callback' => new Delegate( $log, 'debug' ) ),

);
// フィルタ設定なしで実行 → すべてのログが出力される
foreach( $list as $config ) {
  $callback = $config['callback'];
  $callback->invoke( "name = {$config['name']}" );
}

// フィルタ条件を変えてもう一度ループ実行
// フィルタでZend_Log::ERR以下のプライオリティをブロックする
echo "Zend_Log::ERRでフィルタ指定\n";
$log->addFilter( new Zend_Log_Filter_Priority( Zend_Log::ERR ) );
foreach( $list as $config ) {
  $callback = $config['callback'];
  $callback->invoke( "name = {$config['name']}" );
}


このサンプルではDelegateのリストをベタでコーディングしちゃったけど、Zend_Logのプライオリティ名の配列を元にループ処理で組み立ててもよいかも。

しかし、微妙か。

こんな感じで使うんだけど、微妙。独自クラスにしちゃってるので当然array_filter()とかのコールバックにはそのまま使えないし($_callbackを返すgetterでも実装すればいいのだが)、コンストラクタで引数の型はチェックしているけど、別に関数・メソッドの実在をチェックしてるわけじゃないので、ランタイムでエラーになるかもしれないし。第一、ちょっとした用途なら素直にcall_user_func()とか使ったほうが手っ取り早いし

それでも個人的には上のサンプルみたいに同じ形のメソッドを呼び出すちょっとしたテストに使ったりとかでそれなりに便利には使ってるんだけどね。

多様的にメソッド呼び出し行うなら、インターフェイス(か抽象クラス)から派生・実装させるのが本筋なんだろうけど、これなら組み込み関数も自作クラスのインスタンスメソッドもシグニチャが同じものなら同じように代替させられるので、その点だけはメリットかな、と思うんだけど。でもやっぱりcall_user_func()するのがPHPの流儀かしら。

おクスリのトラバが鬱陶しいなぁ2008年03月14日 00時30分53秒

ここ数日英語のトラバがやけに増えてる。全部クスリ関係で大半がGoogle Groups。いいかげん飽きてきた。

アサブロの使い方に

「spam」を設定するとそのトラックバックは非公開になります。また、その情報はスパム対策の有効な情報としてASAHIネットで利用することがあります。
とあるので一応即削除せずにいったん「spam」指定して、半日くらい寝かせてから削除しているのだが、これってほんとに使われてるのかなぁ。

ずーっと「spam」にしとかないとスパム対策に使われないんじゃ、管理画面にフィルタ機能でもない限り邪魔なだけで意味ない気がするのだが。