付録 A. PHP 4 用の PHPUnit

PHP 4 用の PHPUnit には、TextUI テストランナーがありません。 PHP 4 用の PHPUnit でテストを実行する際の一般的な方法は、 テストスイートを書いた後で、それを 例 A.2 のように手動で実行することです。

高木さんの涙が出る程ありがたい翻訳マニュアル。

ところで、

「PHP4用のPHPUnitにはTextUIテストランナーがありません。」

この事実、以前辿り着くのが結構大変だった覚えがあります。ありそうなんでドキュメントやソースをかなり漁っちゃうんですよね。 車輪の再開発の嫌い過ぎも瞬発力が無くなっちゃいますね。

bookmark_rss.png

ブックマークのRSSでエントリのrdf:aboutやlink要素ってブックマーク先のURIになってるけどこれっていいのかな?(del.icio.usもはてブも) ブックマークサービス上の1ページ(こういうページ)は1個のエントリとはみなさないってこと? フィードをキャッシュしようと思った時、これがエントリのユニークなIDになるかと思ったんだけど頭にフィード自体のURI付けないと駄目か。うzzzzz

faviconのエントリはてブもらったので調べてみました。

WWW::Blog::Metadata::Icon – Extract icon (FOAF photo, favicon) from weblog – search.cpan.org

DESCRIPTION

WWW::Blog::Metadata::Icon is a plugin for WWW::Blog::Metadata that attempts to extract photos/icons for a weblog author. It looks in three places:

1. a FOAF file, from either an img or depiction element. 2. a shortcut icon in a tag in the document. 3. a HEAD check on $uri/favicon.ico.

Ben TrottのWWW::Blog::Metadata::Iconってmoduleで取れるそうです。

sub on_got_tag {
    my $class = shift;
    my($meta, $tag, $attr, $base_uri) = @_;
    if ($tag eq 'link' && $attr->{rel}) {
        my %rel = map { $_ => 1 } split /\s+/, lc $attr->{rel};
        if ($rel{icon}) {
            $meta->favicon_uri(URI->new_abs($attr->{href}, $base_uri))->as_string;
        }
    }
}

sub on_finished {
    my $class = shift;
    my($meta) = @_;
    $meta->icon_uri($meta->foaf_icon_uri || $meta->favicon_uri);
}

おお、すげー参考になる。foafのiconも見るとこがビリッとしてんなー。 これのPHP4版無いのかな。

個別の記事のページにずっとjsのエラーが出てたのを直しました。 コメント時に名前とかがCookieに保存されないのはこれかよ。 (Sleipnirだとjsエラー時に「デバッガを起動しますか?」みたいなポップアップが出るそうでかなりウザいそうです)

よく見たらMTが使うSite JavaScript(mt-site.js)が元々存在しなかった。 いつのバージョンからあったのかわからないが今さら気づいたので本家からテンプレ持ってきたら動きました。よかった。

3.2x Default Templates

Index templates

OKWebでの質問返ってこなかった…。

………よしッ!!!(リアクション間違い)

宣言通りしようがなくJavascriptのParser自分で書いた。

本当に泥臭いから覚悟しろ!

使い方:

<html>
<head>
<script src="scripts/ObjTree.js" type="text/javascript"></script>
<script src="scripts/prototype.js" type="text/javascript"></script>
<script src="scripts/feed.js" type="text/javascript"></script>
<script language="javascript">
var url = 'http://d.hatena.ne.jp/m-komagata/rss';        // RSS1.0
//var url = 'http://d.hatena.ne.jp/m-komagata/rss2';   // RSS2.0
//var url = 'http://p0t.jp/mt/atom.xml';                     // Atom0.3
//var url = 'http://komagata.blogspot.com/atom.xml';  // Atom0.3

url = 'proxy.php?uri=' + url;
new Ajax.Request(url, {
  onComplete : function(req) {
    var feed = new Feed(req.responseText);
    feed.parse();
    var list = '';
    feed.getItems().each(function(item) {
      list += '<h2>' + item.title + '</h2>';
      list += '<p>' + item.link + '</p>';
      list += '<p>' + item.description + '</p>';
      list += '<p>' + item.date + '</p>';
    });
    Element.update('list', list);
  }
});
</script>
</head>

<body>
<div id="list" ></div>
</body>
</html>

パーサー:

var Feed = Class.create();
Feed.prototype = {
  initialize : function(data) {
    this.data = data
  },
  parse : function() {
    var xotree = new XML.ObjTree();
    var tree = xotree.parseXML(this.data);
    this.data = tree;

    var kind = this.getKind();
    eval('this.parser = new ' + kind);
    this.parser.data = this.data;
    this.parser.kind = kind;
  },
  getKind : function() {
    var kind;
    if (this.data['rdf:RDF'] &&
    this.data['rdf:RDF']['-xmlns'] == 'http://purl.org/rss/1.0/') {
      kind = 'RSS10';
    } else if (this.data['rss'] && this.data['rss']['-version'] == '2.0') {
      kind = 'RSS20';
    } else if (this.data['feed'] && this.data['feed']['-version'] == '0.3') {
      kind = 'Atom03';
    } else {
      kind = 'UnknownFeed';
    }
    return kind;
  },
  getTitle : function() {
    return this.parser.getTitle();
  },
  getLink : function() {
    return this.parser.getLink();
  },
  getItems : function() {
    return this.parser.getItems();
  },
  getJSON : function() {
    return {
      title : this.getTitle(),
      link : this.getLink(),
      items : this.getItems()
    };
  }
};

var RSS10 = Class.create();
RSS10.prototype = {
  initialize : function() {},
  getTitle : function() {
    return this.data['rdf:RDF'].channel.title;
  },
  getLink : function() {
    return this.data['rdf:RDF'].channel.link;
  },
  getItems : function() {
    var items = this.data['rdf:RDF'].item;
    var res = [];
    items.each(function(item) {
      item['date'] = item['date'] || item['dc:date'];
      res.push(item);
    });
    return res;
  }
};

var RSS20 = Class.create();
RSS20.prototype = {
  initialize : function() {},
  getTitle : function() {
    return this.data['rss'].channel.title;
  },
  getLink : function() {
    return this.data['rss'].channel.link;
  },
  getItems : function() {
    var items = this.data['rss'].channel.item;
    var res = [];
    items.each(function(item) {
      item['date'] = item['date'] || item['dc:date'] || item['pubDate'];
      res.push(item);
    });
    return res;
  }
};

var Atom03 = Class.create();
Atom03.prototype = {
  initialize : function() {},
  getTitle : function() {
    var title =  this.data['feed'].title;
    if (typeof(title) == 'string') {
      return title;
    } else {
      return title['#text'];
    }
  },
  getLink : function() {
    var link = this.data['feed'].link;
    var res = '';
    if (!link['-href']) {
      link.each(function(ln) {
        if (ln['-rel'] == 'alternate') {
          res = ln['-href'];
        }
      });
    } else {
      res = this.data['feed'].link['-href'];
    }
      return res;
  },
  getItems : function() {
    var items = this.data['feed'].entry;
    var res = [];
    items.each(function(item) {
      var title = item['title'] = item.title;
      if (typeof(title) == 'string') {
        item['title'] = title;
      } else {
        item['title'] = title['#text'];
      }
      item['date'] = item['date'] || item['dc:date'] || item['created'];
      if (typeof(item.link['-href']) != 'string') {
        item.link.each(function(ln) {
          if (ln['-rel'] == 'alternate') {
            item['link'] = ln['-href'];
          }
        });
      } else {
        item['link'] = item.link['-href'] || '';
      }

      if (typeof(item.content['#cdata-section']) == 'string') {
        item['description'] = item.content['#cdata-section'];
      } else {
        item['description'] = item.content.div['#text'];
      }
      res.push(item);
    });
    return res;
  }
};

proxy.phpのソース:

<?php
require_once 'PHP/Compat/Function/file_get_contents.php';
echo @file_get_contents($_GET['uri']);
?>

結果:

feed-result.png

相当泥臭い。大体のフィード読めてるけどいくつか取れないのも。あとでちゃんとRSS/Atomの仕様読んでちゃんとした方法で対応したい。本題のfavicon取る機能はまだ…。

XMLをJSONに変換してくれるXML.ObjTreeが必須。

最近、家の回りに質の悪い連中をちょくちょく見かける。

冬にはまったく見なかったのに、最近は頻繁に目に付いて嫌でも奴らの勢力の強さを感じます。

家の前で見掛けた時、おれがカッと睨み付けてやっても連中は、「ん?なにか?」ってなもんです。

g.jpg

昨日も玄関前でBabyG(*)を見た。このままでは室内に出現するのも時間の問題…。

(*子供のG)

寝苦しい夜が続いていますがいかがお過ごしでしょうか。世間から大体1年ぐらい遅れてJavascriptを書きはじめたkomagataです。

前置きはさておき、ぶっちゃけたこと聞いていいでしょうか?

「varって何?」

「付けたときと付けないときどう違うの?」

それでは宜しくお願い申し上げます。あ、わかった、myみたいなもんか。

AwesomeFrameworkってどうなってるの?」って言われたんで最新版(0.9.0)をアップしました。

ちょこちょこいじってはいるんですが、相変わらずぱっと見でわかるぐらいのソースです。

追加機能は以下。

  • defineいじればDocumentRootとかを好きな場所に設定できるようになった。
    (いままでは男らしいフラット管理のみだった。でもまだデフォルトはフラット)
  • 設定ファイルを置けば読んでくれるようになった。
    (別に置かなくてもいい)
  • $R($_REQUEST), $S($_SESSION), $C(設定)とか短くアクセスできるようになった。
  • get関数が追加。
    (テンプレ中で<? if (isset($foo)): ?><?=$foo?><? endif; ?>と書きたくないためだけに生まれた。NOTICE切ればいいだけだが…。)

以下サンプル。

アクション(default.php):

&lt;?php
assign('hello', 'Hello World!!!');
forward("view");
?&gt;

テンプレ(view.php):

&lt;html&gt;
&lt;head&gt;&lt;title&gt;&lt;?=fetch('title')?&gt;&lt;/title&gt;&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;&lt;?=fetch('title')?&gt;&lt;/h1&gt;
&lt;p&gt;&lt;?=$hello?&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;

コンポーネント(title.php):

Hello World

(コンポーネントはアクションでもなんでもいい)

結果:

&lt;html&gt;
&lt;head&gt;&lt;title&gt;Hello World&lt;/title&gt;&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Hello World&lt;/h1&gt;
&lt;p&gt;Hello World!!!&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;

アプリ作るとき(1人か2人を想定)あと面倒なのってDBとValidation。 DBはこの間のヤツがちゃんとできれば簡単になりそう。Validationは…なんも思いつかない。

以下、全ソース。

&lt;?php
//
// AwesomeFramework - extremely simple framework.
//
// Copyright (C) 2005 Masaki Komagata &lt;komagata@p0t.jp&gt;
//     All rights reserved.
//     This is free software with ABSOLUTELY NO WARRANTY.
//
// You can redistribute it and/or modify it under the terms of 
// the PHP License, version 3.0.
//
define('DEFAULT_PAGE', 'default');
define('VERSION', '0.9.0');
define('EXT', '.php');
define('PARAM', 'p');
define('DOCROOT_DIR', dirname(__FILE__) . '/');
define('ROOT_DIR', DOCROOT_DIR);
define('APP_DIR', ROOT_DIR);
define('LIB_DIR', ROOT_DIR);
define('CONF_FILE', ROOT_DIR . 'config.php');
define('INC_FILE', APP_DIR . 'include.php');
define('DOCROOT_DIR_URI', dirname($_SERVER['SCRIPT_NAME']) . '/');
define('ROOT_DIR_URI', DOCROOT_DIR_URI);

$vars = array();

if (file_exists(CONF_FILE)) {
    include_once CONF_FILE;
}

if (file_exists(INC_FILE)) {
    include_once INC_FILE;
}

session_start();
forward(isset($_REQUEST[PARAM]) ? $_REQUEST[PARAM] : DEFAULT_PAGE);

function forward($page)
{
    $C = &$GLOBALS['C'];
    $R = &$_REQUEST;
    $S = &$_SESSION;
    extract($GLOBALS['vars']);
    include APP_DIR . '/' . $page . EXT;
}

function fetch($page)
{
    ob_start();
    forward($page);
    $buffer = ob_get_contents();
    ob_end_clean();
    return $buffer;
}

function redirect($page, $params = array())
{
    header('Location: ' . url($page, $params));
    exit();
}

function url($page, $params = array())
{
    $param = '';
    foreach ($params as $name =&gt; $value) {
        $param .= '&' . $name . '=' . $value;
    }
    return 'index.php?' . PARAM . '=' . $page . $param;
}

function assign($name, $value)
{
    $GLOBALS['vars'][$name] = $value;
}

function assign_by_ref($name, &$value)
{
    $GLOBALS['vars'][$name] =& $value;
}

function get($name)
{
    if (isset($$name)) {
        return $$name;
    }
}
?&gt;

今日、とても泥臭いソースを書きましたのでご報告申し上げます。

urlからfaviconのurlを取る。

% cat favicon.php
&lt;?php
require_once 'PHP/Compat/Function/file_get_contents.php';
require_once 'XML/XML_HTMLSax.php';

function getFavicon($url)
{
    class FaviconHandler
    {
        var $favicon;
        function openHandler(&$parser, $name, $attrs)
        {
            if ($name  'link' and $attrs['rel']  ‘shortcut icon’) {
                $this->favicon = $attrs[‘href’];
            }
        }
        function closeHandler(&$parser, $data) {}
    }

    $handler = new FaviconHandler();
    $parser =& new XML_HTMLSax();
    $parser->set_object($handler);
    $parser->set_element_handler(‘openHandler’, ‘closeHandler’);

    $doc = file_get_contents($url);
    $parser->parse($doc);

    $favicon = $handler->favicon;

    if (!$favicon) {
        if ($url{strlen($url)-1} != ’/’) {
            $url .= ’/’;
        }
        $favicon =  $url . ‘favicon.ico’;
    }
    return $favicon;
}

echo getFavicon(‘http://del.icio.us/komagata’) ?> % php favicon.php http://del.icio.us/favicon.ico

ヘッダに書いてない場合は何故か無理矢理URLにfavicon.icoをくっ付けて返す。 誰かJavaScript版ください。さもないとまたおれが泥臭いの書くぞ!faviconの決めつけは良くないということで一応、あるかどうか探すように変更した。

$favicon = $link . 'favicon.ico';
if (!@file_get_contents($favicon)) {
    $favicon = false;
}

file_get_contents()はやっぱりイイ!ネット上のファイルの存在確認もわかる!(404だとfalse返ってくるみたい)

はじめてのBillGレビューのこと – The Joel on Software Translation Project

Excelチームは、我々に必要なのはExcelのためのVisual Basicなんだと言ってBasicチームを説得した。私は自分のお気に入りの機能を4つBasicにどうにか付け加えた。ひとつはVariant型だ。これはどんな型でも保持できる共用データ型で、これがなければswitch文を使わずにスプレッドシートのセルを変数に格納できなかったからだ。

Variant型はExcelチームの提案だったのカ!