December 30, 2013

FuelPHP Advent Calendar 2013 が電子書籍として達人出版会から出版されました。

FuelPHP Advent Calendar 2013 の計25記事が一冊の電子書籍になり、達人出版会から2013年12月27日に出版されました。
http://tatsu-zine.com/books/fuelphpadvent2013

無料ですし、様々なTIPが掲載されていますので、ぜひダウンロードして読んでみてください。TIP集な構成ですので、読みたい箇所から読めます。尚、EPUB版とPDF版があります。

今年はDay 12、Day 18、Day 21と担当させて頂きました。その中では、Day 21で書いた「Rocketeer」の話が、調べていて面白かったです。

昨年もそうでしたが、この速さで出版されるのって凄いですね。kenjiさん、色々な取りまとめ、お疲れ様でした & ありがとうございました。


参考:
電子書籍『FuelPHP Advent Calendar 2013』が達人出版会より出版されました!

December 23, 2013

chefのcomposerリソースを作ってみました。

レポジトリは以下です。
https://github.com/mp-php/chef-resources-composer

GitHubにはcomposerリソースがいくつかあるのですが、BerkshelfでPHPに依存していた関係で、かえって使いづらい事もあったので、試しに自作してみました。

例えばVagrant + chefで使うには、(所定の場所に配置した上で)まずVagrantfileでchef.add_recipeします。
chef.add_recipe "composer"
composerのインストールは、レシピの好きな場所でします。
composer '/usr/local/bin' do
  action :install
end
composer self-update は"action :update"、アンインストールは"action :uninstall"です。

各プロジェクトでの composer install は以下の通りです。
composer_project '[プロジェクトのルート]' do
  action :install
  user 'vagrant'
  group 'vagrant'
end
各プロジェクトでの composer update は"action :update"です。

とりあえずGitのサブモジュールで個人的に使ってます。まあ、リソース作りの練習ということで。(README.mdは気が向いた時に書きます。。。)

chefでnpmパッケージをインストールする時に、インストール済みパッケージをスキップする

chefの小ネタです。(凄く中途半端な終わり方ですが。。。)

自前でnpmパッケージをインストールする時に
%w{xxx yyy}.each do |p|
  execute p do
    command 'npm install -g ' + p
  end
end
としていたんですが、これだと毎回installが実行されてしまうので、レシピの実行時間が長くなってしまっていました。

なんとか解決したいなーと調べてみると https://github.com/balbeko/chef-npm/pull/9/files を見つけたので、真似してみました。
%w{xxx yyy}.each do |p|
  execute p do
    command 'npm install -g ' + p
 not_if "npm -g ls 2> /dev/null | grep '^[├└]─[─┬] #{p}@'"
  end
end
みたいな感じで手動で試してみたらうまくいくんですけど、chefからだとうまくいかないのは何故だ。。。(解決したら更新します。。。)


Rocketeerで1サーバNステージに対応する

先に、Rocketeerの基本的な使い方については、以下を御覧ください。
http://madroom-project.blogspot.jp/2013/12/fac20131221.html
FuelPHPでの使用例ですが、一般的なPHPプロジェクトで、ほぼそのまま使えると思います。

今回の記事は、あまり良くない気もしますが、例えば1つのサーバにstagingとproductionの2つの環境(以下、ステージ)を持つ場合に、Rocketeerでどう対応するか。です。

まず rocketeer/stages.php を以下のような感じで修正します。
// Adding entries to this array will split the remote folder in stages
// Like /var/www/yourapp/staging and /var/www/yourapp/production
'stages' => array('staging', 'production'),

// The default stage to execute tasks on when --stage is not provided
'default' => 'staging',
これで、Rocketeerを今まで通りに実行すれば、"staging"がデフォルトステージとして扱われます。

ディレクトリ構成で言うと
/[root_directory]/[application_name]/releases/

/[root_directory]/[application_name]/staging/releases/
になりました。

Rocketeer実行時に "-S=production"(または"--stage=production") とすると
/[root_directory]/[application_name]/production/releases/
になりました。

また、rocketeer/config.php の "on.stages" で、ステージ毎の設定ができるみたいです。そうなると "on.connections" はコネクション毎の設定なんだと思います。(まだ試していません。)

December 21, 2013

FuelPHPをRocketeerで自動デプロイしてみる。マイグレーションとPHPUnitも実行してみる。

2014/1/25 追記: 先日、Rocketeerのバージョンが1.0.0になりました。当記事の内容は、それよりも古いバージョンで確認しています。当記事記載のサンプルレポジトリは対応済みです。詳しはそちらのコミットログを御覧ください。

--

FuelPHP Advent Calendar 2013 21日目です。@madmamor が担当します。昨日は @Altsencturely さんの「FuelPHPとFluentdの連携」でした。

今日は、PHP製デプロイツール「Rocketeer」を使って、FuelPHPをコマンド一つでデプロイしてみます。デプロイする最中に、PHPUnitやマイグレーションも実行してみます。

Rocketeer公式ドキュメント:
http://rocketeer.autopergamene.eu/
ライセンスファイルへのリンクが切れてしまっていますが、MITライセンスと書かれています。

RocketeerのGitHub:
https://github.com/Anahkiasen/rocketeer

今回の内容は、Mac OSX Mavericks(以下、ローカル)とVagrantで起動しているUbuntu 13.10(以下、リモート)な環境で確認しています。ローカルは普段通りな開発を行う場所で、そこからコマンドを実行して、リモートにデプロイするイメージです。FuelPHPは1.7.1を使いましたが、Composer対応以降のバージョンであれば、あまり関係は無いはずです。

記事内のソースのライセンスについては、Rocketeerが生成するファイルはRocketeerのライセンスに準じます。私が作成したファイルは、ソースにも書きますが、WTFPLライセンスにします。 http://www.wtfpl.net/txt/copying/


1. 下準備(ローカル)

php.iniで以下の設定をします。
phar.readonly = Off
これをしないと、後述のrocketeer.pharが自身の内部を更新する関係か、以下の警告が大量に出ました。
failed to open stream: phar error: write operations disabled by the php.ini setting phar.readonly
併せて、FuelPHPプロジェクトを作成して、Gitレポジトリへコミットしておいて下さい。このGitレポジトリはリモート側からアクセスできる必要があります。アクセスには、ユーザ名とパスワード、あるいはユーザ名と鍵ファイル(と鍵のパスワード)による認証が使えます。

注意: リモートで鍵の設定時 ~/.ssh/config に以下が無いとエラーになる可能性が有ります。あるいは、一度手動でcloneして、ホストの登録を済ませておきましょう。

StrictHostKeyChecking no
尚、この記事を作成するにあたって作成したFuelPHPプロジェクトのサンプルを公開してあります。
https://github.com/mp-php/fuelphp-advent-calendar-2013-rocketeer-sample


2. 下準備(リモート)

git、PHPとmcrypt extension、Composerをインストールしておきます。更に、以下のsymlinkを貼っておきます。このリンク先は、今現在は存在しませんが、それで構いません。
$ sudo ln -s /home/vagrant/www/fuel-rocketeer-sample/current/public /var/www/fuel-rocketeer-sample


3. Rocketeerのインストールと設定ファイルの準備

ローカルで、以下をダウンロードして、プロジェクトルートに配置します。ダウンロード方法は何でも構いません。
http://rocketeer.autopergamene.eu/versions/rocketeer.phar

以下のコマンドでコマンド一覧やヘルプが表示できればRocketeerのインストールは完了です。
$ php rocketeer.phar # コマンド一覧
$ php rocketeer.phar -h # ヘルプ
次に、設定ファイルを準備します。以下のコマンドを実行して下さい。設問は、とりあえず全て未入力でEnterで良いです。
$ php rocketeer.phar ignite
以下のファイルが生成されたはずです。
  • rocketeer/config.php ... 主にリモートの接続情報を設定する
  • rocketeer/hooks.php ... 主にデプロイ時等のbefore/afterのタスクを設定する(今回は使いません。)
  • rocketeer/paths.php ... phpやcomposer等のコマンドのパスを設定する
  • rocketeer/remote.php ... リモートのデプロイ先に関する色々な設定をする
  • rocketeer/scm.php ... Gitレポジトリの設定をする(SVNも使えるみたいです)
  • rocketeer/stages.php ... 同一サーバに複数ステージ(stagingやproduction)がある場合に使う?(今回は使いません。)
注意: rocketeer.pharは自身の内部にキャッシュ的に接続設定を保存するようです。以降の設定が正しく反映されない場合以下のコマンドを実行してみてください。
$ php rocketeer.phar flush
また、その性質上、rocketeer.pharをパブリックなレポジトリにコミットするのはリスクが有るかもしれません。.gitignoreで除外してしまうのも有りかと思います。


4. リモートの接続情報を設定して確認してみる

rocketeer/config.php を修正します。以下は例なので、適切に書き換えて下さい。(以降、同様です。)
'connections' => array(
    'production' => array(
        'host'      => '192.168.33.10',
        'username'  => 'vagrant',
        'password'  => '',
        'key'       => '/Users/mamor/.vagrant.d/insecure_private_key',
        'keyphrase' => '',
    ),
),
SSHのポートを22以外にしている場合は"xxx.yyy.com:2222"のように指定してあげればOKです。

早速、正しく設定できたか確認してみましょう。
$ php rocketeer.phar check

No repository is set for the repository, please provide one :
No username is set for the repository, please provide one :
No password is set for the repository, please provide one :
Checking presence of git
Checking PHP version
Checking presence of Composer
Checking presence of mcrypt extension
Your server is ready to deploy
Execution time: 0.8238s
正しく接続できて、gitコマンド、PHPバージョン、composerコマンド、mcrypt extensionのチェックが行われました。必要なPHPバージョンは、すみません、確認していません。が、Rocketeerのcomposer.jsonには "php": ">=5.3.0" と書かれています。ちなみに手元は5.5です。


5. デプロイの設定をしてデプロイしてみる

rocketeer/remote.php を修正します。
$ git diff rocketeer/remote.php
diff --git a/rocketeer/remote.php b/rocketeer/remote.php
index a21279b..51424c4 100644
--- a/rocketeer/remote.php
+++ b/rocketeer/remote.php
@@ -11,12 +11,12 @@
        ),

        // The root directory where your applications will be deployed
-       'root_directory'   => '/home/www/',
+       'root_directory'   => '/home/vagrant/www/',

        // The name of the application to deploy
        // This will create a folder of the same name in the root directory
        // configured above, so be careful about the characters used
-       'application_name' => '',
+       'application_name' => 'fuel-rocketeer-sample',

        // The number of releases to keep at all times
        'keep_releases'    => 4,
@@ -25,23 +25,24 @@
        // Use this to list folders that need to keep their state, like
        // user uploaded data, file-based databases, etc.
        'shared' => array(
-               '{path.storage}/logs',
-               '{path.storage}/sessions',
+               'fuel/app/cache',
+               'fuel/app/logs',
+               'fuel/app/tmp',
        ),

        'permissions' => array(

                // The permissions to CHMOD folders to
                // Change to null to leave the folders untouched
-               'permissions' => 755,
+               'permissions' => 777,

                // The folders and files to set as web writable
                // You can pass paths in brackets, so {path.public} will return
                // the correct path to the public folder
                'files' => array(
-                       'app/database/production.sqlite',
-                       '{path.storage}',
-                       '{path.public}',
+                       'fuel/app/cache',
+                       'fuel/app/logs',
+                       'fuel/app/tmp',
                ),

                // The web server user and group to CHOWN folders to
"root_directory"の下に"application_name"な名前のディレクトリが作成され、その中にデプロイされます。この例だと "/home/vagrant/www/fuel-rocketeer-sample" になりますね。

"shared"では、デプロイをまたいで共有したいディレクトリやファイルを設定します。大抵の場合、ログディレクトリやキャッシュディレクトリ等、.gitignoreに書かれているものになると思います。裏を返せば、例えばデプロイ毎にキャッシュをクリアしたければ、あえて共有しなければOKです。尚、共有はsymlinkによって実現されます。

注意: ディレクトリを共有する場合、ディレクトリそのものがレポジトリに含まれている必要があります。.gitkeepや、以下のような.gitignoreファイルをそのディレクトリに入れるなどしておいて下さい。
*
!.gitignore
"permissions"は、指定したディレクトリやファイルを、指定したパーミッションに変更します。今回の例では777を指定していますが、適切な値を設定するようにお願いします。

次に rocketeer/scm.php を修正します。
'repository' => 'https://github.com/mp-php/fuelphp-advent-calendar-2013-rocketeer-sample.git',
"repository"に、GitのレポジトリURLを設定します。今回の例はGitHub上のレポジトリなので、usernameとpasswordは空のままで構いません。また、ファイル内のコメントにも書かれているように、既に鍵認証の設定がされている場合も、空で構いません。

以上で基本的な設定が済んだので、お待ちかねのデプロイを実行してみましょう。
$ php rocketeer.phar deploy

No username is set for the repository, please provide one :
No password is set for the repository, please provide one :
Cloning repository in "/home/vagrant/www/fuel-rocketeer-sample/releases/20131220204831"
Initializing submodules if any
Installing Composer dependencies
Setting permissions for /home/vagrant/www/fuel-rocketeer-sample/releases/20131220204831/fuel/app/cache
Setting permissions for /home/vagrant/www/fuel-rocketeer-sample/releases/20131220204831/fuel/app/logs
Setting permissions for /home/vagrant/www/fuel-rocketeer-sample/releases/20131220204831/fuel/app/tmp
Sharing file /home/vagrant/www/fuel-rocketeer-sample/releases/20131220204831/fuel/app/cache
Sharing file /home/vagrant/www/fuel-rocketeer-sample/releases/20131220204831/fuel/app/logs
Sharing file /home/vagrant/www/fuel-rocketeer-sample/releases/20131220204831/fuel/app/tmp
Successfully deployed release 20131220204831
No releases to prune from the server
Execution time: 61.1617s
Gitレポジトリのclone(submodulesがあればそれも)が行われ、composer installが行われ、パーミッション変更が行われ、共有が行われました。

ブラウザから http://[ドメイン]/fuel-rocketeer-sample/ にアクセスして、おなじみのトップ画面が表示されればデプロイ成功です。

ざっとディレクトリ構造を見てみましょう。
$ ll /home/vagrant/www/fuel-rocketeer-sample/
total 20
drwxrwxr-x 4 vagrant vagrant 4096 Dec 20 11:49 ./
drwxrwxr-x 3 vagrant vagrant 4096 Dec 20 11:48 ../
lrwxrwxrwx 1 vagrant vagrant   63 Dec 20 11:49 current -> /home/vagrant/www/fuel-rocketeer-sample/releases/20131220204831/
drwxrwxr-x 3 vagrant vagrant 4096 Dec 20 11:48 releases/
drwxrwxr-x 3 vagrant vagrant 4096 Dec 20 11:49 shared/
"current"ディレクトリが、先ほどデプロイしたディレクトリへsymlinkされています。

$ ll /home/vagrant/www/fuel-rocketeer-sample/current/fuel/app/
total 56
drwxrwxr-x 11 vagrant vagrant 4096 Dec 20 11:49 ./
drwxrwxr-x  6 vagrant vagrant 4096 Dec 20 11:49 ../
-rw-rw-r--  1 vagrant vagrant  718 Dec 20 11:48 bootstrap.php
lrwxrwxrwx  1 vagrant vagrant   61 Dec 20 11:49 cache -> /home/vagrant/www/fuel-rocketeer-sample/shared/fuel/app/cache/
drwxrwxr-x  5 vagrant vagrant 4096 Dec 20 11:48 classes/
drwxrwxr-x  6 vagrant vagrant 4096 Dec 20 11:48 config/
drwxrwxr-x  3 vagrant vagrant 4096 Dec 20 11:48 lang/
lrwxrwxrwx  1 vagrant vagrant   60 Dec 20 11:49 logs -> /home/vagrant/www/fuel-rocketeer-sample/shared/fuel/app/logs/
drwxrwxr-x  2 vagrant vagrant 4096 Dec 20 11:48 migrations/
drwxrwxr-x  2 vagrant vagrant 4096 Dec 20 11:48 modules/
drwxrwxr-x  2 vagrant vagrant 4096 Dec 20 11:48 tasks/
drwxrwxr-x  5 vagrant vagrant 4096 Dec 20 11:48 tests/
lrwxrwxrwx  1 vagrant vagrant   59 Dec 20 11:49 tmp -> /home/vagrant/www/fuel-rocketeer-sample/shared/fuel/app/tmp/
drwxrwxr-x  2 vagrant vagrant 4096 Dec 20 11:48 vendor/
drwxrwxr-x  3 vagrant vagrant 4096 Dec 20 11:48 views/
共有設定したディレクトリが、"shared"ディレクトリ下にsymlinkされています。パーミッションも、設定したとおりに変更されています。

以上が、Rocketeerによる基本的なデプロイ方法です。


6. マイグレーションも実行してみる

だいぶ長くなってきましたが、続いてマイグレーションの実行です。簡単なマイグレーションファイルを作成してコミットしておきます。
$ php oil generate migration create_users name:text email:string password:string
リモート側でDBやDBユーザの作成、それに対するFuelPHPのconfigのdb.phpの設定も済ませておいて下さい。

次に、2ファイルを新規作成します。

まず、rocketeer/tasks/Migrate.php を新規作成します。今回はサンプルなので、名前空間はつけていません。尚、"Rocketeer\Traits\Task" を継承しますが、このクラスはabstract classであってトレイトではないようです。
<?php

/**
 * Migrate class
 *
 * @author    Mamoru Otsuka http://madroom-project.blogspot.jp/
 * @copyright 2013 Mamoru Otsuka
 * @license   WTFPL License http://www.wtfpl.net/txt/copying/
 */
class Migrate extends Rocketeer\Traits\Task
{
    /**
     * @inheritDoc
     */
    protected $description = 'Migrates the database';

    /**
     * @inheritDoc
     */
    public function execute()
    {
        // 実行時にメッセージとして表示されます
        $this->command->info($this->description);

        // currentディレクトリ内でコマンドを実行します
        $output = $this->runForCurrentRelease('php oil r migrate');

        // 第一引数は失敗時のメッセージです
        // 第二引数は失敗時の詳細です
        // 第三引数は成功時のメッセージです
        return $this->checkStatus('Migrate failed', $output, 'Migrate successfully');
    }
}
rocketeer/tasks.php を新規作成します。
<?php
/**
 * rocketeer/tasks.php
 *
 * @author    Mamoru Otsuka http://madroom-project.blogspot.jp/
 * @copyright 2013 Mamoru Otsuka
 * @license   WTFPL License http://www.wtfpl.net/txt/copying/
 */

// Migrateクラスをカスタムタスクとして登録します
// $ php rocketeer.phar migrate で個別に実行できます
require_once __DIR__.'/tasks/Migrate.php';
Rocketeer\Facades\Rocketeer::add('Migrate');

// deploy後に自動実行されます
// 自動実行したくない場合は書かないで下さい
Rocketeer\Facades\Rocketeer::after('deploy', 'Migrate');
Gitレポジトリにコミット(Push)したら、デプロイを実行してみます。
$ php rocketeer.phar deploy
... 略 ...
Migrates the database
Migrate successfully
Removing 1 release from the server
Execution time: 34.8705s
マイグレーションも実行できました。以下のコマンドでマイグレーションのみを個別に実行することもできます。
$ php rocketeer.phar migrate
注意: deployのafterタスクは、既にsymlinkが貼替えられていることに注意して下さい。尚、マイグレーションを行う場合は、別途、何らかの方法でメンテナンスモードに切り替えるタスクを作成する必要があるかと思います。(もちろん、手作業でも良いですが。)また、後述のPHPUnit失敗時の挙動も気になるところで、マイグレーションを自動化するのはそれなりのリスクが伴いそうです。が、今回はとりあえず自動化して進めます。

after(やbefore)タスクにはクラスの他に、インラインによるコマンド設定や、クロージャの設定もできるみたいです。(まだやったことはありません。)クラスにすると "$this->runForCurrentRelease('コマンド')" のように、便利なメソッドでパス周りの調整が簡単になるので、迷ったらクラスで良いのかなと思います。クラスだと、前述のように個別で実行もできますね。


7. PHPUnitも実行してみる

そろそろ最後です。ソースをcloneして、PHPunitを実行して、全てのテストが成功したらデプロイ続行、一つでも失敗したらデプロイ中止(symlinkの貼替えを行わない)できたら良いですね。

composer.jsonに以下を追記します。
$ git diff composer.json
diff --git a/composer.json b/composer.json
index e1b21ea..5ef630e 100644
--- a/composer.json
+++ b/composer.json
@@ -20,6 +20,9 @@
         "monolog/monolog": "1.5.*",
        "fuelphp/upload": "2.0.1"
     },
+    "require-dev": {
+        "phpunit/phpunit": "3.*"
+    },
     "suggest": {
         "mustache/mustache": "Allow Mustache templating with the Parser package",
         "smarty/smarty": "Allow Smarty templating with the Parser package",
プロジェクト直下にphpunit.xmlを用意します。fuel/core/phpunit.xmlをコピーして、パス周りを整えただけです。
<?xml version="1.0" encoding="UTF-8"?>

<phpunit colors="true" stopOnFailure="false" bootstrap="fuel/core/bootstrap_phpunit.php">
    <php>
        <server name="doc_root" value="./"/>
        <server name="app_path" value="fuel/app"/>
        <server name="core_path" value="fuel/core"/>
        <server name="package_path" value="fuel/packages"/>
        <server name="vendor_path" value="fuel/vendor"/>
        <server name="FUEL_ENV" value="test"/>
    </php>
    <testsuites>
        <testsuite name="core">
            <directory suffix=".php">fuel/core/tests</directory>
        </testsuite>
        <testsuite name="packages">
            <directory suffix=".php">fuel/packages/*/tests</directory>
        </testsuite>
        <testsuite name="app">
            <directory suffix=".php">fuel/app/tests</directory>
        </testsuite>
    </testsuites>
</phpunit>
rocketeer/paths.php を修正します。
$ git diff rocketeer/paths.php
diff --git a/rocketeer/paths.php b/rocketeer/paths.php
index f366b41..2b4d6d4 100644
--- a/rocketeer/paths.php
+++ b/rocketeer/paths.php
@@ -19,4 +19,6 @@
        // Path to the Artisan CLI
        'artisan'  => '',

+       // Path to PHPUnit
+       'phpunit' => 'fuel/vendor/bin/phpunit',
 );
Rocketeerは /usr/local/bin 等のグローバルな場所のphpunit、あるいはプロジェクト直下の vendor/bin/phpunit は勝手に見つけてくれます。FuelPHPの場合は fuel/vendor/bin/phpunit になるので、この設定が必要です。(この設定がない場合は、対話式でパスの入力が可能ですが。)

-t オプションをつけてデプロイを実行してみます。
$ php rocketeer.phar deploy -t
... 略 ...
Running tests...
Tests passed successfully
... 略 ...
Execution time: 194.1824s
テストが行われました。単体でも実行できます。
$ php rocketeer.phar test
... 略 ...
Testing the application
Running tests...
... 略 ...
[vagrant@192.168.33.11] (production) Time: 9.23 seconds, Memory: 23.25Mb
[vagrant@192.168.33.11] (production) OK (361 tests, 413 assertions)
Tests passed successfully
Execution time: 9.6588s
注意: テストの実行をafterタスクで行うこともできますが、その時には既にsymlinkが貼替わってしまっています。-tオプションを使うようにしましょう。

最後に、必ず失敗するテストを作成して、どうなるかも確認してみます。(ソースは割愛します。)
$ php rocketeer.phar deploy -t

No username is set for the repository, please provide one :
No password is set for the repository, please provide one :
Cloning repository in "/home/vagrant/www/fuel-rocketeer-sample/releases/20131220215555"
Initializing submodules if any
Installing Composer dependencies
Running tests...
Tests failed
PHPUnit 3.8-g5fb30aa by Sebastian Bergmann.

Configuration read from /home/vagrant/www/fuel-rocketeer-sample/releases/20131220215555/phpunit.xml

The Xdebug extension is not loaded. No code coverage will be generated.

...............................................................  63 / 362 ( 17%)
............................................................... 126 / 362 ( 34%)
............................................................... 189 / 362 ( 52%)
............................................................... 252 / 362 ( 69%)
............................................................... 315 / 362 ( 87%)
..............................................F

Time: 8.6 seconds, Memory: 23.25Mb

There was 1 failure:

1) Test_Example::test_fail

/home/vagrant/www/fuel-rocketeer-sample/releases/20131220215555/fuel/app/tests/example.php:14

FAILURES!
Tests: 362, Assertions: 413, Failures: 1.
Tests failed
Rolling back to release 20131220215158
Migrates the database
Migrate successfully
Execution time: 196.3031s
デプロイが中断されました。symlinkは以前のままです。中断されたデプロイのディレクトリはゴミとして残りますが、rocketeer/remote.phpの"keep_releases"により、そのうち掃除されると思いますので、あまり気にしなくても良いかなと思います。マイグレーションが実行されてしまうのは想定外だったので、この点は今後の課題にします。。。


8. まとめ

(全ての機能を把握できているわけではありませんし、公式ドキュメントに記載されているプラグイン機能も気になるところですが)Rocketeerを使って、FuelPHPをコマンド一つで、PHPUnitやマイグレーションの実行を含めてデプロイできました。

デプロイツールはRuby製のCapistranoが有名ですが、RocketeerはPHP製ということもあり、ComposerやPHPUnitの扱いを標準でサポートしてくれていて助かります。今日現在、Rocketeerの使い方に関する日本語の情報はかなり少ない(というか無いかもしれません。あったらすみません。)ので、今後、盛り上がってくれると良いなーと思います。

chefとvagrantのおかげで、こういったサーバが絡む実験もやりやすくなったので、ぜひ試してみてください。

以上です。お疲れ様でした。



December 18, 2013

FuelPHPとMongoDBとTraceKitでJavaScriptのエラー情報を収集してみる

FuelPHP Advent Calendar 2013 18日目です。@madmamor が担当します。昨日は @suno88 さんの「レンタルサーバー XREA/CORESERVER で FuelPHP を使う [実践編]」でした。

今日は、FuelPHPとMongoDBとTraceKitを使って、JavaScriptのエラー情報を監視、収集する方法を紹介します。

TraceKitはJavaScriptのエラーを簡単に監視できる、MITライセンスなJavaScriptライブラリです。
https://github.com/occ/TraceKit

また、FuelPHPではMongoDBを簡単に扱えるので、それらを組み合わせることで、JavaScriptのエラーを容易に収集できるのでは。と思いつき、試してみました。

記事内のソースは、WTFPLライセンスとします。 http://www.wtfpl.net/txt/copying/

以下、手順です。


1. 下準備

MongoDBとPECLモジュールのインストールを済ませておいて下さい。

MongoDB
http://www.mongodb.org/

PECL :: Package :: mongo
http://pecl.php.net/package/mongo

PHPからMongoDBが使用可能かは、phpinfo()で確認できます。

FuelPHPのインストールも済ませておいて下さい。トップページが見れる状態です。尚、当記事ではv1.7.1で確認しています。


2. TraceKitのインストール

https://github.com/occ/TraceKit のtracekit.jsをダウンロードして、public/assets/jsに置きます。


3. FuelPHPの設定

config/db.php にMongoDB用の設定を追加します。以下、例です。
'mongo' => array(
    'tracekit' => array(
        'hostname'   => 'localhost',
        'port'       => '27017',
        'database'   => 'tracekit',
        'username'   => 'YOUR_USERNAME',
        'password'   => 'YOUR_PASSWORD',
    ),
),


4. コントローラの作成

app/classes/controller/tracekit.php を作成します。
<?php

/**
 * TraceKitが送信するエラー情報をMongoDBにInsertするコントローラ
 *
 * @author    Mamoru Otsuka http://madroom-project.blogspot.jp/
 * @copyright 2013 Mamoru Otsuka
 * @license   WTFPL License http://www.wtfpl.net/txt/copying/
 */
class Controller_Tracekit extends Controller
{

    /**
     * AjaxでPOSTされたエラー情報をMongoDBにInsertする
     */
    public function post_errors()
    {
        if (Input::is_ajax() and Security::check_token())
        {
            $input = Input::post();
            unset($input[Config::get('security.csrf_token_key')]);

            $mongodb = Mongo_Db::instance('tracekit');
            $mongodb->insert('errors', $input);
        }
    }

}


5. viewの修正

app/views/welcome/index.php を修正します。
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>FuelPHPとMongoDBとTraceKitでJavaScriptのエラー情報を収集してみる</title>
    <!-- エラー情報の送信にjQueryを使います -->
    <?php echo Asset::js('http://code.jquery.com/jquery-1.10.2.min.js'); ?>
    <!-- tracekit.jsです -->
    <?php echo Asset::js('tracekit.js'); ?>
    <!-- jQueryでPOSTする際のトークン生成に使います -->
    <?php echo Security::js_fetch_token(); ?>
    <script>
        // TraceKitでエラーを購読します
        TraceKit.report.subscribe(function myLogger(errorReport) {
            // トークンをセットします
            errorReport.<?php echo Config::get('security.csrf_token_key'); ?> = fuel_csrf_token();
            // エラー情報をPOSTします
            $.post('<?php echo Uri::create('tracekit/errors') ?>', errorReport);
        });
        // 意図的にエラーを発生させてみます
        throw new Error('oops');
    </script>
</head>
<body>
    <h1>FuelPHPとMongoDBとTraceKitでJavaScriptのエラー情報を収集してみる</h1>
</body>
</html>


6. 動作の確認

プラウザからトップページにアクセスして、MongoDBを確認します。MongoDBに以下のようなエラーデータが入っていれば正しく動いています。
{
   "_id": ObjectId("52aef1afece0b97316058000"),
   "mode": "onerror",
   "message": "Uncaught Error: oops",
   "url": "http://192.168.33.10/fuel/",
   "stack": [
     {
       "url": "http://192.168.33.10/fuel/",
       "line": "41",
       "func": "myLogger",
       "context": [
         "\t\t\terrorReport.fuel_csrf_token = fuel_csrf_token();",
         "\t\t\t// エラー情報をPOSTします",
         "\t\t\t$.post('http://192.168.33.10/fuel/tracekit/errors', errorReport);",
         "\t\t});",
         "\t\t// 意図的にエラーを発生させてみます",
         "\t\tthrow new Error('oops');",
         "\t</script>",
         "</head>",
         "<body>",
         "\t<h1>FuelPHPとMongoDBとTraceKitでJavaScriptのエラー情報を収集してみる</h1>",
         "</body>" 
      ] 
    } 
  ],
   "useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36" 
}
どんな情報が含まれているか、簡単に確認してみましょう。
  • message ... エラーメッセージです。普段、コンソールに出るヤツです。
  • url ... エラーが発生したURLです。
  • stack.line ... エラーが発生した行。のように見えますが、ズレています。。。
  • stack.func ... JavaScript側の送信関数名です。
  • stack.context ... 発生した行と、前後5行ずつのソースです。
  • useragent ... ユーザエージェントです。
注意: stack.contextにminifyされたJavaScriptが含まれると、相当な量になってしまいます。


7. まとめ

FuelPHPとMongoDBとTraceKitを組み合わせると、JavaScriptのエラーを簡単に保存できました。ブラウザで発生するエラーも、こういった方法で把握して、改善していきたいものです。


19日目は @omoon さんの「FuelPHP 5 分で API を実装するチュートリアル(スクリーンキャストあり)」です。

MongoDBのコマンドメモ

単なるメモです。。。(書き足していくかも)
$ mongo # MongoDBに接続する
$ show dbs # データベース一覧を表示する
$ use [データベース名] # データベースに接続する
$ db.getCollectionNames() # コレクション一覧を表示する
$ db.[コレクション名].find() # コレクションのデータを表示する
$ db.[コレクション名].drop() # コレクションを削除する

December 17, 2013

PHPとIMAPでGmailに下書き保存してみる

まず、PHPでIMAPを使えるようにしておく必要があります。
http://www.php.net/manual/en/book.imap.php

Ubuntuだと、以下でインストールできます。
$ sudo apt-get install php5-imap -y
以下、PHP + IMAPでGmailに下書きを保存するサンプルです。
$mailbox = '{imap.gmail.com:993/imap/ssl}';
$username = 'xxxxxxxxxx@gmail.com';
$password = 'yyyyyyyyyyyyyyyyyyyy';
$connection = imap_open($mailbox, $username, $password) or die(imap_last_error());

$envelope['subject']  = 'Subject';
$body[0]['contents.data'] = 'Contents';
$message = imap_mail_compose($envelope, $body);

imap_append($connection, $mailbox.'[Gmail]/Drafts', $message) or die(imap_last_error());
尚、以下で言語を英語にしておく必要がありました。
https://www.google.com/settings/language

December 14, 2013

chefで非対話にコマンドを実行する小ネタ

例えばUbuntuの"ufw enable"は[y|n]の回答を求めてきます。

その場合、chef(というか自動処理)だと非対話に進める必要があるので
execute 'ufw-enable' do
  command 'printf y | ufw enable'
end
としてやるとうまくいきました。

December 12, 2013

FuelPHPでChatWorkパッケージを使ってみる

FuelPHP Advent Calendar 2013 12日目です。@madmamor が担当します。昨日は @chatii0079 さんの「FuelPHP をもっと Composer で使う」でした。


今日は、FuelPHPのChatWorkパッケージを紹介します。

ChatWorkのAPIは、昨月(2013年11月)末にプレビュー版として公開されました。
http://blog-ja.chatwork.com/2013/11/api-preview.html
そこで早速、FuelPHPのパッケージとして実装してみました。
https://github.com/mp-php/fuel-packages-chatwork

ChatWorkパッケージのライセンスはMITライセンスです。
http://opensource.org/licenses/MIT

では、準備と使い方の説明です。


1. ChatWorkのAPIトークンを発行する

現在、ChatWorkのAPIはプレビュー版なので、利用の申請が必要です。
の"お申し込み方法"の通りに、APIの利用申請をします。後日、利用開始のメールが届きます。

利用開始のメールが届いたら、APIトークンを発行します。
の、"APIトークンの取得"の通りです。発行されたAPIトークンは後で使いますので、控えておいて下さい。


2. FuelPHPとChatWorkパッケージのインストール

FuelPHPのインストールを済ませて、トップページが閲覧可能な状態にします。以下の手順はFuelPHP1.7.1で確認していますが、composer対応以降のバージョンであれば問題無いと思います。

次に、composer.json の"require"に以下を追記します。
"mp-php/fuel-packages-chatwork": "dev-master"

次に、composer update します。
$ php composer.phar update

FuelPHPとChatWorkパッケージのインストールは以上です。尚、ChatWorkのAPIを使用する関係で、curlとOpenSSLを有効にしておいて下さい。


3. ChatWorkパッケージの設定

fuel/packages/chatwork/config/chatwork.php を fuel/app/config/ にコピーして、ChatWorkのAPIトークンを設定します。
<?php

return array(
    'api_token' => 'ここにChatWorkのAPIトークンを設定します。',
);

次に、ChatWorkパッケージを有効にします。方法は二つあります。

各所でChatWorkパッケージを使う場合は
fuel/app/config/config.php の"always_load.packages"に"chatwork"を追記します。
'always_load'  => array(
    ...
    'packages'  => array(
        ...
        'chatwork',
        ...

局所的にChatWorkパッケージを使う場合は
その場所(や、そのクラスのコンストラクタ等)でロードします。
Package::load('chatwork');

これで、全ての準備が完了です。


4. ChatWorkパッケージを使ってみる

自分の情報を取得してみます。
http://developer.chatwork.com/ja/endpoint_me.html#GET-me
$response = Chatwork::get('/me');
Debug::dump($response);

指定したルームに投稿してみます。
http://developer.chatwork.com/ja/endpoint_rooms.html#POST-rooms-room_id-messages
$response = Chatwork::post('/rooms/[ルームID]/messages', array('body' => '内容'));
Debug::dump($response);
ルームIDは、URLに含まれる"rid"に続く数値です。"rid0123456789"であれば、ルームIDは"0123456789"になります。

どちらも簡単ですね。

今回はChatwork::get()メソッドとChatwork::post()メソッドを紹介しましたが、Chatwork::put()メソッドとChatwork::delete()メソッドも用意してあります。


5. その他のAPIを使ってみる

現在、APIは大きく分けると
  • /me
  • /rooms
  • /my
  • /contacts
の4種類で、それぞれ、更に細かくAPIが用意されています。
http://developer.chatwork.com/ja/endpoints.html

どれを使うにしても
  • HTTPメソッドは何か
  • 必須パラメータは何か、任意パラメータにどんなパラメータがあるか
を確認すれば簡単に使えるので、ぜひ試してみてください。


最後に、現在のChatWork APIの認証はトークン式のみなので、自分(トークンの持主)が使う前提になっています。パッと思いつく使い方としては、GitHubのHookから特定のルームにメッセージを投稿する。というような、通知の自動投稿でしょうか。タスクを管理する何か。も面白そうですね。

尚、今後、OAuth 2.0式の認証も提供予定とのことです。


以上、ChatWorkパッケージの紹介でした。

December 8, 2013

Ubuntu13.10で、chefのserviceでsshをリロードできない

chefのserviceでsshをリロードしようとしたら
Recipe: default::default
  * service[ssh] action restart
================================================================================
Error executing action `restart` on resource 'service[ssh]'
================================================================================


Mixlib::ShellOut::ShellCommandFailed
------------------------------------
Expected process to exit with [0], but received '1'
---- Begin output of /etc/init.d/ssh restart ----
STDOUT:
STDERR:
---- End output of /etc/init.d/ssh restart ----
Ran /etc/init.d/ssh restart returned 1
のエラーが出たので、原因はよく読んでいませんが
https://github.com/opscode-cookbooks/openssh/pull/30/files
を参考に
provider Chef::Provider::Service::Upstart
を追加してやったら上手くいくようになりました。

(mongodbでも似たようなエラー出て同じ方法で解決(?)した...)