「CakePHP2+Jenkinsで継続的インテグレーション」について話してきました

週末に行われた「(CakePHPとか)PHPのテストについての勉強会」@Co-Edoで、「CakePHP2+Jenkinsで継続的インテグレーション」について話してきました。

今回は、CIの概念とかメリットに関してはこことか、ここなど先人のすばらしい資料に丸投げし(^^;、実際にCakePHP2でCIする際の流れやビルドの設定方法などを中心に話しました。割と突発的に行われた勉強会だった印象なのに非常に中身の濃い勉強会で僕自身もとても勉強になりました!

僕の発表は資料だけみてもちょっと分かりにくそうなので以下補足です。

Jenkinsの設定

実際の設定は以下を見て、このページに紹介されている「Job Template」をコピーする方法を使えば、あとで紹介するbuildファイル用の設定が一通り完了しているJobを簡単に作れるかと思います。

CakePHP2用のBuildファイル サンプル(Ant)

以下が、Template for Jenkins Jobs for PHP Projectsで紹介されているBuildファイルをCakePHP2用に修正したものです。

Jenkinsで実行する際は、tools-parallel というターゲットを実行することになります。

<?xml version="1.0" encoding="UTF-8"?>

<project name="name-of-project" default="build">
 <target name="build" depends="prepare,lint,phploc,pdepend,phpcb,phpmd-ci,phpcs-ci,phpcpd,phpunit"/>
 <target name="build-parallel" depends="prepare,lint,tools-parallel,phpunit"/>

 <target name="tools-parallel" description="Run tools in parallel">
  <parallel threadCount="2">
   <sequential>
    <antcall target="pdepend"/>
    <antcall target="phpmd-ci"/>
   </sequential>
   <antcall target="phpcpd"/>
   <antcall target="phpcs-ci"/>
   <antcall target="phploc"/>
   <antcall target="phpcb"/>
  </parallel>
 </target>

 <target name="clean" description="Cleanup build artifacts">
  <delete dir="${basedir}/build/api"/>
  <delete dir="${basedir}/build/code-browser"/>
  <delete dir="${basedir}/build/coverage"/>
  <delete dir="${basedir}/build/logs"/>
  <delete dir="${basedir}/build/pdepend"/>
 </target>

 <target name="prepare" depends="clean" description="Prepare for build">
  <mkdir dir="${basedir}/build/api"/>
  <mkdir dir="${basedir}/build/code-browser"/>
  <mkdir dir="${basedir}/build/coverage"/>
  <mkdir dir="${basedir}/build/logs"/>
  <mkdir dir="${basedir}/build/pdepend"/>
  <mkdir dir="${basedir}/build/phpdox"/>
 </target>

 <target name="lint" description="Perform syntax check of sourcecode files">
  <apply executable="php" failonerror="true">
   <arg value="-l" />

   <fileset dir="${basedir}/app">
    <include name="**/*.php" />
    <include name="**/*.ctp" />
    <modified />
   </fileset>
  </apply>
 </target>

 <target name="phploc" description="Measure project size using PHPLOC">
  <exec executable="phploc">
   <arg value="--log-csv" />
   <arg value="${basedir}/build/logs/phploc.csv" />
   <arg value="--exclude"/>
   <arg value="Test" />
   <arg value="--exclude"/>
   <arg value="Config/Migration" />
   <arg value="--exclude"/>
   <arg value="Plugin" />
   <arg path="${basedir}/app" />
  </exec>
 </target>

 <target name="pdepend" description="Calculate software metrics using PHP_Depend">
  <exec executable="pdepend">
   <arg value="--jdepend-xml=${basedir}/build/logs/jdepend.xml" />
   <arg value="--jdepend-chart=${basedir}/build/pdepend/dependencies.svg" />
   <arg value="--overview-pyramid=${basedir}/build/pdepend/overview-pyramid.svg" />
   <arg value="--exclude=${basedir}/app/Vendor,${basedir}/app/webroot" />
   <arg path="${basedir}/app" />
  </exec>
 </target>

 <target name="phpmd"
         description="Perform project mess detection using PHPMD and print human readable output. Intended for usage on the command line before committing.">
  <exec executable="phpmd">
   <arg path="${basedir}/app" />
   <arg value="text" />
   <arg value="${basedir}/build/phpmd.xml" />
  </exec>
 </target>

 <target name="phpmd-ci" description="Perform project mess detection using PHPMD creating a log file for the continuous integration server">
  <exec executable="phpmd">
   <arg path="${basedir}/app" />
   <arg value="xml" />
   <arg value="codesize,unusedcode,design,${basedir}/build/phpmd-naming.xml" />
   <arg value="--reportfile" />
   <arg value="${basedir}/build/logs/pmd.xml" />
   <arg value="--exclude" />
   <arg value="Test,Config,Vendor,Plugin" />
  </exec>
 </target>

 <target name="phpcs"
         description="Find coding standard violations using PHP_CodeSniffer and print human readable output. Intended for usage on the command line before committing.">
  <exec executable="phpcs">
   <arg value="--standard=${basedir}/build/phpcs.xml" />
   <arg path="${basedir}/app" />
  </exec>
 </target>

 <target name="phpcs-ci" description="Find coding standard violations using PHP_CodeSniffer creating a log file for the continuous integration server">
  <exec executable="phpcs">
   <arg value="--report=checkstyle" />
   <arg value="--report-file=${basedir}/build/logs/checkstyle.xml" />
   <arg value="--standard=${basedir}/build/phpcs.xml" />
   <arg value="--extensions=php"/>
   <arg value="-p"/>
   <arg path="${basedir}/app" />
  </exec>
 </target>

 <target name="phpcpd" description="Find duplicate code using PHPCPD">
  <exec executable="phpcpd">
   <arg value="--log-pmd" />
   <arg value="${basedir}/build/logs/pmd-cpd.xml" />
   <arg value="--exclude"/>
   <arg value="Test" />
   <arg value="--exclude"/>
   <arg value="Config/Migration" />
   <arg value="--exclude"/>
   <arg value="Plugin/" />
   <arg path="${basedir}/app" />
  </exec>
 </target>

 <target name="phpdox" description="Generate API documentation using phpDox">
  <exec executable="phpdox"/>
 </target>

 <target name="make-tmp-folders">
  <mkdir dir="${basedir}/app/tmp/"/>
  <mkdir dir="${basedir}/app/tmp/cache"/>
  <mkdir dir="${basedir}/app/tmp/cache/persistent"/>
  <mkdir dir="${basedir}/app/tmp/cache/models"/>
  <mkdir dir="${basedir}/app/tmp/cache/views"/>
  <mkdir dir="${basedir}/app/tmp/logs"/>
  <mkdir dir="${basedir}/app/tmp/tests"/>
  <mkdir dir="${basedir}/app/tmp/sessions"/>
 </target>

 <target name="migration">
  <exec executable="bash" failonerror="true" dir="${basedir}/app">
   <arg value="Console/cake"/>
   <arg value="Migrations.migration"/>
   <arg value="run"/>
   <arg value="all"/>
  </exec>
 </target>

 <target name="phpunit" depends="make-tmp-folders,migration" description="Run unit tests with PHPUnit">
  <exec executable="bash" failonerror="true" dir="${basedir}/app">
   <arg value="Console/cake"/>
   <arg value="test"/>
   <arg value="app"/>
   <arg value="AllTests"/>
   <arg value="--configuration"/>
   <arg path="${basedir}/build/phpunit.xml"/>
  </exec>
 </target>

 <target name="phpcb" description="Aggregate tool output with PHP_CodeBrowser">
  <exec executable="phpcb">
   <arg value="--log" />
   <arg path="${basedir}/build/logs" />
   <arg value="--source" />
   <arg path="${basedir}/app" />
   <arg value="--output" />
   <arg path="${basedir}/build/code-browser" />
   <arg value="--exclude=*/Config/*,*/Test/*,*/Vendor/*,*/View/*" />
  </exec>
 </target>
</project>

この設定を使用すると以下の処理が実行されます。

  1. lint - 文法チェック
  2. phploc(*) - プロジェクトの規模を測定
  3. pdpend (*) - パッケージ単位のメトリクスの測定(依存度とか)
  4. phpcb (*) - ソースコード毎の状態を解析
  5. phpmd - 静的解析(実装上の問題を検出)
  6. phpcs - コーディング規約チェック
  7. phpcpd - コピペコードの検出
  8. migration - DBのマイグレーション(cakedc/Migrations plugin)
  9. UnitTest - 単体テスト(CakePHP TestCase)

ツール毎に処理対象を除外する記述が異なってて以外と面倒なので、参考になれば嬉しいです。

おまけ

今進行中ののプロジェクトでは、以下の様なターゲットを作って、BDD Pluginでのストーリテストの実行やCapistranoでの自動デプロイもしてます。

<?xml version="1.0" encoding="UTF-8"?>

 <target name="migration-bdd">
  <exec executable="bash" failonerror="true" dir="${basedir}/app">
   <env key="APP_ENV" value="bdd" />
   <arg value="Console/cake"/>
   <arg value="Migrations.migration"/>
   <arg value="run"/>
   <arg value="all"/>
  </exec>
 </target>

 <target name="bdd-story" depends="make-tmp-folders,migration-bdd" description="Run Bdd Story tests">
  <exec executable="bash" failonerror="true" dir="${basedir}/app">
   <env key="APP_ENV" value="bdd" />
   <arg value="Console/cake"/>
   <arg value="Bdd.story"/>
   <arg value="--format=junit"/>
   <arg value="--out"/>
   <arg path="${basedir}/build/behat/"/>
  </exec>
 </target>

 <target name="deploy-staging" description="Deploy Appliction to Staging">
  <exec executable="cap" failonerror="true" dir="${basedir}">
   <arg value="staging"/>
   <arg value="deploy"/>
  </exec>
 </target>

ACL PluginでACL再入門

CakePHP Advent Calendar 2012 18日目の記事です。
昨日は、@msngさんのCakePHP の Configure クラスで最も悩ましい点を解決する方法 でした。確かにConfigureはちと長いですよね(^^;。

当初、BDD Pluginについて何か書こうと思っていたのですが、なかなか時間が取れず記事に出来るほどネタがたまっていないので、ストックしてあったネタからACL Pluginについて書きたいと思います。ACLに関しては、@ootatterさんの7日目の記事、ACL.phpとACL.iniについてでも取り上げられていますのであわせて読むと良いかと思います。

ACLは難しい

ACL難しい」という話は結構良く聞きますし、実際僕もそう思っていました。今まではあまり細かな権限管理を要求される事も無かった事もあって、ACLは使わずに簡易的な権限管理機能を自作していました。先日たまたまACL Plugin 2.2.0がリリースされているのを発見したので、今後の為にと試用してみました。実際試してみると、「難しい」というより「少し面倒」といった程度な感じでした。一度使い始めると本当に手放せない機能だと実感してます。ただ、実際に使うには結構準備が大変なので、簡単にACL Pluginを手元の環境で体験できるサンプルを作りました。

GitHub - ACL Plugin Sample

以下で実際の設置方法や注意点等を紹介します。

ACL Plugin sampleの設定

1. サンプルプロジェクトの配置
設置したいディレクトリで以下の手順で、サンプルプロジェクトを配置します。

$ git clone https://github.com/kaz29/acl_plugin_sample.git
$ cd acl_plugin_sample
$ chmod -R go+w ./app/tmp
$ cp ./app/Config/database.php.default ./app/Config/database.php
$ vim ./app/Config/database.php => データベースの設定

2. サンプル用テーブルの作成
以下の様な手順で、サンプルで使用するテーブルを作成します。私はPostgreSQL使いなので以下はPostgreSQLの例ですが、MySQL用のファイルも"schema_mysql.sql" という名前で用意してありますので、適宜読み替えてください。

$ psql データベース名 < ./app/Config/Schema/schema_postgres.sql

今回使用するテーブルは以下の2つです。

テーブル名 モデル名 説明
users User ユーザー情報テーブル(belongsTo:Group)
groups Group 権限情報テーブル(hasMany:User)

3. ACL用テーブルの作成
ACLで利用するテーブルはSchemaファイルが用意されているので以下の様に作成します。

$ ./app/Console/cake schema create DbAcl

ACL Plugin sampleを試す

設定が終わったら、http://localhost/admin/にアクセスしてみましょう。

1. ユーザーの作成

ユーザーが存在しない状態でアクセスをすると、ユーザー作成画面に遷移するのでユーザーを作成します。この時点でグループが存在しない場合、「サイト管理者」と「一般ユーザー」の2つのグループが自動的に作成されます。

2. ログイン

ユーザー作成が完了すると、ログインページに遷移するので先ほど作成したアカウントでログインします。

3. ACO(Access Control Object)の初期化(*1)

ログイン後、acosテーブルにデータが存在しない場合、ACL Pluginのaco同期処理(/admin/acl/acos/synchronize)に遷移します。現在定義されているコントローラ/アクションとacosテーブルの内容をチェックし、存在しない項目がリストアップされます。

画面の下の方にある"Synchronize"をクリックし、acoを作成します。

4. アクセス権限(ARO)の設定

次に、アクセス権限の設定をします。画面上の"Permissions" => "Roles permissions"をクリックしグループ毎の権限設定画面を表示します。

初期状態ではアクセス権限が設定されていないので、サイト管理者の横にある緑のチェックマークをクリックし、サイト管理者にすべての機能へのアクセス権を与えます。

設定が完了すると以下の様に、すべてのアクションへのアクセス権限が付与された事が分かります。

ACLが正常に機能しているかを確認する為に、Groups->admin_indexへのアクセス権を削除してみましょう。下記の画像の赤丸の部分をクリックします。

処理が完了すると、以下の様に表示が切り替わります。

5. 権限が無い機能にアクセスしてみる

この状態で、http://localhost/admin/groups/ にアクセスすると権限が無いため以下の様にエラーが表示されます。

ACL Pluginを使うととても簡単に権限の管理が可能です。グループ単位の設定の他、ユーザー毎にアクセスを許可したり、拒否したりといった事も簡単に出来ますので色々と試してみてください。

実際のカスタマイズ内容はGitHubのCommitログを見ていただければ参考になるかと思います。幾つかコメントも書いておいたので、気づいた点等あれば適宜追記していただければと思います。

ACL Plugin を使う上でのちょっとした注意点

adminルーティングの利用が必須

ACL Pluginはadminルーティングを利用する事を前提に実装されています。URL構造を検討する際には考慮しておきましょう。

Group,User共にモデル経由で作成する必要がある

ACLに関連するデータは、ACLビヘイビアが自動的に生成等の処理を行います。Group,Userのテーブルデータを作成しただけではエラーが発生して正常に動作しません。特に初期状態を作成する際には注意が必要かもしれません。

(*1)user_id:1は特権ユーザー

上の「ACL Plugin sampleを試す」の「ACO(Access Control Object)の初期化」で、まだアクセス権限が設定されていないのに、Acl関連のコントローラーにアクセスできるのを不思議に思った方もいるのではないでしょうか?ACL Pluginではデフォルトでuser_id=1のユーザーが特権ユーザーとして設定されています。下記の'acl.role.access_plugin_user_ids'に設定されているユーザーIDは特権ユーザーとして扱われ、ACLの設定がされていなくてもAcl関連のコントローラにアクセス出来ます。また、'acl.role.access_plugin_role_ids'にグループIDを設定すると設定したグループに所属するユーザーに特権が与えられます。

Acl/Config/bootstrap.php 47行目〜
<?php
...
/*
 * You can add here role id(s) that are always allowed to access the ACL plugin (by bypassing the ACL check)
 * (This may prevent a user from being rejected from the ACL plugin after a ACL permission update)
 */
Configure :: write('acl.role.access_plugin_role_ids', array());

/*
 * You can add here users id(s) that are always allowed to access the ACL plugin (by bypassing the ACL check)
 * (This may prevent a user from being rejected from the ACL plugin after a ACL permission update)
 */
Configure :: write('acl.role.access_plugin_user_ids', array(1));

まとめ

如何でしたでしょうか?ACL Pluginを使う事でかなり簡単にACLを利用出来る事がお分かりいただけたかと思います。是非一度試してみてください。

明日は、@konsanの「Chosen + Search + Collectionableの3つのプラグインの組み合わせ」です。楽しみ!

おまけ

ACL Plugin 2.2.0 の potファイルには漏れがある

ACL Plugin 2.2.0に付属のacl.potには一部の文言が含まれていないため、日本語化したい場合は注意が必要です。i18nコマンドを使って再抽出するか、手動で追加する必要があります。

Authコンポーネントのflashメッセージをカスタマイズ

これは私が実際に困って調べたのですが、TwitterBootstrapPlugin等を使用してデザインをカスタマイズしている場合Authプラグイン内で生成されるflashメッセージが表示されないという問題がありました。こんな場合には以下の様にAuthコンポーネントに設定をする事でカスタマイズ出来ました。

<?php
class AppController extends Controller {
	public $components = array(
            'Acl',
            'Auth' => array(
                .....
                'flash' => array(
                    'element' => 'alert',
                    'params' => array(
                        'plugin' => 'TwitterBootstrap',
                        'class' => 'alert-error'
                    ),
                    'key' => 'flash',
                ),
            ),
....
        );
....
}

今時なCakePHPでの開発環境!?

前職を退職してもう半年以上経ってしまいました。おかげさまで、レガシーなコードとのバトル等色々お仕事しています。

さて、先日のPHP Matsuri2012での@ryuzeeさんの発表を聞いて、開発環境や手法を改善するべく、今月から始まったプロジェクト用の環境を構築しています。

CakePHPは最新で!

CakePHPの最新stable版は 2.2.3 ですが、既に 2.3.0-beta が公開されています。幾つか2.3.0-betaの機能で使いたかったものがあったのと、今回のプロジェクトのリリース時期が来年の5月という事もあって、2.3.0-beta を使って開発をしています。来年の5月なら2.3.0Stable版がリリースされるんじゃないかとの読みです(^^;さて、どうなるでしょうかw?

出来る限り公開されているPluginを使う

前職の職場では、ある程度蓄積された自前のPluginがあった関係で公開されているPluginはあまり使っていませんでした。当然、前職の職場で開発した資産を今回使う事は出来ないので、既に公開されているものは積極的に利用しています。どれも定番ですが、今のところ使う予定のものは以下の通り。

これらのプラグインを上手く使って開発を進めていく予定です。幾つか初めて使ったものもあるのですがもう手放せません(^^。

既存のサービスを使う!

今まではソースコードは自前のgitリポジトリ(gitolite)、issue管理はcandycane(お客さんのredmineやbacklogって事も...)で管理はしていましたが、今回は以下の様なツールを使う事にしました。

今回のプロジェクトは、クライアントさんは別にして開発者全員がバラバラの場所で作業するのでオンライン会議はかなり重要です。他にも色々あるとは思いますが、最近はco-meeting+Facebookってパターンがほとんどです。
また、今回はお客さんにGithubとPivotal Trackerを契約していただけたのでありがたく使わせていただきます(^^。色々試していますが、今後は自前のgitリポジトリはバックアップにして、この構成で進める事が多くなりそうです。

本番環境のサーバーはまだ選定中ですが、NIFTY Cloudになる予定。

簡単デプロイ

今までは簡単なシェルスクリプトでデプロイをする事が多かったのですが、CakePHP2実践入門でデプロイの章を書いた事もあり、Capistrano+capcakeでデプロイする環境を作りました。詳しい事は以下に書いてありますので、まだの方は是非ご購入を検討してみてください(^^!

CakePHP2 実践入門 (WEB+DB PRESS plus)

CakePHP2 実践入門 (WEB+DB PRESS plus)

Jenkinsさんに協力してもらう

さていよいよ本題(^^;。いままではがっつりアジャイル開発をした経験が実はなく、今回アジャイル開発に挑戦という事で久々にJenkinsを使ってCI環境を作りました。Unitテストを実行してカバレッジをとりつつ以下のプラグインを使って色々とソースコード解析もしています。

また、GithubのService HookとJenkinsのJob Notificationsを使って以下の様な内容を自動実行しています。

  • commit時にJenkinsのビルドを自動実行
  • commit時にPivotal Trackerに自動連携 : 自動的にストーリーをFinishしたり、Activityを追加したり。
  • commit/Jenkinsのビルド失敗時にFacebookグループに自動投稿 : サンドボックスモードな自前アプリ(^^;
  • 毎朝作業前の時間にビルドを自動実行

これらが実際に動き始めたのは昨日なのですが、なかなかいい感じです。まだ本格的な開発はこれからなので、頑張って育てていくつもりです。今後は、受け入れテストの自動化や自動的にデプロイもせたいですね。

まとめ

こんな感じで、作業環境としてはかなり整ってきた感じですが、実際の開発は12月からなのでこれからが僕らの真価の発揮どころです。ツールに振り回されずに、地道に作業進めていこうと思っています。

CakePHP2実践入門がでました

f:id:kaz_29:20121001100131j:plain

9/29にわたしも共著で執筆に参加した「CakePHP2 実践入門」が発売されました。
安藤さんを筆頭に錚々たるメンバーです。CakePHPを使っている方であれば、ブログや書籍などでほぼ100%お世話になったことがあるのではないかと思います。

執筆環境

今回の執筆環境を書こうと思って書き始めたところで、新原さんがすばらしいまとめをしてくれたので、そちらをご覧ください(^^;。

私は新原さんとは違うツールを使っていたのでそのツールを紹介します。

Mou

f:id:kaz_29:20121001095125p:plain

私はエディタ兼Markdownのプレビューの為に Mou を使いました。リリース当初はちょいちょいハングしたりしていたのですが、バージョンアップを重ねかなり動作も安定してきています。

f:id:kaz_29:20121001095145p:plain

左側が編集画面、右側がプレビュー画面になっていて、編集画面で編集するとプレビューも連動して更新されます。

テキストで原稿を書きながら、プレビューが出来るので、昨年別の書籍を執筆した時よりも全体をつかみやすくとても良かったと思います。

執筆のきっかけ

多分、安藤さんから「CakePHP2本の企画を進めるんだけど、執筆に興味ある人挙手!」というメールが来たのは去年の12月頃の話。

今年一月に技術評論社さんで編集者さんを交えて顔合わせをして以降の慣れない執筆作業は、締め切りが迫って来てドキドキしたり、校正のスケジュールと仕事の締め切りがかち合って寝られなかったり、執筆中に次々にアップデートが公開されて青くなったりとなかなかスリリングでした(^^;。

慣れない作業はなかなかに大変でしたが、執筆作業が(ほぼ)終わってAmazonに載ったり、実際に本が刷り上がって手にとった時はとても胸が熱くなりました(^^。

まとめ

今回の書籍の内容ですが、特に後半部分に関しては執筆に関わった私自身も色々な気づきをもらえたので、かなり濃い内容なのではないかと思います。初学者の方やCakePHP2系へのノウハウの移行をしたいユーザーだけでなく、「CakePHPは結構使いこなせているよ!」という方にもお勧めできる一冊になっていると思います。是非手に取って頂ければうれしいです。

CakePHP2 実践入門 (WEB+DB PRESS plus)

CakePHP2 実践入門 (WEB+DB PRESS plus)

最後に執筆陣がこの書籍について書いたブログ記事をまとめておきます。

とりは市川さんですね!期待してます(^^。

UITableViewの行削除でセクション数が減るとクラッシュする

久々のiPhoneアプリ開発ではまったのでメモ。

行を削除でセクションがなくなるような場合、deleteRowsAtIndexPathsで行を削除しただけでは以下のようなエラーでクラッシュします。


f:id:kaz_29:20120831071434p:plain

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the table view after the update (1) must be equal to the number of sections contained in the table view before the update (2), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).'

行の削除によりセクションにデータがなくなるような場合は以下の様にセクションも削除する必要がある様です。

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle 
                                            forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        [tableView beginUpdates];

        // self.tableData = NSMutableArray
        [[self.tableData getObject:[indexPath section]] removeObjectAtIndex:[indexPath row]];
        if ([[self.tableData getObject:[indexPath section]] count] == 0) {
            [self.tableData removeObjectAtIndex:[indexPath section]];
        }

        if ([self.tableData count] != [self.tableView numberOfSections]) {
            [self.tableData removeObjectAtIndex:[indexPath section]];
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:[indexPath section]] withRowAnimation:YES];
        }
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];

        [tableView endUpdates];
    }
}
  • 2012/8/31 10:20 サンプルコードに問題があったので微修正。

CakePHP2+PostgreSQLでGeoデータを簡単に扱う!?

@tkengoさんから引き続き CakePHP Advent Calendar 14日目の記事です。

深く考えず参加申し込みをしたらダブルヘッダーになってしまいヒーヒー言っているわたなべです(^^;

この記事はCakePHP2系の記事です。


明日にはこんなイベントも開催されるようで、最近は位置情報を扱う機会も多いのではないかと思います。

先月くらいに位置情報に関して色々試していて、CakePHPから扱う場合にいつもと同じように扱いたいなぁということでPostgres Datasourceを拡張してみたので解説したいと思います。

PostgreSQLで扱える位置情報の種類

PostgreSQLで扱える位置情報関連のデータ型には以下のようなものがあります。

型名 表現
point 平面における座標点
line 無限の直線
lseg 有限の線分
box 矩形
path 閉経路
path 開経路
polygon 多角形
circle

幾何データ型

今回は携帯での位置情報を簡単に扱えるようにという目標なのでこの中からデータ型としては 'point'型と'box'型のサポートをしました。

位置情報関連の演算子

下記のマニュアルページを見ていただければ分かる通り、たくさんの演算子がサポートされています。

幾何データ演算子

今回はこの中から以下の演算子をサポートを追加しました。

演算子 説明
@ 含む、または境界上
~= 同等か?
<-> 距離

スキーマ定義

今回の追加機能のテスト用に作成したFixtureは下記になります。

<?php
class PostgresGeoFixture extends CakeTestFixture {

/**
 * name property
 *
 * @var string 'PostgresGeo'
 */
	public $name = 'PostgresGeo';

/**
 * fields property
 *
 * @var array
 */
	public $fields = array(
		'id' => array('type' => 'integer', 'key' => 'primary'),
		'loc' => array('type' => 'point', 'null' => false),
		'rect' => array('type' => 'box', 'null' => false),
	);
}

point型の locとbox型の rect フィールドを作成しています。

使い方

データを保存する場合は、以下の様に座標データを配列形式で設定すればOKです。
box型の場合は左上の座標と、右下の座標を配列で渡します。

<?php

$data = array(
  'loc'	 => array(10.1,10.1),
  'rect' => array(array(10.1,10.2), array(100.1,100.2)),
);

$Model->create();
$Model->set($data);
$result = $Model->save();

特定の円の中に含まれているデータを検索する場合はこんな感じです。

<?php
// @ operator
// compare to circle
$conditions = array(
  'loc @' => array('circle' => array('point'=> array(10.1,10.1), 100)),
);
// order by distance
$order = array(
  'loc <-> point(10.1,10.1)'
);

$result = $model->find('all', array('conditions'=>$conditions, 'order'=>$order));

この例では中心座標 10.1,10.1、半径100の範囲内に含まれるデータが取得できます。また、<->演算子で並べ替えることで、中心座標に近い順に並べ替えることが可能です。

Postgresデータソースのテストを書いてあるのでそちらも見ていただけると、もう少しイメージが湧くかと思います。

誤算

当初この修正を始めたときは、PostgreSQLデータソース内部ですべて解決できそうな気がした(^^; ので、CakePHP2をforkして作業を始めたのですが、実際に作業を進めて見た結果、DboSource(データベース関連データソースの共通クラス)にも手を入れないと実現ができませんでした。

さすがにDatasource固有の演算子サポートのためにDboSourceに手を入れる修正はコアに取り込んでもらえないなぁと思い、今後この機能をプラグイン化できないか調査をしているところです。

実際に動くコードは、GitHubに上がっているのでもし興味がある方は試してみてください。結構便利だと思いますよ。

明日は @scriptwork さんです。よろしくお願いします!

zinniaでお手軽手書き文字認識

@haoyayoi さんから引き続き iOS Advent Calendar 14日目の記事です。

アプリ自体はまだ2本しか出していないわたなべです。
iOS Advent Calendar はそうそうたるメンバーがとても参考になる記事を書かれているのでいろいろ勉強させてもらってます(><)!
こんな私ですが、@k_katsumiさんに背中を押されたので、最近試している手書き文字認識エンジン zinniaについて書いてみます

zinnia ってなに?

zinnia は Taku Kudoさんが作成された「手書き文字認識エンジン」です。このエンジンに文字のストローク座標の連続データを渡すことで、高速に文字を認識することができます。確からしさ順にN個の結果を取得することができるので、変換候補を表示したりも出来ます。

iPhone上で動かすには...

@FLCLjp さんが作成されたサンプルが Githubで公開されているのでそれを見るのが一番だと思います。
実際に動かしてみると以下のようにしっかり認識できるはずです。


f:id:kaz_29:20111214124235p:image:w360

で、これだけだと全く中身がない話になってしまうので(^^; もう少し書きますよ〜。

認識率は...

zinniaのサイト上で配布されれいる 認識用モデルファイル(Zinnia-Tomoe) を使って試用した感じではかなり認識率は高いと思います。ただ画数の少ない文字はどうしても正しく認識してくれないケースが多かったです。まぁ、仕方が無いですね。

ですが、特定の文字種に限って(たとえば数字だけとか...)認識できればいいのであれば、自前で認識用モデルファイルを作ってしまえばかなり認識率を高めることはできそうです。

実際、私を含め身近な数人に数字のストロークを入力してもらい作成した認識用モデルファイルを使うと、ほぼ100%に近い確率で認識できました!

認識用モデルファイル

認識用モデルファイルはS式で記述された学習データを元に、zinnia付属のコマンドで作成します。公式ページにも記述がありますが実際の形式は以下のような形式です。

S式フォーマット

(character
 (value 認識したい文字)
 (width キャンバス幅)
 (height キャンバス高さ)
 (strokes
   ((0画目x 0画目y) ... (0画目x 0画目y))
   ((1画目x 0画目y) ... (1画目x 1画目y))
   ((2画目x 2画目y) ... (2画目x 2画目y))
   ...))


フォーマット自体はとてもシンプルなのですが、このデータを作るには実際に認識させるデバイス上で文字のストローク情報を集める必要があります。おそらくzinniaを使っている方はみんな何らかの文字収集ツールを書いてるんじゃないかなぁと思います。


ということで、自分用にiPhoneで動作するアプリを書いたので解説します。

手書きストローク収集ツール(TegakiStroke)

iPhone-Zinnia-TegakiStroke@GitHub


もともと自分用に収集アプリを書いていたのですが、不特定多数の人のストロークを集めたかったので、データをサーバーに上げる方式で作っていました。これだとお手軽に試してもらうわけにはいかないので、すべてのデータをローカルなsqlitedbに保存する形で作り直しました。テーブル構成は以下のようなとても簡単な作りになっているので、、付属のtemplate.dbを覗いてもらえればわかるかと思います。

モデル情報(models) => モデルに含まれる文字情報(characters) => 実際のストローク情報(strokes)


以下がschema構築用のsqlファイルです。

CREATE TABLE models (
id INTEGER PRIMARY KEY,
name TEXT,
created
);

CREATE TABLE characters (
id INTEGER PRIMARY KEY,
model_id INTEGER,
character TEXT,
created
);

CREATE TABLE strokes (
id INTEGER PRIMARY KEY,
character_id INTEGER,
strokes TEXT,
created
);

INSERT INTO models (name,created) values ('number', '2011/12/13');

INSERT INTO characters(model_id,character, created) values(1,'0','2011/12/13');
INSERT INTO characters(model_id,character, created) values(1,'1','2011/12/13');
... 以下略

実際の動作は...

まず入力するモデルを選んで...

f:id:kaz_29:20111214141228p:image:w360

「文字入力」ボタンを押して....

f:id:kaz_29:20111214141229p:image:w360


実際に文字を入力します....

f:id:kaz_29:20111214141230p:image:w360


登録されている文字の入力が終わると、文字一覧に戻ってくるので「S式書出し」ボタンを押せば完了です。

f:id:kaz_29:20111214141229p:image:w360


書きだしたデータは、Documentsフォルダに保存されていますのでiTunes ファイル共有か何かで取り出してください。


f:id:kaz_29:20111214141231p:image:w360


吐き出された、*.sファイルをzinnia付属の zinnia_learn にかければ実際に使えるモデルファイルが出来上がります。
またアプリ内部で使用している sqliteのファイルもDocumentsフォルダに保存してあるので、適宜モデルや文字を追加するなり、自前でstrokeデータを抜き出して加工するなり、いろいろできるのではないかと思います。

まとめ

zinniaを使うと非常に簡単に手書き文字認識を実装できます。実際のアプリで使うには色々と工夫が必要かと思いますが...。

ソースはGitHubに上げてありますので、試してみてください!
若干やっつけ感が漂っているところもありますが(^^; 、時間ができたら機能を追加していこうと思ってます。

明日は @hIDDEN_xv さんです!