DataMapperことはじめ

DataMapperとは

CodeIgniter2.x用のORMライブラリ。
http://datamapper.wanwizard.eu/index.html

  • 特徴
    • 導入・設定が簡単
    • 各プロパティのバリデーションが可能
    • 1対1, 1対多, 多対多のリレーションをサポート
    • (CodeIgniterの)ActiveRecordライクなデータセレクトが可能

CodeIgniterのActiveRecordクラスと比較すると、3番目のn対nのリレーションを自動的に扱ってくれる点が優れている。
いちいちJOINを書くのはだるいです。

基本的な使い方(モデル編)

<?php
class Student extends DataMapper {
}

基本的にはこれだけで使える。
本家本元のActiveRecordと同じく、Studentモデルはstudentsテーブルを参照する。(=モデル名を複数形にしたテーブルを参照する)


クラス変数tableを定義することで、参照先テーブルは変更可能。

<?php
class Student extends DataMapper {
	var $table = 'students';
}


バリデーション、リレーション、クラスメソッドなども書ける。

<?php
class Student extends DataMapper {
    // リレーション
    var $has_many = array('team');
    // バリデーション
    var $validation = array(
        'name' => array(
            'label' => 'Student',
            'rules' => array('required', 'trim', 'unique', 'alpha_dash', 'min_length' => 1, 'max_length' => 50),
        );
    // クラスメソッド
   function hoge {
      // something do
   }
}

基本的な使い方(コントローラー編)

<?php
class Test extends CI_Controller {

    function __construct() {
        parent::__construct();
        
    }
    
    function index() {
       // $this->load->model('student');
        
       $student_model = new Student();
       $student = $student_model->get_by_id(1);
    // 以下の書き方でもOK
       // $this->load->model('student');
       // $student = $this->student->get_by_id(1);
    echo $student->name;
    }
}

リレーションシップについて

studentsテーブル

id name
1 田中
2 北嶋
3 大津
4

temas

id name
1
2 広島

teams_and_studentsテーブル

team_id student_id
1 1
1 2
1 3
1 4
2 4

こんな感じでStudentモデルとTeamモデルの間に多対多の関係があるとする。
モデルとコントローラーは以下のように書ける。

<?php
class Student extends DataMapper {
 
    var $has_many = array(
        'teams' => array(
            'class' => 'team', // リレーション先のクラス(モデル)を指定
	  'join_table' => 'teams_and_students', // JOIN用テーブル名を指定
            'join_other_as' => 'team_id', // リレーション先のIDを指すカラム名を指定
            'join_self_as' => 'student_id', // 自身のIDを指すカラム名を指定
			'auto_populate' => TRUE // リレーションの自動取得を有効に
           )	
    );
<?php

class Test extends CI_Controller {

    function __construct() {
        parent::__construct();
        
    }
    
    function index() {
       $this->load->model('student');
       $student = $this->student->get_by_id(4);
       
       foreach($student->temas as $team)
       {
           echo $team->name . "<br/>";
       }
    }
}

おまけ

DataMapperの日本語用設定ファイルを書いた。
https://github.com/ninoseki/sparks-datamapper

見ているページのリンク先画象をまとめてスライドショー化する拡張機能を作った

https://chrome.google.com/webstore/detail/nfmfmnjjhlnmjhoncchpadcphhopcfoc

画像リンクが多いWebページを見てる時にいちいちクリックして開くの面倒だよねーってことで、見ているページのリンク先画像をざくっと集めてスライドショーとして見ることのできるChrome Extensionを作った。


使い方は単純。
画像リンクがあるWebページを閲覧すると、アドレスバーの右側のアイコンにスライドショーで表示される画象数が表示される。この画像数が1つ以上の時にアイコンをクリックすると別のタブでスライドショーが始まる。


よろしければ使ってみてください。

Javascript製Markdown記法パーサー、markdown-js

http://profile.ak.fbcdn.net/hprofile-ak-snc4/50232_50595864761_2145_n.jpg
Githubなどでお馴染みのMarkdown
Markdown記法パーサーをJavascriptで実装しようとする試みはいくつかあるけれど、markdown-jsは自分が知る中では一番良く出来ている。


ソースを見てもらえればわかると思うけど、Showdownやこれから派生したGFM previewはあまりにも処理を追いにくい。よって拡張しにくい。
その点、markdonw-jsは入力に対してのどのような出力がでるのか処理を追いやすいし、拡張性にも優れている。なかなか良い感じだ。


使い方も単純。

var input = "# input\n hoge  \nhoge";
console.log(window.markdown.toHTML( input );

/* 出力 */
// <h1>input</h1>
//
// <p>hoge<br>hoge</p></div>

Chromeウェブアプリの多重起動を抑制するためのTips

Chromeウェブアプリは基本的に多重起動可能(複数のタブで開ける)。
そのため、ステートフルなウェブアプリで多重起動されてしまうと状態管理がめちゃくちゃになってしまう危険性がある。


ということでChromeウェブアプリの多重起動を抑制し、単一起動に限定させてみるテスト。
background_pageで以下のスクリプトを走らせとけばOK。

// ウェブアプリを開いているタブ
var theTab = undefined;

function changedHandler(tab) {
    // chrome.extension.getURL(path)で指定したリソースへのフルパスが取得できる
    // 引数pathはmanifest.jsonで定義したlocal_pathと一致させること
    if (tab.url === chrome.extension.getURL("index.html")) {
        if (theTab) {
            if (tab.id !== theTab.id) {
                // すでにアプリを開いているタブがある場合、そのタブにフォーカスする
                console.log("duplicated!");
                chrome.tabs.remove(tab.id);
                focus_to_the_tab();
            }
        } else {
            console.log("created");
            theTab = tab
        }
    } else {
        if (theTab && tab.id == theTab.id) {
            removeHandler(tab.id)
        }
    }
}

function removeHandler(tabId) {
    if (theTab && tabId === theTab.id) {
        console.log("closed");
        // アプリを開いているタブが閉じられた場合、タブ情報を初期化する
        theTab = undefined;
    }
}

function focus_to_the_tab() {
    if (theTab) {
        chrome.tabs.update(theTab.id, {selected:true});
    }
}

chrome.tabs.onCreated.addListener(function(tab) {
    changedHandler(tab)
});

chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
    changedHandler(tab);
});

chrome.tabs.onRemoved.addListener(function(tabId, removeInfo) {
    removeHandler(tabId);
});

CodeIgniterでRailsライクなbefore/afterフィルターを

CodeIgniterでRailsライクなbefore/afterフィルターを実現するライブラリを見つけたんでメモ。

導入

https://github.com/zackkitzmiller/codeigniter-filter
からファイルをダンロードし、
/config/hooks.phpの中身を/application/config/hooks.phpに追加。
/hooks/Filter.phpを/application/hooks/に移動。
以上で終了。

使い方

コントローラーのメンバ変数として$before_filterと$after_filerを定義する。ここにフィルターとして実行されるアクションとフィルターの適用対象を記述する。
actionだけを指定すればコントローラー内のすべてのpublicメソッドにフィルターが適用される。Railsのようにonly, exceptで適用対象を限定することも可能。
beforeフィルターはコントローラーのコンストラクタが呼ばれた直後に、afterフィルターはアクションが実行された直後に実行される。

<?php
var $before_filter = array( 
     'action' => 'name_of_method_for_before_filter',
     'except' => array('index', 'logout');
);
var $after_filter = array( 
     'action' => 'name_of_method_for_after_filter',
     'except' => array('index', 'logout');
);


フィルターを複数指定することも可能。

<?php
var $before_filter = array();

$before_filter[] = array(
     'action' => 'before1',
     'except' => array('index', 'logout');
);

$before_filter[] = array(
     'action' => 'before2',
     'except' => array('index', 'logout');
);

この時のフィルターの適用順序は、配列の先頭からとなる。

<?php

class Test extends CI_Controller {
    
    var $before_filter = array();
    
    var $after_filter = array();
        
    function __construct() {
        parent::__construct();
        
        $this->load->helper('url');
        
        $this->before_filter[] = array(
            'action' => '_before_filter_run', // 直接実行されないように、フィルタ用アクションはprivateにしておこう
            'except' => array('home')
        );
                
        $this->after_filter[] = array(
            'action' => '_after_filter_run',
            'only' => array('sent_away')
        );
    }
    
    function index() {
        echo 'You made it to the index';
    }
    
    function home() {
        echo 'This is home<br/>';
        echo 'You may try going to the '.anchor('/test/index/', 'index'). ' of this controller.<br/>';
        echo 'The before_filter will execute the before_filter_run method before the controller action is executed.<br/>';
        echo 'It is demonstrated by having before_filter_run redirect to sent_away if the action is "index" <br/>';
    }
    
    function sent_away() {
        echo "You've been sent here by the before_filter_run action, called by the before_filter!<br/>";
        echo anchor('/test/home/', 'Click here to return to the test controller home');
    }
    
    function _before_filter_run() {
        
        $filter = array('index');
        
        if ( in_array($this->router->fetch_method(), $filter) ) {
            redirect('/test/sent_away/');
        } else {
            return true;
        }
    }
    
    function _after_filter_run() {
        echo '<br/>This text is generated from the after_filter_run method';
    }
}

SNBinderのAjax系メソッドをDefered化してみた

https://github.com/ninoseki/SNBinder
javascript用テンプレートエンジンSNBinderのAjax系メソッドをDefered Objectを返すように変更してみた。

Before
SNBinder.get_named_sections("/static/templates.htm", null, function(templates) {
    var user = { "name":"Leonardo da Vinci" };
    $('.body').html(SNBinder.bind(templates.main, user));
});
After
SNBinder.get_named_sections("/static/templates.htm", null).then(
        function(templates) {
            var user = { "name":"Leonardo da Vinci" };
            $('.body').html(SNBinder.bind(templates.main, user));
        });


Defered化しておくと、テンプレートのレンダリングと、レンダリングした要素へのイベンド操作をthenで分離できていいかなと。callbackを多用するより見通しいいっす。

SNBinder.get_named_sections("/static/templates.htm", null).then(
        function(templates) {
            // テンプレートのレンダリングのみを行う
            var user = { "name":"Leonardo da Vinci" };
            $('.body').html(SNBinder.bind(templates.main, user));
        }).then(function() {
            // イベント処理やらなにやら
        });

JasmineでSammy.jsアプリをテストする

RSpecライクなJavascript用BDDフレームワークJasmineで、SinatraライクなWebフレームワークSammy.jsで作られたアプリをテストしてみる。

非同期処理を伴わない場合

Sammy.js app
var app = $.sammy(function() {
    this.get('#/', function() {
        $('h1').html("hoge");
    });
};

ルートの実行が終了すると、event-context-afterトリガーが起こる。
この発生を検知してからテストを行えばOK。

Jasmine
describe('testing sammy.js app', function() {

    beforeEach(function() {
        $('body').append('<div id="jasmine"><h1>Replace me</h1></div>');
        app.executed = false;
        app.bind('event-context-after', function() {
            app.executed = true;
        });
    });

    afterEach(function() {
        $('div#jasmine').empty().remove();
    });

    it('should set h1', function() {
        app.run('#/');

        waitsFor(function() {
            return app.executed;
        }, "app doesn't stop", 10000);

        runs(function() {
            expect($('h1').html()).toEqual('hoge');
        });
    });

非同期処理を伴う場合

Sammy.js app

var app = $.sammy(function() {

    this.get('#/async', function() {
       // 非同期処理
       this.load('/public/data/myfile.txt')
       .then(function(content) {
         $('h1').text(content);
       });
    });	
});

ルート内で非同期処理を行うと、非同期処理の終了を待たずしてevent-context-afterトリガーが起こってしまう。
なので独自にルートの実行が終了したことを通知するイベントトリガーを作る必要がある。

var app = $.sammy(function() {

    this.get('#/async', function() {
       this.load('/public/data/myfile.txt')
       .then(function(content) {
         $('h1').text(content);
		 
     // ルートの実行が終了したことを知らせる
	 this.trigger('executed');
       });
    });	
});
Jasmine
describe('testing sammy.js app', function() {

    beforeEach(function() {
        $('body').append('<div id="jasmine"><h1>Replace me</h1></div>');
        app.executed = false;
        
        app.bind('executed', function() {
            app.executed = true;
        });
    });

    afterEach(function() {
        $('div#jasmine').empty().remove();
    });

    it('should be h1 text equals to public/data/myfile.txt', function() {
        app.run('#/async');

        waitsFor(function() {
            return app.executed;
        }, "app doesn't stop", 10000);

        runs(function() {
             expect($('h1').html()).toEqual(myfile-data);
        });
    });
});