Hatena::ブログ(Diary)

Blog::koyhoge

2013-12-16

アジャイルメディア・ネットワークを2013年12月一杯で退職することになりました

アジャイルメディア・ネットワーク株式会社に2010年9月に入社してから3年と少し、サーバサイド・クライアントサイド・インフラエンジニアとして働いてきましたが、今月いっぱいで退職することになりました。入社時のエントリを今見かえして、当時はCTO福田さんしかエンジニアがいなくて、とにかく何でもやったなぁと思い出します。

アジャイルメディア・ネットワークに入社しました - Blog::koyhoge


今後については、なにせ急に決まった退職話なので、またフリーランスに戻る事以外は全くのノープランです。2014年冒頭は、仕事のお話を探しつつも、図らずして時間の余裕ができたので、忙しさを理由にしてキャッチアップできていなかった各種技術の習得に時間を割きたいと思います。


もし何かお仕事のお話があれば、メール/twitter等でお気軽にご連絡下さいませ。

サイボウズ式に「モバイルフロンティア」の紹介文を書きました

tech@サイボウズ式」の風穴さんから、エンジニアの方々にアドベントカレンダー的に書籍の紹介記事をお願する企画があるので、小山さんもぜひ何か書いて下さい、と頼まれたが11月の末。何について書こうかといろいろ考えた挙句、やはり今年の書籍といえばこれだよなと、「モバイルフロンティア よりよいモバイルUXを生み出すためのデザインガイド」を選びました。


「モバイルフロンティア」──techな人にお勧めする「意外」な一冊(11) | サイボウズ式


いや、これ本当に良い本なので、みなさんぜひ読んで下さい。


2013-12-14

FuelPHPでMongoDBをちょびっと便利に使う

MongoDB Advent Calendar 2013の14日目です。まぁ途中で一度途切れているので気楽に行きましょうw


さて、このエントリはここ連続で続いている FuelPHP ネタでもあります。

MongoDBSQL的なシーケンスをどうするか

FuelPHPMongoDBを使うには、Coreに含まれている Mongo_Dbクラスを使うのが普通だと思います。基本的なメソッドはひと通り用意されていて、通常使う文にはまあ困らないでしょう。ただしちょっと突っ込んだことをやろうとすると、そのままでは使えなくて何らかの拡張を自分ですることになるのもよくあることです。


MongoDBアプリケーションデータの格納に使うとして、SQL的なシーケンス(SEQUENCE)が欲しいことがあります。MySQLでいうところのSERIAL属性と同様で、一意な値を自動採番していくれる仕組みです。むろんMongoDBでもちょっとした仕掛けで実現可能で、そのための手法はちょっとググれば簡単に見つかります。ただし FuelPHP の Mongo_Db クラスでは、PHP の Mongo オブジェクトがプライベート変数になっていて直接アクセスできるメソッドもないので、直接アクセスするには Mongo_Db クラスを継承したクラスを用意する必要があります。そうやって作ったのが、以下の Util_MongoPlus クラスです。

<?php

class Util_MongoPlus extends \Fuel\Core\Mongo_Db
{
    protected $seq_collection = 'sequences';

    public function getRawMongo()
    {
        return $this->db;
    }

    public function seqNext($name)
    {
        $query = array(
            '_id' => $name,
            );
        $update = array(
            '$inc' => array('seq' => 1),
            );
        $options = array(
            'new' => true,
            'upsert' => true,
            );

        $result = $this->db->{$this->seq_collection}->findAndModify(
            $query,
            $update,
            null,
            $options
            );
        return $result['seq'];
    }
} 

直接 Mongo オブジェクトにアクセスする getRawMongo メソッドと、シーケンスを実現する seqNext メソッドを持っています。シーケンスは sequences というコレクションを作り、そこにシーケンス名がキーのレコードを作成して、アトミックにインクリメントを行うことで実現しています。


※ とここまで書いていて、Mongo_Db::get_collection メソッドを用いれば任意のコレクションにアクセスできるので、直接 Mongo オブジェクトを使う必要がないことに気がつきました。まあいいやw

MongoDBをベースにしたモデルのベースクラスを作る

で、ここまでできたらもういっそ Model のベースクラスまで作ってしまえということで、できたのが以下です。

<?php
use Fuel\Core\Date;

class Model_Mongobase
{
    static protected $table_name = null;
    static protected $uniq_id = null;

    static protected $timestamp = null;

    static public function getMongo()
    {
        return \Util_MongoPlus::instance();
    }

    static public function getTable()
    {
        return static::$table_name;
    }

    static public function getSeqName()
    {
        // dummy
        return null;
    }

    static public function newId()
    {
        $mongo = static::getMongo();
        $seq = static::getSeqName();
        if (empty($seq)) {
            throw new Exception('empty sequence');
        }
        return $mongo->seqNext($seq);
    }

    // base methods
    static public function get($id)
    {
        if (static::$uniq_id === null) {
            throw new Exception('Unique key is not found');
        }

        $mongo = static::getMongo(); 
        $conds = array(
            static::$uniq_id => (int)$id,
            );
        $result = $mongo->get_where(static::getTable(), $conds);
        if (count($result) === 0) {
            throw new Exception('Item not found');
        }
        return $result[0];
    }

    static public function all($options = null)
    {
        $conds = array_val($options, 'conditions');
        $order_by = array_val($options, 'order_by');
        $use_cursor = array_val($options, 'use_cursor', false);

        $mongo = static::getMongo();
        if (!empty($conds)) {
            $mongo->where($conds);
        }
        if (!empty($order_by)) {
            $mongo->order_by($order_by);
        }
        if ($use_cursor) {
            $result = $mongo->get_cursor(static::getTable());
        } else {
            $result = $mongo->get(static::getTable());
        }
        return $result;
    }

    static public function save($data)
    {
        if (static::$uniq_id === null) {
            $result = static::saveWithNonuniq($data);
        } else {
            $result = static::saveWithUniq($data);
        }
        return $result;
    }
    static public function saveWithNonuniq($data)
    {
        // call hook
        static::beforeInsert($data);

        $mongo = static::getMongo();
        return $mongo->insert(static::getTable(), $data);
    }

    static public function saveWithUniq($data)
    {
        $mongo = static::getMongo();

        $id_name = static::$uniq_id;

        $id = array_val($data, $id_name);
        if (empty($id)) {
            $id = static::newId();
            $data[$id_name] = $id;

            static::beforeInsert($data);
            $result = $mongo->insert(static::getTable(), $data);
        } else {
            $conds = array(
                $id_name => (int)$id,
                );

            static::beforeUpdate($data);
            $result = $mongo->where($conds)->update(static::getTable(), $data);
        }
        return $result;
    }

    static public function deleteWithUniq($id)
    {
        $mongo = static::getMongo();

        if (static::$uniq_id === null) {
            $id_key = '_id';
        } else {
            $id_key = static::$uniq_id;
        }
        $conds = array(
            $id_key => (int)$id,
            );
        return $mongo->where($conds)->delete(static::getTable());
    }

    static public function deleteAll($conds)
    {
        $mongo = static::getMongo();

        return $mongo->where($conds)->delete_all(static::getTable());
    }

    static protected function beforeInsert(&$data)
    {
        $hook = array_val(static::$timestamp, 'before_insert');
        if (empty($hook)) {
            return;
        }
        $key = array_val($hook, 'key');
        static::handleTimestamp($data, $key);
    }

    static protected function beforeUpdate(&$data)
    {
        $hook = array_val(static::$timestamp, 'before_update');
        if (empty($hook)) {
            return;
        }
        $key = array_val($hook, 'key');
        static::handleTimestamp($data, $key);
    }

    static public function handleTimestamp(&$data, $key)
    {
        foreach ($key as $k) {
            $data[$k] = Date::forge()->format('mysql');
        }
    }
}

あー、いつものarray_val()を使ってますね。ここで紹介している自作の便利関数です。


使用するには、例えば以下の様な Model_Mongobase を継承したクラスを作って

<?php

class Model_Item extends Model_Mongobase
{
    static protected $table_name = 'item';
    static protected $sequence = 'item';
    static protected $uniq_id = 'id';

    static protected $timestamp = array(
        'before_insert' => array(
            'key' => array('created_at', 'updated_at'),
            ),
        'before_update' => array(
            'key' => array('updated_at'),
            ),
        );

    static public function getSeqName()
    {
        return static::$sequence;
    }
}

あとは任意のデータを格納するだけです。

<?php
  :
    Model_Item::save(array('fuga' => 'hoge'));

継承したクラスで

static protected $uniq_id = 'id';

のように $uniq_id プロパティが定義されていれば、それに対してシーケンス適用してくれます。


いつもなら Package にまとめて github で公開したりするんですが、これに関してはちょっとどういうまとめ方が良いのか悩んでいるところもあって、とりあえず生ソースをブログで公開することにしました。


明日のアドカレは@さんです。

2013-12-08

FuelPHPのViewの自動エスケープについて

前回のエントリ「JavaScript側にPHP変数を簡単にまるごと渡す方法 #FuelPHPAdvent2013 - Blog::koyhoge」について、PHPのjson_encode()関数は標準ではエスケープ処理は行わないのでXSS脆弱性があるのではないか、という指摘をいただきました。

json_encode()のエスケープオプション

確かにPHPマニュアルには、各種文字にエスケープ対応するオプションが存在します。

PHP: json_encode - Manual

この場合で言えば

    return sprintf($fmt, $name, json_encode($val)); 

を以下のようにエスケープオプションを追加するべきということです。

    return sprintf($fmt, $name, json_encode($val, JSON_HEX_TAG |JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP)); 

ただこのコードを書いた時に自動的にエスケープ処理がかかることを確認していたので、どこでそれが行われるかは深く調べずに、json_encodeのオプションを省いたという経緯がありました。

自動エスケープは\Fuel\Core\Viewの機能だった

その後fuelphp.jpグループで@さんに指摘されて、Parserパッケージの標準設定で 'auto_encode’ が true になっているおかげでテンプレートに渡される変数が自動でエスケープされていた事がわかりました。

fuel/packages/parser/config/parser.php

の以下の部分ですね。

<?php
:
        'View_Twig' => array(
                'auto_encode' => true,
:

この auto_encode 設定は、\Fuel\Core\View のコンストラクタに $auto_filter として渡され、結果的に\Fuel\Core\Security::clean() が呼び出されます。つまりTwig Extensionに渡される際にはすでにエスケープ済になっていたわけですね。


PHP 変数JSON にして JavaScript に渡す仕組みは、別に FuelPHP でなくても使用できますので、その場合は XSS に注意して json_encode にオプションを随時追加して下さい。

JSON の埋め込み方の問題

他にもfuelphp.jpグループでは@さんより、HTML 要素にテキストとして JSON を書き出すよりは、要素の data-option 属性として埋め込んだ方が良いのではないかとの指摘を受けました。

  <div class="hidden">{"fuga":"hoge"}</div>

ではなく

  <div class="hidden" data-option='{"fuga":"hoge"}'></div>

とせよということですね。ふむー、これはちょっと試してみたいと思います。

Twigのクラスが古かった

元記事とその前の記事で用いた以下のクラスはすでに古く、2.0でなくなる予定だと@さんに指摘いただきました。

  • Twig_Filter_Function
  • Twig_Function_Method

これは気が付いてなかったので、Twig_SimpleFunction, Twig_SimpleFilter を使うように元記事を修正しました。

その他の反応への返事

はてブより。

id:teppeisid:teppeis $nameもjson_encode()もエスケープが足りないです。危険。

http://b.hatena.ne.jp/teppeis/20131207#bookmark-172246146

json_encodeについては上記に書いたとおり。$nameはテンプレートに直接記述されるので、そこに外部からの変数が渡される事態は、コード全体を見直したほうが良いレベルだと思うのですがどうでしょう?

id:thujikunid:thujikun JSON形式のコードをJS変数に直接代入する方が楽な気が。。。ひとつグローバル変数使うことにはなるけども。

http://b.hatena.ne.jp/thujikun/20131208#bookmark-172246146

JavaScriptテンプレートエンジンを通して変数展開を埋め込む方が、自分的にはあり得ないです。HTMLに埋め込み JS を直接記述することは現在は全くやっていません。

id:fakechanid:fakechan PHPレガシーっぷりに驚きを隠せない。というか、こういう場合はREST APIを作って「js側から」Ajaxでアクセスすればいいのでは。Ajaxのロードが終わるまでは「ロード中...」とかかぶせて。

http://b.hatena.ne.jp/fakechan/20131208#bookmark-172246146

いやこれとPHPレガシーは関係ないでしょ。PHPディスりたい病にかかっているようですね。何でもRESTAjaxすれば良いやというのは、JS 側の処理を無駄に複雑にするだけではありませんか?

2013-12-06

JavaScript側にPHP変数を簡単にまるごと渡す方法 #FuelPHPAdvent2013

ハイ、昨日のオレに引き続きFuelPHP Advent Calendar 2013の6日目です。

今回の内容もまたTwig絡みです。実は昨日の記事は、本日の記事の前準備になっていたのでした。

JavaScript側にPHPオブジェクトを渡したい

最近のWebアプリUIのインタラクションが凝っていて、ブラウザ側のJavaScriptで色んな制御をすることも当たり前になってきました。jQueryや様々なjQueryプラグインを駆使して、ユーザに分かりやすく使いやすいサービスを提供することは、もはやウェブエンジニアとしては持っていて当然のスキルになっています。


そのようなUIを作っている際に、JavaScript側に動作パラメータの初期値を渡すのに値を一つ一つテンプレート記法で埋め込むのが面倒だったので、一発で渡せるTwig Extensionを作ったので紹介します。

data_bind関数

アイデアとしては、見えないHTML要素を作成してそのテキストに値をJSON化して突っ込もうという、まぁ普通に思いつきそうなものです。でもこれがやってみると思ったより便利で。

extension本体はこんなコードです。

<?php
class Hoge_Twig_Extension extends Twig_Extension
{
  :
    public function getFunctions()
    {
        return array(
            new Twig_SimpleFunction('data_bind', array($this, 'dataBind')),
            );
    }

    public function dataBind($name, $val, $exclude = null)
    {
        if (is_object($val) && is_callable(array($val, 'to_array'))) {
            $val = $val->to_array();
        }
        if (!empty($exclude)) {
            if (is_string($exclude)) {
                $exclude = array($exclude);
            }
            foreach ($exclude as $key) {
                unset($val[$key]);
            }
        }
        $fmt = '<div id="data-%s" class="hide">%s</div>';
        return sprintf($fmt, $name, json_encode($val));
    } 
}

div要素を不可視にするために、ここでは'hide'というclassを指定していますが、これはCSS

.hide {
  display: none;
}

的なものがあることを前提にしています。Bootstrapには含まれてますね。もちろん直接styleを書いてしまってもよいでしょう。


コントローラ側のアクションメソッドで以下のようにテンプレートに値を渡して、

<?php
 :
  function action_xxx()
  {
    :
    // $userinfo は情報が入ったObjectまたは連想配列
   $this->template->user = $userinfo;
  }

テンプレート側ではこう記述します。

{{ data_bind('user', user) }}

JavaScript側でその値を使用するには、例えばjQueryだったら

  var user = $.parseJSON($('#data-user').text());

と書くと、user変数PHPで渡した値が入ります。

パラメータの解説

data_bind 関数は3つのパラメータを持ちます。

$name: 名前

HTML上で展開される名前です。'data-名前' がその要素のidになります。

$val: 変数

展開する変数です。Twigの変数になります。

$exclude: 排除するキー (省略可能)

変数を全部JS側に渡すのが楽とはいっても、ユーザ側に公開したくない内部プロパティが含まれているかもしれません。そういう場合には、第3引数にそのプロパティ名を渡すことであらかじめ削除した上で展開することができます。

単なる文字列として指定することもできますし、

{{ data_bind('user', user, 'password') }}

配列にして複数指定することもできます。

{{ data_bind('user', user, ['password', 'rank']) }}

ということでお手軽 tips でした。明日のアドベントカレンダーは@さんの[W] FuelPHP開発でローカルとWebで構造が変わっても対応できる小技 | Work Tool Smith [ワークツールスミス]です。



12/8 追記:

HTMLに埋め込む際のエスケープ処理に関しては、状況によってはjson_encode()にエスケープ用のオプションが必要です。またTwig_Function_Method はすでに古いインターフェースだと @ さんからご指摘を受けたので、現在推奨されている Twig_SimpleFunction に書き換えました。詳しくは以下をご覧ください。

FuelPHPのViewの自動エスケープについて - Blog::koyhoge

2013-12-05

FuelPHPでTwig Extension #FuelPHPAdvent2013

4日目の@さんのFuelphpのエラーハンドリングがなんか今ひとつ物足りなかったのでなんとかしてみた話 - どうにもならない日々@mkknに引き続き、FuelPHP Advent Calendar 2013の5日目です。


ここ数年はアドベントカレンダーの時にしか技術的な内容を書いていない気がするのが恐ろしいところですが、気にせずいきましょう。

FuelPHPのParserパッケージ

FuelPHPは、基本的にはビューに生のPHPスクリプトを使うことになっていますが、標準バンドルされているParserパッケージを用いることで、様々なテンプレートエンジンを用いることができます。現在サポートされているエンジンは以下の通り。

このうち自分ではTwigを愛用しています。何か機能を追加するにも簡単にできるところが良いですね。

Parserパッケージが標準で用意してくれるFuelPHP向けExtension

ParserパッケージでTwigを使用すると、Uri, Config, Form, Input, Html, Asset などの便利そうなFuel coreのメソッドを、あらかじめTwig Extensionとしてロードしてくれます。これを行っているのは

fuel/packages/parser/classes/twig/fuel/extension.php

にある\Parser\Twig_Fuel_Extensionクラスで、これ自体も標準的なTwig Extensionです。

これのおかげで、例えば

Asset::js('hogehoge.js');

を呼びたい場所では

{{ asset_js('hogehoge.js') }}

と書くことができるわけです。

アプリ独自のTwig Extensionを使う

とはいえ、ただ単にTwigを使ってHTMLテンプレートを書くだけではなく、アプリケーション独自のTwig Extensionをがしがし登録して使いこなしてこそTwigの便利さが際立つというもの。早速やってみましょう。


独自のTwig Extensionを登録するには、まずTwig_Extensionクラスを継承したクラスを作ります。クラス名は他とぶつからなければ何でも良いですが、ここではHogeアプリ向けにHoge_Twig_Extensionという名前にすることにしましょう。FuelPHPのファイル名規則に則り以下の場所に作ります。

fuel/app/classes/hoge/twig/extension.php

中身はこんな感じ。

<?php

class Hoge_Twig_Extension extends Twig_Extension
{
    /**
     * Gets the name of the extension.
     *
     * @return  string
     */
    public function getName()
    {
        return 'hoge';
    }

    /**
     * Sets up all of the functions this extension makes available.
     *
     * @return  array
     */
    public function getFunctions()
    {
        return array(
            new Twig_SimpleFunction('swap_empty', array($this, 'swapEmpty')),
        );
    }

    /**
     * Sets up all of the filters this extension makes available.
     *
     * @return  array
     */
    public function getFilters()
    {
        return array(
            new Twig_SimpleFilter('json', 'json_encode'),
        );
    }

    public function swapEmpty($value)
    {
        return empty($value)? '-' : $value;
    } 
} 

ここではTwigの関数とフィルターを一つずつ登録しています。

swap_empty関数
もし引数がempty()で真だったら「-」を出力、そうでなければそのまま。
jsonフィルター
引数PHPのjson_encodeに渡した結果を出力。

テンプレート上では以下のように使います。

{# 変数の設定、本来はPHP側から渡される #}
{% set foo = 0 %}
{% set bar = {'fuga': 'hoge', 'move': 'puge'} %}

{{ swap_empty(foo) }}
{{ bar|json }}

ただファイルを置いただけではParserパッケージはそのExtensionの存在を知らないので、Parserのconfigを通して教えてやります。FuelPHPのConfigは大変に賢くて、追加・変更したい部分だけapp以下に書けば良いので、

fuel/app/config/parser.php

に以下の内容を記述します。

<?php
return array(
    'View_Twig' => array(
        'extensions' => array(
            ‘Hoge_Twig_Extension',
            ),
        ),
); 

Twigは非常に柔軟性の高いテンプレートエンジンで、上記で紹介した関数・フィルターの他にも

などを独自に拡張できます。詳しく知りたい方は、Extending Twig を読むとよいでしょう。


明日のアドベントカレンダーも引き続き私ですw


12/8追記:

Twig_Function_Method, Twig_Filter_Function はすでに古いインターフェースだと @ さんからご指摘を受けたので、現在推奨されている Twig_SimpleFunction, Twig_SimpleFilter に書き換えました。