移転しました。

ガチャみたいに設定した確率で抽選を行うPHPライブラリを作った

くじ引きやガチャガチャのように、あるものはよく出る、あるものはあまり出ない、というように確率に差がある上で一つ抽出する、というプログラムをたまに書くことがあるので、その部分だけを行うPHPライブラリを作った。

インストール

Composerを使う場合は、composer.jsonのrequireに

"cloned/luckybox": "$VERSION"

を追加。$VERSIONに入れられるバージョンはPackagistを参照。今のところは 0.9.* としておくと良いでしょう。

Composer推奨だけど、Composerを使わない場合はGitHubからソースコードをダウンロードしてrequire_onceしてもOK。

使い方

コインは60%、キノコは35%、スターは5%、という設定で1回くじを引いてみる例はこんな感じ。

<?php
use LuckyBox\LuckyBox;
use LuckyBox\Card\IdCard;

// Items
$items = array(
    1 => array('name' => 'Coin',     'rate' => 60), // 60%
    2 => array('name' => 'Mushroom', 'rate' => 35), // 35%
    3 => array('name' => 'Star',     'rate' => 5),  //  5%
);

// Setup
$luckyBox = new LuckyBox();

foreach ($items as $id => $item) {
    $card = new IdCard();
    $card->setId($id)
         ->setRate($item['rate']);

    $luckyBox->add($card);
}

// Draw
$card = $luckyBox->draw();
$item = $items[$card->getId()];

echo "You got {$item['name']}" . PHP_EOL;

LuckyBoxに確率(rate)を設定したCardを詰めて、LuckyBox#draw()するとその確率に応じた割合で一つ取得できる。
バンドルされているIdCardクラスは、rate以外にidのみ保持できるクラスで、より複雑なCardを作りたい場合は、Cardインタフェースを実装したものであれば自作しても利用可能。

デフォルトでは無限に LuckyBox#draw() できるけれど、LuckyBox#setConsumable(true) するとadd()したCardの分しか引けなくなる。こんな感じで利用できる。

<?php
$luckyBox = new LuckyBox();

// Add some cards.

$luckyBox->setConsumable(true);

while (!$luckyBox->isEmpty()) {
    $card = $luckyBox->draw();

    // Do something.
}

0.5% みたいなより精度の高い確率を設定したい場合は、より大きな数をrateに設定すると可能。

<?php
$card1 = new IdCard();
$card2 = new IdCard();
$card1->setRate(1023); // 10.23%
$card2->setRate(8977); // 89.77%

ここでは説明しやすいように合計を100とか10000にしているけれど、実際には全てのrateの合計に対する比率になっているので、「1:1:3」みたいな指定も可能。「1:1:3」とすると「20%:20%:60%」という確率になる。

同じような要件があって、まだコーディングしていなければ、是非使ってみてください。

ウェブサイトで英数字のIDを表示するときに最適なフォント

注文IDや招待IDなど英数字の羅列を表示することがあるとして、ユーザーがその文字列をコピペできない(もしくはコピペが面倒、難しい)場合、各文字を間違わずに識別できる必要がある。具体的にはゼロとオーなどの似ている文字を勘違いせずに済むようにしたい。

一般的にウェブサイトで良く利用されるフォントであるArial、Verdanaでゼロとオーを表示すると下記のようになる。次に勘違いしやすそうなイチとエルも書いてある。
左から、ゼロ オー イチ エル。

こうして見ると細長い方がゼロだなと区別は付く。とはいえ、ゼロにスラッシュがあればより勘違いする可能性が低いだろうから、ゼロにスラッシュのあるフォントを探してみた。

OS X Mountain Lion:フォントリストより、Macにインストールされているフォント(ただし英語フォントのみ)でゼロにスラッシュのあるものを探してみた。こんなにたくさんのフォントがインストールされているのに、ゼロにスラッシュが入っているフォントは下記のみだった。

今度はList of Microsoft Windows fontsからWindowsにあるフォントでゼロにスラッシュのあるものを探してみた。

え、Consolasだけ?あんなにもフォントがあるのに。

結論

font-family: "Monaco", "Consolas";

MacをMonacoにしたのはイチとエルの区別も分りやすいというのがありますが単に好みです。Consolasはイチとエルが似ているのが微妙ですね。Linuxは手元にGUIが動く環境を持っていないので検証していません。また、Webフォントはブラウザが対応していない可能性があるので検証していません。

PHPカンファレンス関西2013のLTでPHPUnitに関する発表をしてきました

PHPカンファレンス関西2013のLT(ライトニングトーク)セッションでPHPUnitを使ってCoverage 100%を目指すためのちょっとしたTipsについて発表してきました。

はてダにSlideShareをうまく貼付ける方法が判らないので、発表資料は以下のリンクからどうぞ。

一人でゲームをリリースするための自動化 PHPUnit編 ~目指せCoverage 100%~

「ヒッグス粒子の謎」を読んだ

ヒッグス粒子の謎(祥伝社新書290)

ヒッグス粒子の謎(祥伝社新書290)

難しい。専門的な用語を使わずに、使う場合も説明を添えて使っているので、素粒子量子力学にバックグラウンドがある人が読んだら凄く平易な本なのかもしれないけれど、今の僕にはどうも釈然としないまま本が終わってしまった。
もしかしたら平易な説明にするために平易な表現になりすぎているため、本来理解に必要な難しい理屈が記載されていないのが原因かもしれない。
とはいえ、「真空」「反粒子」「場」といったテーマについて今までよりも知ることができたし、何よりヒッグス粒子が質量の起源として考えられているという基本的なことも本書で初めて知ることできたのは良かった。

Symfony2+Doctrine2.3でSharding(水平分割)を実現する

Symfony2 + Doctrine2.3の環境でデータベースのSharding(水平分割)を行う際の実装方法など。ここで言うShardingは、例えば10台データベースを利用するとしてユーザーIDなどを基準に利用するデータベースを各10台のどれかに振り分けるような場合(参考: 分割 (データベース) - Wikipedia

確認環境

下記手順でSymfonyを展開。DocumentRootがSymfony/web。

% wget "http://symfony.com/download?v=Symfony_Standard_Vendors_2.1.1.tgz" .
% tar zxvf Symfony_Standard_Vendors_2.1.1.tgz
% chmod 777 Symfony/app/cache Symfony/app/logs

データベースはMySQL 5.5を利用。mysqld_multiを使ってlocalhostに3インスタンス起動(ポートは3306、3307、3308)。
3つのインスタンス全てに下記SQLを実行しておく。今回はuserテーブルをそれぞれのデータベースで振り分けるという例。

CREATE DATABASE `sharding` DEFAULT CHARSET=utf8;
GRANT ALL PRIVILEGES ON `sharding`.* TO 'sharding'@'127.0.0.1' IDENTIFIED BY 'sharding'

USE sharding;
CREATE TABLE `user` (
  `id` int(11) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Bundle作成手順を省略するために確認はAcmeDemoBundleを変更して行う。下記URLにアクセスしているイメージ。

http://localhost/app_dev.php/demo/

自前でConnectionを切り替える

app/config/config.ymlに下記のようなDoctrineの設定を行う(内容をわかりやすくするためにparameters.ymlの値を使ってません)

doctrine:
    dbal:
        connections:
            db1:
                driver:   pdo_mysql
                host:     127.0.0.1
                port:     3306
                dbname:   sharding
                user:     sharding
                password: sharding
                charset:  UTF8
            db2:
                driver:   pdo_mysql
                host:     127.0.0.1
                port:     3307
                dbname:   sharding
                user:     sharding
                password: sharding
                charset:  UTF8
            db3:
                driver:   pdo_mysql
                host:     127.0.0.1
                port:     3308
                dbname:   sharding
                user:     sharding
                password: sharding
                charset:  UTF8

同じホストだけどそれぞれポートが3306、3307、3308と分かれて設定されている。この状態でコントローラに下記のようなコードを書くと各MySQLにそれぞれデータが挿入される。

<?php
class DemoController extends Controller
{
    public function indexAction()
    {
        $conn = $this->get('doctrine')->getConnection('db1');
        $conn->insert('user', array('id' => 1));

        $conn = $this->get('doctrine')->getConnection('db2');
        $conn->insert('user', array('id' => 2));

        $conn = $this->get('doctrine')->getConnection('db3');
        $conn->insert('user', array('id' => 3));

3306ポートにはuserテーブルにid:1のデータ。3307にはid:2、3308にはid:3。これで一番基本的なコネクションの切り替えは出来るのだけど、当然のことながらDoctrineを使っているのにこのようなコードは書きたくない。

EntityManagerで切り替える

Symfonyのドキュメントにあるようなデータの取得方法をしようとする場合は素のConnectionを扱わずにEntityManagerを使いたい。
下記コマンドでEntityクラスを作成する(src/Acme/DemoBundle/Entity/User.phpが作られる)。

% php app/console doctrine:mapping:import AcmeDemoBundle annotation
% php app/console doctrine:generate:entities AcmeDemoBundle

※今回はプライマリキーに値を自分で設定するので、上記コマンドでUser.phpを生成したあとにidに対してのsetterを追加している(idがstrategy="IDENTITY"なので上記コマンドだけだとidに対するsetterが生成されない)。

<?php
use Acme\DemoBundle\Entity\User;

class DemoController extends Controller
{
    public function indexAction()
    {
        $user = new User();
        $user->setId(1);
        $em = $this->get('doctrine')->getEntityManager();
        $em->persist($user);
        $em->flush();

ドキュメントにあるように普通にこのようにすると3306ポートのMySQLにデータを保存してしまう。そこでapp/config/config.ymlに下記のようなORMの設定を追加する。

doctrine:
    dbal:
        ~上に同じなので省略~
    orm:
        auto_generate_proxy_classes: %kernel.debug%
        entity_managers:
            db1:
                connection: db1
                mappings:
                    AcmeDemoBundle: ~
            db2:
                connection: db2
                mappings:
                    AcmeDemoBundle: ~
            db3:
                connection: db3
                mappings:
                    AcmeDemoBundle: ~

PHP側のコードはこんな感じ。

<?php
use Acme\DemoBundle\Entity\User;

class DemoController extends Controller
{
    public function indexAction()
    {
        $user = new User();
        $user->setId(5);
        $em = $this->get('doctrine')->getEntityManager('db2');
        $em->persist($user);
        $em->flush();

これで3307にid:5が挿入される。データを取得する場合も同じようにEntityManagerを指定して取得すれば該当のMySQLに接続してデータを取得できる。

<?php
use Acme\DemoBundle\Entity\User;

class DemoController extends Controller
{
    public function indexAction()
    {
        $em = $this->get('doctrine')->getEntityManager('db2');
        $repos = $em->getRepository('AcmeDemoBundle:User');
        $user = $repos->find(5);

ShardManagerを使う

ここまでの情報をうまく使って実装してもSharding自体は可能だけど、Shardingのためのオリジナリティあふれる自前コードを書かなければならないし、Doctrineのコード全体に渡ってコネクション名を管理するのは避けたい。
そこで、Doctrine2.3で提供されているShardManagerインタフェースを使ってShardingを実現するというのがこのエントリの本旨。ここまでに書いたデータベースの切り替え方法は他でもよく書かれていることなのだけど、土台から書かないとよくわからない感じになりそうだったので書いてみた。

13. Sharding — Doctrine DBAL 2.1.0 documentation

このページは英語だけどDoctrineに限らないShardingをする上での検討事項がわかりやすく書いてあるので原文を参照するのがおすすめ。主にShardingすることによる制約、Sharding対象の全データベースにまたいで一意になるプライマリキーをどのように生成するか、ShardManagerインタフェースの使い方が書かれている。

データベースにまたがって一意になるプライマリキーの生成

13.1.2. Table Generatorの方法をここではやってみる。IDをインクリメントするだけの管理テーブルを作成して採番する方法。Single point of failureなどのこの方法に起因する欠点も原文に書かれているので参照のこと。

下記のようにテーブルを作成する。ここでは3306ポートのMySQLに下記SQLを実行(Doctrine\DBAL\Id\TableGeneratorのdocコメントにこのテーブル定義が書いてある)。

CREATE TABLE `sequences` (
  `sequence_name` varchar(255) NOT NULL,
  `sequence_value` int(11) NOT NULL DEFAULT '1',
  `sequence_increment_by` int(11) NOT NULL DEFAULT '1',
  PRIMARY KEY (`sequence_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

あとはPHPでTableGeneratorを利用すればすぐに採番を開始できる。

<?php
use Doctrine\DBAL\Id\TableGenerator;

class DemoController extends Controller
{
    public function indexAction()
    {
        $conn = $this->get('doctrine')->getConnection('db1');
        $tableGenerator = new TableGenerator($conn, 'sequences');
        $id = $tableGenerator->nextValue('user');

sequencesテーブルは下記のようになる。nextValueする度にsequence_valueがsequence_increment_by分だけインクリメントする。

mysql> select * from sequences;
+---------------+----------------+-----------------------+
| sequence_name | sequence_value | sequence_increment_by |
+---------------+----------------+-----------------------+
| user          |              1 |                     1 |
+---------------+----------------+-----------------------+
1 row in set (0.00 sec)

ShardManagerを使ってShardingする

プライマリキーの生成ができるようになったので、いよいよShardManagerを使ってShardingをしてみる。ShardManagerはインタフェースなので今回はDoctrine\DBAL\Sharding\PoolingShardManagerクラスを使って実装する。実装結果を先に見た方がわかりやすいのでまずはPHPから。

<?php
use Doctrine\DBAL\Id\TableGenerator;
use Doctrine\DBAL\Sharding\PoolingShardManager;
use Acme\DemoBundle\Entity\User;

class DemoController extends Controller
{
    public function indexAction()
    {
        $conn = $this->get('doctrine')->getConnection();
        $shardManager = new PoolingShardManager($conn);

        // globalとして設定しているデフォルトの接続先に接続
        $shardManager->selectGlobal();

        $tableGenerator = new TableGenerator($conn, 'sequences');
        $id = $tableGenerator->nextValue('user');

        // $idを基準にして接続すべきデータベースへ接続
        $shardManager->selectShard($id);

        $user = new User();
        $user->setId($id);
        $em = $this->get('doctrine')->getEntityManager();
        $em->persist($user);
        $em->flush();

大体こんな感じになる。selectGlobal、もしくはselectShardするとその後のクエリは全て同じデータベースに流される。つまり、適切にselectShardさえできれば個々のクエリの接続先は意識しなくても大丈夫。データベースをまたがってのトランザクションは制約として使えないので、1トランザクションで1shardということを意識してShardManagerを扱う。

selectGlobalはデフォルトとして設定されている接続先(後述)に接続する。selectShardは引数を基準にしてSharding先に接続する。
データを取得するときも同じで、事前にselectShardさえしていれば特定のEntityManagerを呼び出したりしなくても大丈夫。

<?php
use Doctrine\DBAL\Id\TableGenerator;
use Doctrine\DBAL\Sharding\PoolingShardManager;
use Acme\DemoBundle\Entity\User;

class DemoController extends Controller
{
    public function indexAction()
    {
        $conn = $this->get('doctrine')->getConnection();
        $shardManager = new PoolingShardManager($conn);

        $shardManager->selectShard($id); // $idはリクエストなどから取得した仮定
        $em = $this->get('doctrine')->getEntityManager();
        $repos = $em->getRepository('AcmeDemoBundle:User');
        $user = $repos->find($id);

ShardManager#selectShardしたときの接続先を決定する

selectShardに渡される基準値を使って接続先を決める必要がある。これはDoctrine\DBAL\Sharding\ShardChoser\ShardChoserインタフェースを実装したクラスを作成して行う。selectShardしたときに内部ではShardChoserが呼び出される。

<?php
namespace Acme\DemoBundle;

use Doctrine\DBAL\Sharding\PoolingShardConnection;
use Doctrine\DBAL\Sharding\ShardChoser\ShardChoser;

class SimpleShardChoser implements ShardChoser
{
    public function pickShard($distributionValue, PoolingShardConnection $conn)
    {
        return ($distributionValue % 2) + 1;
    }
}

ここではあくまでも例なので偶数だったら1、奇数だったら2を返す単純なもの。このpickShardの戻り値が各shardのid(後述)になる。つまりselectShardの引数を使ってshardのidを返すように実装する。

ShardManagerが動作するように設定を行う

ここが難所で、今のところ正攻法な解決ができていない(というかわからない)。普通にapp/config/config.ymlに設定しようとするとShardManagerの設定をDoctrine\Bundle\DoctrineBundle\DependencyInjection\Configurationが想定していないので、設定エラーとみなされてしまう。この辺はおいおい対応されるのかなという感じ。今のところは13.8. Generic SQL Sharding Supportにあるように直接PHPコードで設定してしまうしかないのかもしれない。
よろしくないけど、一応こういうことをすると設定が書けるようになるっちゃなる(真似しないようにしましょう)。

--- Configuration.php    2012-09-11 08:24:24.000000000 +0000
+++ vendor/doctrine/doctrine-bundle/Doctrine/Bundle/DoctrineBundle/DependencyInjection/Configuration.php    2012-09-19 14:25:52.615927030 +0000
@@ -139,6 +139,17 @@
                 ->booleanNode('profiling')->defaultValue($this->debug)->end()
                 ->scalarNode('driver_class')->end()
                 ->scalarNode('wrapper_class')->end()
+                ->scalarNode('shardChoser')->end()
+                ->arrayNode('global')
+                    ->useAttributeAsKey('key')
+                    ->prototype('scalar')->end()
+                ->end()
+                ->arrayNode('shards')
+                    ->prototype('array')
+                        ->useAttributeAsKey('key')
+                        ->prototype('scalar')->end()
+                    ->end()
+                ->end()
                 ->booleanNode('keep_slave')->end()
                 ->arrayNode('options')
                     ->useAttributeAsKey('key')

この変更を行った上でのapp/config/config.ymlはこんな感じ。

doctrine:
    dbal:
        connections:
            default:
                wrapper_class: Doctrine\DBAL\Sharding\PoolingShardConnection
                shardChoser:   Acme\DemoBundle\SimpleShardChoser
                global:
                    driver:   pdo_mysql
                    host:     127.0.0.1
                    port:     3306
                    dbname:   sharding
                    user:     sharding
                    password: sharding
                    charset:  UTF8
                shards:
                    -
                        id:       1
                        driver:   pdo_mysql
                        host:     127.0.0.1
                        port:     3307
                        dbname:   sharding
                        user:     sharding
                        password: sharding
                        charset:  UTF8
                    -
                        id:       2
                        driver:   pdo_mysql
                        host:     127.0.0.1
                        port:     3308
                        dbname:   sharding
                        user:     sharding
                        password: sharding
                        charset:  UTF8
    orm:
        auto_generate_proxy_classes: %kernel.debug%
        auto_mapping: true

PHPで設定するにしてもYamlで設定するにしても、ポイントはwrapper_classでConnectionクラスをラッパークラスに切り替えること。上記設定はあくまでもDoctrine\DBAL\Sharding\PoolingShardConnectionに準じた設定になっていて、この辺もおいおい変更されていく可能性がありそうだなと思う。

横浜まで来たら是非立ち寄りたいビアカフェHOPMAN

神奈川県の茅ヶ崎にあるBeer Cafe HOPMAN。東京からだと少し遠いのだけど、横浜や小田原からだとそれぞれ30分で行ける。茅ヶ崎駅からは歩いて5分。

ハンドポンプ@HOPMAN
ハンドポンプ@HOPMAN posted by (C)cloned

日本の地ビールや世界のビールを樽生で30種類(ハンドポンプ2本)用意しております

Beer Cafe HOPMAN

30種類の樽生というと都内のビアバー激戦区でも指折りの数だと思う。【国内ビアバー】樽生ビール取扱い数ランキング【1位〜12位編】によるとHOPMANは堂々4位ということらしい。

志賀高原ビール・AFPA@HOPMAN
志賀高原ビール・AFPA@HOPMAN posted by (C)cloned

ビールの種類が多いというだけでもメリットだけど、特に注目したいのはお料理。「安くて旨い」というのはこういうお店を言うものだなと思った。

しいたけの炭火焼@HOPMAN
しいたけの炭火焼@HOPMAN posted by (C)cloned

運ばれて来た瞬間にしいたけの香りが広がり、口にすると炭火の香りと味がぐっとくるのに300円。

ヘルシー野菜グラタン@HOPMAN
ヘルシー野菜グラタン@HOPMAN posted by (C)cloned

大口な野菜がたっぷり入った熱々のグラタンが480円(写真はSサイズ)。

きゅうりの天ぷら@HOPMAN
きゅうりの天ぷら@HOPMAN posted by (C)cloned

甘みたっぷりのキュウリがカラっと揚げたてで380円。

食べログを見ると、星が3.52というのも凄いけど、「夜(平均)¥3,000〜¥3,999」というのも地味に凄いと思う。他のビアバーだと5,000円を超えることが多いと思う。

柔らかくなるまで何時間も炒めた玉ねぎが美味しいカレーライスも530円と、とてもビアカフェという値付けではない。旬の野菜を仕入れてくれているのでつい野菜ばかり注文してしまったけれど、もちろんチキン&チップスや手羽先などのお肉なメニューもある。

HOPMANのカレーライス@HOPMAN
HOPMANのカレーライス@HOPMAN posted by (C)cloned

お店は照明が明るくてスペースが(席の間隔も)広い。また整理整頓・清掃がきっちりされていてきれいだった。化粧室前は雰囲気が違ってこんな感じでお洒落。

化粧室前@HOPMAN
化粧室前@HOPMAN posted by (C)cloned

オーナーは「気軽に来れる店にしたい」ということを言っていたけど、この価格設定でこの種類のビールでこのお店の雰囲気ですからすでに実現しているじゃないですかと感じた。もちろん今後も引き続き期待しています(オーナー写真の掲載も快くOKしてくれた)。

オーナー@HOPMAN
オーナー@HOPMAN posted by (C)cloned

僕は独身だったころ結構一人で下北沢のビアバーうしとらに通っていたけど、うしとらに通えたのは一人でも行けるくらい雰囲気が良くてお客さんがフレンドリーで、そして何よりスタッフがお客さんの会話を聞き漏らさずにちゃんと話をつないでくれるお店だったからだと思う。HOPMANにもその良さがきっちりあって、オーナーを含めスタッフの方と会話を楽しむために来ているお客さんも多そうな雰囲気だった。

お隣さんのお誕生日サイズ@HOPMAN
お隣さんのお誕生日サイズ@HOPMAN posted by (C)cloned

お店に伺った昨日はちょうどオーナーの誕生日だったのだけど、なんとお隣のカウンターに座った方も誕生日ということで、お誕生日サイズのビールを召し上がっていた。おめでとうございました。

1年半前にHOPMANに行ったときは、どちらかというと無事オープンおめでとうということで書いたけど、オープンからもうすぐ2年を迎えるに当たってきっちり理想のお店を目指して経営されているなと心から感じた。