fuelphp-dynamoutilを公開した

さて、FuelPHP 勉強会 東京 vol.3 : ATND で発表したスライドの中で、FuelPHPAmazon DynamoDB を使いやすくする SDK のラッパーを OSS で公開したいと予告していたのですが、先ほど github で公開しました。

ドキュメントはまだないです。クラス 4 つしかない小さなものなので、ソース読んだだけでも使い方は分かるかもしれません。

FuelPHP勉強会東京 vol.3に参加してきた

ここ数ヶ月間は会社のプロジェクトでは FuelPHP を使っています。丁度よいタイミングで勉強会が開催されたので参加しました。

申し込んだ時点では参加者全員 LT という約束だったので、ここ数ヶ月 FuelPHP を使った実感を簡単にまとめてスライドにしました。


昼の勉強会も面白い発表をたくさん聞けて、夜の懇親会もさらに楽しくて、とても良い時間を過ごさせて頂きました。

知ってる人が一人もいない勉強会も新鮮でいいなぁ〜。もっと色んなところに出て行かないといけないなと実感しました。

月の最後の日をPHPで簡単に知る方法

集計系の処理のプログラムを開発していると、その月の最後の日を求める必要がちょくちょくあったりしませんか? 私はあります。さてそういう時にどうやって求めるでしょう。答えは一発↓

<?php
$d = date('Y-m-t');

これだけですw

書式「t」って何ですか?

恥ずかしながら date() 関数の書式に使える「t」の存在を今日まで知りませんでした。意味としては以下になっています。

t : 指定した月の日数。

t は DateTime クラスにも使えます。
例えば今年の各月に対して最終日を求めるのには、以下のようにします。

<?php
$d = new DateTime();
$year = 2012;
foreach (range(1, 12) as $month) {
    $d->setDate($year, $month, 1);
    echo $d->format('Y-m-t') . "\n";
}

従来はどうやっていた?

t フォーマットを知る前の従来のセオリーはこうでした。

  1. その月の最初の日(1日)を求める
  2. それに "+1 month" する
  3. それにさらに "-1 day" する

コードで書くとこんな感じ。

<?php
$d = new DateTime();
$d->setDate($d->format('Y'), $d->format('m'), 1);
$d->modify('+1 month -1 day');
echo $d->format('Y-m-d');

おまけ

PHPマニュアル「相対的な書式」をつらつら見てたら

<?php
$d = new DateTime('last day of 2012-06');
echo $d->format('Y-m-d');

とかでもいいんじゃね? とか思った。ダメじゃんオレw

PEAR::Mail_mimeで日本語ファイル名の添付ファイルを送る方法

世の中もう大体 UTF-8 で OK かなと思ったら、メールの添付ファイル名はそうでもないっぽいです。UTF-8 + URLエンコードで添付した日本語ファイル名が正しく読めないメール環境があったので、ISO-2022-JP + base64 にしたら大丈夫でした。


これをするのにPEAR::Mail_mimeだと、ややこしい引数を渡さなければいけないのでそれのメモ。以下のソースは UTF-8 で書かれていることを前提にしています。

<?php
require_once 'Mail.php'; // PEAR::Mail
require_once 'Mail/mime.php'; // PEAR::Mail_mime

$textbody = '日本語の本文。';
$filename = '日本語添付ファイル名.txt';
$filedata = '日本語添付ファイルの中身';

// 文字コードを ISO-2022-JP に変える
mb_convert_variables('ISO-2022-JP', 'UTF-8', $textbody, $filename);

// 各 mime パートの構築
$mime = new Mail_mime;
$mime->setParam('text_charset', 'ISO-2022-JP');
$mime->setParam('text_encoding', '8bit');
$mime->setTxtBody($textbody);
$mime->addAttachment($filedata // data
                     ,'application/octet-stream'  // content-type
                     ,$filename // attached file name
                     ,false // isfile
                     ,'base64' // encoding
                     ,'attachment' // disposition
                     ,'' // charset
                     ,'' // language
                     ,'' // location
                     ,'base64' // n_encoding
                     ,'base64' // f_encoding
                     ,'' // description
                     ,'ISO-2022-JP' // h_charset
    );

$subject = '日本語表題';
$jis = mb_convert_encoding($subject, 'ISO-2022-JP', 'UTF-8');
$encsubj = mb_encode_mimeheader($jis, 'ISO-2022-JP', 'B');

$recpts = 'example@example.jp';

$headers = array(
    'From' => 'webapp@example.jp';
    'To' => $recpts,
    'Subject' => $encsubj,
    );
$headers = $mime->headers($headers);
$body = $mime->get();

// メール送信には SMTP を直接使うのが好きw
$mail = Mail::factory('smtp', array('host' => 'localhost'));
$mail->send($recpts, $headers, $body);

子プロセス制御ふたたび : PHP Advent Calendar jp 2011 Day 8

はい、7日目の @ さんのエントリ「DateTimeクラスの落とし穴と対策 : PHP Advent Calendar jp 2011 Day 7」から引き続いて、PHP Advent Calendar jp 2011の8日目なわけです。


今回は何を書こうかずいぶん悩んで、ちょうど昨晩開催されたPHP忘年会2011@関東でネタ募集したところ、@さんが口走ったphpQueryネタをパクるという案もあったのですが、やはり正攻法で手持ちのネタでいくことにしました。

子プロセスfork

このはてなダイアリーでの数少ないPHPヒットネタとして「pcntl extensionを使って一定個数の子プロセスに作業させる方法 - Blog::koyhoge」という記事がありまして、公開した2007年以来ぼちぼちとずっとアクセスを稼いでくれております。


ただこの時に書いたサンプルコードも、話を単純にするためにグローバル空間にベタ書きですし、今となってはあまり良いサンプルとは呼べません。実は今年になって、とある目的できちんとクラス化したものが作ったのでそれを紹介しようと思います。

きっかけは Amazon SQS+SDB のワーカー

その目的というのは、AMN の広告配信ログサーバを改善する際の途中成果として作成した、キューからデータを取ってきて DB に入れるワーカー処理です。11月15日に開催された「第7回 MongoDB 勉強会 in Tokyo」で発表した以下のスライドで、そこに至る背景と経緯を説明しています。

ProcessForker クラス

では早速コードを見ていきましょう。

<?php
class ProcessForker {
    // defaults
    protected $_options = array(
        'max_children' => 10,  // max number of child processes
        'process_limit' => 0,  // return when this count tasks are finished
        'loop_task' => false,  // reuse tasks
        'sleep' => 0, // microseconds
        'single_execution' => null,
        );

    protected $_idx_task = 0;

    public function __construct($options = null) {
        if (!empty($options)) {
            $this->_options = array_merge($this->_options, $options);
        }
    }

    public function fetchTask(&$tasks) {
        if ($this->_options['loop_task']) {
            $idx = $this->_idx_task;
            $task = $tasks[$idx];

            // circulation in tasks
            ++$idx;
            if ($idx >= count($tasks)) {
                $idx = 0;
            }
            $this->_idx_task = $idx;
        } else {
            $task = array_shift($tasks);
        }
        return $task;
    }

    public function run($tasks) {
        // number of current running child processes
        $nchildren = 0;
        // number of finished task
        $nfinished = 0;
        for (;;) {
            if (empty($tasks)) {
                break;
            }
            $maxproc = $this->_options['process_limit'];
            if (($maxproc > 0) && ($maxproc <= $nfinished)) {
                break;
            }
            if ($nchildren <= $this->_options['max_children']) {
                $task = $this->fetchTask($tasks);

                $pid = pcntl_fork();
                if ($pid === -1) {
                    throw new Exception('pcntl_fork faild');
                } else if ($pid) {
                    // parent process
                    ++$nchildren;
                } else {
                    $exit_code = 0;
                    // child process
                    $func = $task[0];
                    $arg = $task[1];
                    if (!is_array($arg)) {
                        $arg = array($arg);
                    }

                    try {
                        call_user_func_array($func, $arg);
                    } catch (Exception $e) {
                        $exit_code = -1;
                    }
                    // care singleExecution:
                    // child process must not unlock
                    $se = $this->_options['single_execution'];
                    if ($se !== null) {
                        $se->setDoUnlock(false);
                    }
                    exit($exit_code);
                }

                $sleep = $this->_options['sleep'];
                if ($sleep > 0) {
                    usleep($sleep);
                }
            } else {
                $pid = pcntl_waitpid(0, $status, 0);
                --$nchildren;
                ++$nfinished;
            }
        }
    }
}

前回は決め打ちだった各種パラメータや実際に実行する処理を、すべて外部から渡せるようにしてあります。

使用例

この ProcessForker クラスは以下のように使用します。

<?php
require_once 'ProcessForker.php';

function hoge($str) {
    srand();
    $rand = rand(1, 5);
    echo "$rand: $str\n";
    sleep($rand);
}
$opts = array(
    'max_children' => 4,
    'process_limit' => 50,
    'loop_task' => true,
    );
$pf = new ProcessForker($opts);

$tasks = array(
    array('hoge', 'a'),
    array('hoge', 'b'),
    );
$pf->run($tasks);

まずはオプションを指定してProcessForkerオブジェクトを作ります。それぞのれオプションの意味は以下になります。

max_children 子プロセスの最大同時実行数
process_limit いくつのタスクを処理したら終了するか
loop_task 渡されたタスクを繰り返すかどうか
sleep 親プロセスが処理を行うたびにスリープする時間(us)
single_execution singleExecutionオブジェクト(後述)

この例では、

  • 最大4つの子プロセスを起動し (max_children => 4)
  • 全部で50回の処理を行い (process_limit => 50)
  • タスクを繰り返しながら (loop_task => true)

実行するという意味になります。

タスク指定

子プロセスとして処理するタスクは、配列の形で渡します。

array(<callable>, <引数>)

callablePHPis_callable() でtrueとなるもので、以下のどれかになります。

タスクへの引数は、ひとつの場合はそのままで構いませんが、複数の場合はそれも配列にして渡します。

繰り返し処理

loop_task オプションが true になっていると、渡されたタスクをすべて処理し終えてても終了せずに、またタスクリストの先頭から処理を行います。同じ処理を何度も繰り返したいときは、タスクを1つだけ用意して loop_task を true にすれば良いです。

single_execution 対応

常にプロセスが動いていて欲しい場合は cron で毎分キックするということをよくやりますが、しかし大元の親プロセスが複数立ち上がってしまうのは好ましくありません。そこで @ さんが作られた single_execution を利用して、同時起動チェックを行いました。しかし単に ProcessForker と single_execution を組み合わせるだけではどうも上手く動いてくれません。


調べたところ ProcessForker から起動された子プロセスが、プロセス終了時に singleExecution の管理しているロックファイルを削除してしまっていることが解りました。single_execution はそこから更に子プロセスが fork されることなど想定していないので当然ですね。

ということで、single_execution.php にパッチを当ててプロセス終了時のロック解除を選択できるように変更しました。ProcessForker 作成時の single_execution オプションに singleExecution オブジェクトを渡せば、子プロセスの場合はロックを解除せずに終了するようになっています。改変した single_execution.php は以下に置いてあります。

https://gist.github.com/1446922

これですでにプロセスが動作しているかどうか気にせずに、cronからスクリプトを定期的に起動するだけで、複数プロセスで動的に処理を行なってくれるようになりました。


明日の Advent Calendar は @takuya_1st さんです。

TwigからEntity::findするフィルターを書いてみた (Symfony Advent Calendar JP 2011 2日目)

初日の @brtriver さんに引き続き Symfony Advent Calendar JP 2011 の2日目です。


ここ数ヶ月 Symfony2 を触っています。実は Symfony 1.X の頃は興味ありながらもほとんど触っていなくて、2になってから触り始めたビギナー&ニューカマーなんです。諸先輩方よろしくお願いします。

Symfony Advent Calendar JP 2011ができるまで

昨年はSymfony Advent 2010をやっているのは知ってたので、今年はどうするのかなーやらないのかなー? と思って

とつぶやいたら、さっそく@ さんからリプライがあって、という流れで、何故か新人なのに取りまとめ役をやることになりました。不用意につぶやくと話が転がって面白い事になるという良い例です(笑)

Twig フィルターを書いてみた

プロパティの動的参照

さて本題です。Symfony2 の標準テンプレートエンジンの位置にある Twig ですが、開発しているのが Symfony の作者である Fabien さんだけあって、View としての役割がきっちり守られていて、余計なことはできないようになっています。


例えばオブジェクトのプロパティを動的に取得するようなことも、現在 Symfony2 にバンドルされている Twig 1.1.2 ではできません。Github Issues で話題になった時も Fabien は

This feature won't be included in Twig. Twig is a template system, not a fully-features language. Dynamic properties is clearly out of Twig scope.
訳: それは Twig の機能として含まれるべきではないよ。Twig はただのテンプレートシステムでフル機能の言語じゃないんだ。動的なプロパティ参照は Twig のスコープ外だよ。

https://github.com/fabpot/Twig/issues/41

と答えています。(その後考え直したのか Twig 1.2 から attribute 関数が導入されて、オブジェクトのプロパティを動的に取得できるようになりました。)

Entityから動的に取得する Twig フィルター

さて今回作成した Twig Filter は、Entity に対して検索をして値を取得するという、上記をさらに一歩踏み越えたある種のキワモノです。ロジックは Model や Controller でという原則からは外れるので安易に使うべきではありません。とは言えそういうキワモノを作るにはそれなりの理由もあるのです。

たとえばユーザの属性情報を考えてみましょう。ユーザはたくさんの属性情報を持っている場合があります。そのたくさんの属性のうちどれをどうやって表示するかを決めるのは View ですが、それを実際に表示するには Model や Controller 側でお膳立てをしておく必要があります。あるアイテムの所有者としてユーザ情報を表示するときに、そのユーザ情報の種類がすごく多かったり、直接 JOIN できなかったりする場合、M/C 側で全てを準備することは逆に現実的ではないと思うのです。

Symfony2 で Twig Extension

ということで、こんな Twig フィルターを作りました。

<?php
namespace Koyhoge\HogeBundle\Twig;
class HogeExtension extends \Twig_Extension {
    public function getFilters() {
        return array(
            'entity' => new \Twig_Filter_Method($this, 'entityFilter'),
            );
    }
    public function entityFilter($entity, $id, $prop, $by = null) {
        if (empty($by)) {
            $obj = $entity->find($id);
        } else {
            $objs = $entity->findBy(array($by => $id));
            $obj = $objs[0];
        }
        if ($obj == null) {
            return '';
        }
        $method = 'get' . ucfirst($prop);
        $call = array($obj, $method);
        if (!is_callable($call)) {
            throw new \Exception('Not callable:' . $method);
        }
        return call_user_func($call);
    }
    public function getName() {
        return 'hoge';
    }
}
使い方:Controller 側

Controller 側で以下のように Entity を直接 Twig に渡します。

<?php
namespace Koyhoge\HogeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class DefaultController extends Controller {
    public function indexAction() {
        $users = $this->getDoctrine()->getRepository('KoyhogeHogeBundle:User');
        $items = array(
            array('user_id' => 1),
            array('user_id' => 11),
        );
        return $this->render('KoyhogeHogeBundle:Default:index.html.twig',
                             array('users' => $users', 'items' => $items));
    }
}
使い方:Twig テンプレート側

テンプレート側では以下のように呼び出せます。

{% for item in items %}
  {{ users|entity(item.user_id, 'name') }}
{% endfor %}

{{ users|entity(item.user_id, 'name') }}

の部分で、User Entityから item.user_id で find() を行い、その結果のオブジェクトから name プロパティを取得する、という処理を行います。

他のキーで findBy()

別のユニークキーで findBy() したい場合は

{{ users|entity(item.other_id, 'name', 'other_key') }}

などのように3番目の引数に渡します。

まとめ

正直あまり実用的ではない tips ですが、View に必要なデータを Controller 側ですべてお膳立てするのはなんか違うなーと感じていたので、試しに実装して見ました。何にでも多用することは危険だと思われますが、デザインの都合によっていつ表示するか分からない基本的な情報をこのやり方で処理するのは、有効なのではないかと思っています。


明日は、今回の Advent Calendar 企画を私に振った張本人、日本の Symfony 界の重鎮 @hidenorigoto さんです。よろしくお願いします!

闇PHPMatsuriで発表した

最初はネタ無いしなーと思ってたけど、Net_IPv6 に送ったパッチの話は Twitter にしか書いてなかったので、さくっとスライド作って発表した。

Slideshare は .key を直接アップロードできるようになったのは良いけど、font matrix がずれるみたいすね。