Laravel 4 + Sentry 2でデフォルトの外部キー名を変更する

前回の続きです。

前回のエントリでは、Sentryで使用するテーブル「users」「groups」「users_groups」「throttle」を、「my_users」「my_groups」「my_users_groups」「my_throttle」に変更しました。しかし、カラム名は変更しなかったので、「my_users_groups」「my_throttle」テーブルから「my_users」「my_groups」テーブルに対して張られる外部キーの名前がデフォルトの「user_id」「group_id」のままになっており、Eloquentの命名規約(モデル名 + id)とずれてしまっていました。そこで今回は外部キーの名前をそれぞれ「my_user_id」「my_group_id」に変更します。

手順

1. Userモデル、Groupモデルの外部キーの命名規約を変更する

「my_users_groups」テーブルから「my_users」「my_groups」テーブルに対して張られる外部キーの命名規約を変更するのは簡単です。SentryのEloquentでは、UserモデルのgroupsメソッドでGroupモデルに対する多対多のリレーションを、GroupモデルのusersメソッドでUserモデルに対する多対多のリレーションをそれぞれ定義しています。これらのメソッドをオーバーライドすることで外部キーの命名規約を変更できます。

app/models/MyUser.php

<?php

class MyUser extends Cartalyst\Sentry\Users\Eloquent\User {

	protected $table = 'my_users';

	public function groups()
	{
		return $this->belongsToMany(static::$groupModel, static::$userGroupsPivot, 'my_user_id', 'my_group_id');
	}

}

belongsToManyメソッドの第3引数、第4引数に関係するキー名を渡します。

app/models/MyGroup.php

<?php
class MyGroup extends Cartalyst\Sentry\Groups\Eloquent\Group {

	protected $table = 'my_groups';

	public function users()
	{
		return $this->belongsToMany(static::$userModel, static::$userGroupsPivot, 'my_group_id', 'my_user_id');
	}

}

こちらも同様です。

2. Throttleモデルの外部キーの命名規約をオーバーライドする

「my_throttle」テーブルから「my_users」テーブルに対して張られる外部キーの命名規約を変更するのは少し面倒です。vendor/cartalyst/sentry/src/Cartalyst/Sentry/Throttling/Eloquent/Provider.phpのfindByUserメソッド内で「user_id」という文字列がハードコーディングされているためです。
これを回避するために、app/start/global.phpなどに下記を追加し、findByUserメソッドをオーバーライドして、「user_id」を「my_user_id」に置換します*1

app/start/global.php

<?php
...

$app = app();
$app['sentry.throttle'] = $app->share(function ($app)
{
	$model = $app['config']['cartalyst/sentry::throttling.model'];
	return new MyThrottleProvider($app['sentry.user'], $model);
});

use Cartalyst\Sentry\Users\UserInterface;

class MyThrottleProvider extends Cartalyst\Sentry\Throttling\Eloquent\Provider {

	public function findByUser(UserInterface $user, $ipAddress = null)
	{
		$model = $this->createModel();
		$query = $model->where('my_user_id', '=', ($userId = $user->getId()));

		if ($ipAddress)
		{
			$query->where(function($query) use ($ipAddress) {
				$query->where('ip_address', '=', $ipAddress);
				$query->orWhere('ip_address', '=', NULL);
			});
		}

		if ( ! $throttle = $query->first())
		{
			$throttle = $this->createModel();
			$throttle->my_user_id = $userId;
			if ($ipAddress) $throttle->ip_address = $ipAddress;
			$throttle->save();
		}

		return $throttle;
	}

}

あとは「my_users_groups」テーブルのときと同じように、EloquentのリレーションをオーバーライドすればOKです。
ThrottleモデルのuserメソッドでUserモデルに対する1対多のリレーションが定義されているので、MyThrottleモデルでuserメソッドをオーバーライドします。

app/models/MyThrottle.php

<?php

class MyThrottle extends Cartalyst\Sentry\Throttling\Eloquent\Throttle {

	protected $table = 'my_throttle';

	public function user()
	{
		return $this->belongsTo('MyUser', 'my_user_id');
	}

}

belongsToメソッドの第2引数に外部キー名を渡します。

3. マイグレーションを変更する

「my_users_groups」「my_throttle」テーブルのマイグレーションをエディタで開き、カラム名を「user_id」「group_id」から「my_user_id」「my_group_id」に変更します。

app/database/migrations/2013_12_19_091459_create_my_users_groups.php

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateMyUsersGroups extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
		Schema::create('my_users_groups', function(Blueprint $table)
		{
			$table->integer('my_user_id')->unsigned();
			$table->integer('my_group_id')->unsigned();

			// We'll need to ensure that MySQL uses the InnoDB engine to
			// support the indexes, other engines aren't affected.
			$table->engine = 'InnoDB';
			$table->primary(array('my_user_id', 'my_group_id'));
		});
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
		Schema::drop('my_users_groups');
	}

}

app/database/migrations/2013_12_19_091441_create_my_throttle.php

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateMyThrottle extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
		Schema::create('my_throttle', function(Blueprint $table)
		{
			$table->increments('id');
			$table->integer('my_user_id')->unsigned();
			$table->string('ip_address')->nullable();
			$table->integer('attempts')->default(0);
			$table->boolean('suspended')->default(0);
			$table->boolean('banned')->default(0);
			$table->timestamp('last_attempt_at')->nullable();
			$table->timestamp('suspended_at')->nullable();
			$table->timestamp('banned_at')->nullable();

			// We'll need to ensure that MySQL uses the InnoDB engine to
			// support the indexes, other engines aren't affected.
			$table->engine = 'InnoDB';
			$table->index('my_user_id');
		});
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
		Schema::drop('my_throttle');
	}

}

このあと上記のマイグレーションを実行すればOKです。

*1:もう少しスマートな方法はないものか。

Laravel 4 + Sentry 2でデフォルトのテーブル名を変更する

LaravelでSentryを使う際に、Sentryが使用するテーブル名を変更する方法についてまとめます。
Sentryはデフォルトでは「users」「groups」「throttle」「users_groups」の4つのテーブルを使用しますが、SentryのEloquentモデルを継承したモデルを作成することで、Sentry本体のソースコードを書き換えることなく使用するテーブル名を変更することができます。

環境

データベースはMySQLを利用しています。Laravel、Sentry、MySQLのバージョンは下記のとおりです。

Laravel 4.0
Sentry 2.0
MySQL 5.5

前提

Sentry 2のインストール手順(https://cartalyst.com/manual/sentry/installation/laravel-4)を実行している前提です。

手順

1. SentryのEloquentモデルを継承したモデルを作成する

app/config/packages/cartalyst/sentry/config.phpを見るとわかるのですが、Sentryはデフォルトで下記のEloquentモデルを使用しています。

  • Cartalyst\Sentry\Users\Eloquent\User
  • Cartalyst\Sentry\Groups\Eloquent\Group
  • Cartalyst\Sentry\Throttling\Eloquent\Throttle

これらのモデルを継承したモデルを作成し、tableプロパティをオーバーライドすることで、Sentryで使用するテーブル名を変更できます。

app/models/MyUser.php

<?php

class MyUser extends Cartalyst\Sentry\Users\Eloquent\User {

	protected $table = 'my_users';

}

app/models/MyGroup.php

<?php

class MyGroup extends Cartalyst\Sentry\Groups\Eloquent\Group {

	protected $table = 'my_groups';

}

app/models/MyThrottle.php

<?php

class MyThrottle extends Cartalyst\Sentry\Throttling\Eloquent\Throttle {

	protected $table = 'my_throttle';

}

上記のモデルでは、「users」「groups」「throttle」テーブルの代わりに「my_users」「my_groups」「my_throttle」テーブルを使うように指定しています。

app/config/packages/cartalyst/sentry/config.phpを編集して、作成したモデルを使うように変更します。また、user_groups_pivot_tableの設定値を変更することで、「users_groups」テーブルの名前も変更できます。

diff --git a/app/config/packages/cartalyst/sentry/config.php b/app/config/packages/cartalyst/sentry/config.php
index 3cd7151..d5e1603 100644
--- a/app/config/packages/cartalyst/sentry/config.php
+++ b/app/config/packages/cartalyst/sentry/config.php
@@ -95,7 +95,7 @@ return array(
                |
                */

-               'model' => 'Cartalyst\Sentry\Groups\Eloquent\Group',
+               'model' => 'MyGroup',

        ),

@@ -120,7 +120,7 @@ return array(
                |
                */

-               'model' => 'Cartalyst\Sentry\Users\Eloquent\User',
+               'model' => 'MyUser',

                /*
                |--------------------------------------------------------------------------
@@ -149,7 +149,7 @@ return array(
        |
        */

-       'user_groups_pivot_table' => 'users_groups',
+       'user_groups_pivot_table' => 'my_users_groups',

        /*
        |--------------------------------------------------------------------------
@@ -186,7 +186,7 @@ return array(
                |
                */

-               'model' => 'Cartalyst\Sentry\Throttling\Eloquent\Throttle',
+               'model' => 'MyThrottle',

                /*
                |--------------------------------------------------------------------------
2. マイグレーションを作成する

このままだとデータベースにテーブルがないので、マイグレーションを作成します。

下記のコマンドを実行します。

$ php artisan migrate:make create_my_users --table=my_users --create
$ php artisan migrate:make create_my_groups --table=my_groups --create
$ php artisan migrate:make create_my_throttle --table=my_throttle --create
$ php artisan migrate:make create_my_users_groups --table=my_users_groups --create

app/database/migrationsディレクトリにマイグレーションが生成されます。生成されたマイグレーションをエディタで開き、upメソッドの中身をvendor/cartalyst/sentry/src/migrationsディレクトリにあるSentryのマイグレーションのupメソッドの中身と置き換えます。

app/database/migrations/2013_12_19_091344_create_my_users.php

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateMyUsers extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
		Schema::create('my_users', function(Blueprint $table)
		{
			$table->increments('id');
			$table->string('email');
			$table->string('password');
			$table->text('permissions')->nullable();
			$table->boolean('activated')->default(0);
			$table->string('activation_code')->nullable();
			$table->timestamp('activated_at')->nullable();
			$table->timestamp('last_login')->nullable();
			$table->string('persist_code')->nullable();
			$table->string('reset_password_code')->nullable();
			$table->string('first_name')->nullable();
			$table->string('last_name')->nullable();
			$table->timestamps();

			// We'll need to ensure that MySQL uses the InnoDB engine to
			// support the indexes, other engines aren't affected.
			$table->engine = 'InnoDB';
			$table->unique('email');
			$table->index('activation_code');
			$table->index('reset_password_code');
		});
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
		Schema::drop('my_users');
	}

}

他の3つのマイグレーションファイルについても同様に編集します。

すでにSentryのマイグレーションを実行している場合にはロールバックします。

$ php artisan migrate:rollback
Rolled back: 2012_12_06_225988_migration_cartalyst_sentry_install_throttle
Rolled back: 2012_12_06_225945_migration_cartalyst_sentry_install_users_groups_pivot
Rolled back: 2012_12_06_225929_migration_cartalyst_sentry_install_groups
Rolled back: 2012_12_06_225921_migration_cartalyst_sentry_install_users

今回作成したマイグレーションを実行します。

$ php artisan migrate
Migrated: 2013_12_19_091344_create_my_users
Migrated: 2013_12_19_091422_create_my_groups
Migrated: 2013_12_19_091441_create_my_throttle
Migrated: 2013_12_19_091459_create_my_users_groups

これでデータベースに「my_users」「my_groups」「my_throttle」「my_users_groups」テーブルが作成されます。

このままだとロールバック時にclass not foundエラーが発生してしまうので、下記コマンドを実行します。

$ composer.phar dump-autoload
Generating autoload files

これでロールバックもできるようになります。

CodeIgniterアプリをデプロイするときのCapistranoレシピ

CodeIgniterアプリをデプロイするときのCapistranoレシピを考えてみた。

deploy.rb

set :application, "my.domain.com"
set :deploy_to, "/home/httpd/#{application}"
set :repository, "http://path_to_myproject"
set :scm, :subversion
set :use_sudo, false

role :web, "myhostname"

after "deploy", "deploy:create_app_symlink"
after "deploy:setup", "deploy:chown_log_dir"

namespace :deploy do
  task :create_app_symlink, :roles => :web do
    run "rm -f #{deploy_to}/application; ln -s #{deploy_to}/current/application #{deploy_to}/application"
    run "rm -f #{deploy_to}/html; ln -s #{deploy_to}/current/html #{deploy_to}/html"
    run "rm -f #{deploy_to}/system; ln -s #{deploy_to}/current/system #{deploy_to}/system"
  end

  task :chown_log_dir, :roles => :web do
    run "sudo chown httpd #{deploy_to}/shared/log"
  end
end

このレシピを使ってcap deploy:setup、cap deployを実行すると、デプロイ先に下記のディレクトリ構成でアプリケーションがデプロイされます。

$ tree -L 3 /home/httpd/my.domain.com/
/home/httpd/my.domain.com/
├── application -> /home/httpd/my.domain.com/current/application
├── current -> /home/httpd/my.domain.com/releases/20130224145516
├── html -> /home/httpd/my.domain.com/current/html
├── releases
│   └── 20130224145516
│       ├── Capfile
│       ├── REVISION
│       ├── application
│       ├── html
│       ├── log -> /home/httpd/my.domain.com/shared/log
│       ├── public
│       ├── system
│       └── tmp
├── shared
│   ├── log
│   ├── pids
│   └── system
└── system -> /home/httpd/my.domain.com/current/system

16 directories, 2 files

以下、補足です。

設定ファイルの置き場所について

CapfileをCodeIgniterのルートディレクトリ、deploy.rbをapplication/config/に置いています。
capifyコマンドの実行手順は下記のとおりです。

まずCodeIgniterのルートディレクトリで、application/を指定してcapifyコマンドを実行します。

$ capify application/
[add] writing 'application/Capfile'
[add] writing 'application/config/deploy.rb'
[done] capified!

実行後、Capfileをルートディレクトリに移動させます。

$ mv application/Capfile .

そして、Capfileを編集して、application/config/deploy.rbを読み込めるように変更します。

$ diff Capfile.org Capfile
4c4
< load 'config/deploy' # remove this line to skip loading any of the default tasks
\ No newline at end of file
---
> load 'application/config/deploy' # remove this line to skip loading any of the default tasks

application/config/に.phpファイルにまぎれて1つだけ.rbファイルがあって違和感がすごいですが、気にしないことにします。

CodeIgniterへのシンボリックリンクについて

create_app_symlinkタスクで、current/application、current/html、current/systemへのシンボリックリンクを作成しています。これはindex.phpをhtml/に配置して、ドキュメントルートとして/home/httpd/my.domain.com/html/を指定しているためです*1
ドキュメントルートに直接/home/httpd/my.domain.com/current/html/を指定すればシンボリックリンクが不要になりますが、Capistranoを使う環境(productionサーバやstagingサーバ)と使わない環境(developサーバ)とでドキュメントルートを揃えておきたかったのでこうしました。

ログディレクトリについて

ログファイルは、CodeIgniterのデフォルトのログディレクトリ(application/logs/)ではなくcurrent/logに出力するようにしています。current/logはshared/log/へのシンボリックリンクで、デプロイ時にCapistranoが作成してくれます。ここにログを出力することで、すべての世代のアプリケーションでログファイルを共有可能になります。

config.phpでは、Capistranoを使わない環境も想定して、下記のようにAPPPATHからの相対パスを指定しています。

$ diff config.php.org config.php
194c194
< $config['log_path'] = '';
---
> $config['log_path'] = APPPATH . '../log/';

なお、上記レシピでは、deploy:setup実行時にshared/log/の所有者をApacheユーザに変更しています。

まとめ

CodeIgniterアプリをデプロイするときのCapistranoレシピを考えてみました。
まだlocalhostで検証しただけなので、実際に運用を始めるといろいろ改善点が出てくると思いますが、とりあえずこのレシピでCapistranoを使ってみようと思います。

*1:index.phpをhtml/に置いている理由は、ドキュメントルートにapplication/やsystem/があるのがセキュリティ上よろしくないためです。http://www.ci-guide.info/basic/install/

CodeIgniterでmemcachedに持続的接続する

CodeIgniterのmemcachedドライバを改造して、memcachedに持続的接続するというお話。

環境

CodeIgniterのバージョンは下記のとおりです。

CodeIgniter 2.1.3
普通にmemcachedドライバを使う

普通にmemcachedドライバを使うと非持続的接続になります。
確認のため、memcachedに接続するだけの簡単なコントローラを書いてみます。

application/config/memcached.php

<?php
$config['memcached'] = array(
    'hostname' => '127.0.0.1',
    'port'     => 11211,
    'weight'   => 1
);

application/controllers/test.php

<?php
class Test extends CI_Controller
{

    public function index()
    {
        $this->load->driver('cache');
        $this->cache->memcached->save('foo', 'bar', 10);
        echo $this->cache->memcached->get('foo');
    }
}

index.php/testに10回リクエストを送信し、netstatでWebサーバからmemcachedへのコネクションを確認してみます。

$ ab -n 10 -c 1 http://$HOSTNAME/index.php/test > /dev/null 2>&1; netstat | awk '{print $5,$6}' | grep 11211
localhost.11211 TIME_WAIT
localhost.11211 TIME_WAIT
localhost.11211 TIME_WAIT
localhost.11211 TIME_WAIT
localhost.11211 TIME_WAIT
localhost.11211 TIME_WAIT
localhost.11211 TIME_WAIT
localhost.11211 TIME_WAIT
localhost.11211 TIME_WAIT
localhost.11211 TIME_WAIT

リクエストのたびにmemcachedに接続しているため、コネクションが10個張られています。
memcachedとの通信はすで終了しているので、コネクションの状態はTIME_WAIT(終了確認待ち)になっています。

何が問題?

TIME_WAIT状態のコネクションは一定時間経つと消えるため、通常はこれで問題ありません。
しかし、アクセス数が非常に多いサイトなどの場合、短時間にアクセスが集中することで、TIME_WAIT状態のコネクションが大量発生してポートを使い切ってしまい、それ以上新しいコネクションを張れない状態になってしまうことがあります。(結果、TIME_WAITが消えてポートに空きが出るまで待つことになり、memcachedのレスポンスタイムが急激に悪化します。)

持続的接続する

そこで、system/libraries/Cache/drivers/Cache_memcached.phpを改造します。

まず、165行目を下記のように変更します。

-$this->_memcached = new Memcached();
+$this->_memcached = new Memcached('memcached_pool');

次に、167〜187行目を下記のようにif文で囲みます。

-foreach ($this->_memcache_conf as $name => $cache_server)
-{
-	...
-}
+if (!count($this->_memcached->getServerList()))
+{
+	foreach ($this->_memcache_conf as $name => $cache_server)
+	{
+		...
+	}
+}

最終形です。

<?php
...
$this->_memcached = new Memcached('memcached_pool');

if (!count($this->_memcached->getServerList()))
{
	foreach ($this->_memcache_conf as $name => $cache_server)
	{
		if ( ! array_key_exists('hostname', $cache_server))
		{
			$cache_server['hostname'] = $this->_default_options['default_host'];
		}

		if ( ! array_key_exists('port', $cache_server))
		{
			$cache_server['port'] = $this->_default_options['default_port'];
		}

		if ( ! array_key_exists('weight', $cache_server))
		{
			$cache_server['weight'] = $this->_default_options['default_weight'];
		}

		$this->_memcached->addServer(
				$cache_server['hostname'], $cache_server['port'], $cache_server['weight']
		);
	}
}
...

これでmemcachedに持続的接続することが可能になり、同じサーバプロセスが受け付けたリクエストであればコネクションを共有できるようになります。
つまり、memcachedへの接続に必要なポート数が高々サーバプロセス数になります。

確認しやすくするため、httpd.confに下記の1行を追記し、Apacheのサーバプロセス数を1に設定します。

ServerLimit 1

同じようにindex.php/testに10回リクエストを送信し、netstatしてみます。

$ ab -n 10 -c 1 http://$HOSTNAME/index.php/test > /dev/null 2>&1; netstat | awk '{print $5,$6}' | grep 11211
localhost.11211 ESTABLISHED

今度はコネクションが1個しか張られないようになっています。
コネクションを張りっぱなしにして再利用するため、コネクションの状態はESTABLISHED(接続確立)のままになっています。

まとめ

CodeIgniterでmemcachedを使った大規模サイトを開発する場合には、memcachedドライバを改造して持続的接続するといいかもしれません、というお話でした。(最初から設定ファイルで接続方法を変更できるようになっているといいんですけどねー。)

Xdebug + VimでPHPのWebアプリをステップ実行してみた

PHPerの強力な味方var_dump。
しかし、あまりにもvar_dumpにばかり頼っていると、「えーマジvar_dump?」「var_dumpデバッグが許されるのは小学生までだよねー」などと罵られかねません。
そこで、今回は脱var_dumpデバッグを目指し、Xdebug + VimPHPのWebアプリをステップ実行してみます。

環境

OS Mac OS X 10.6.8
PHP 5.3.15
Vim 7.3

debugger.vimのインストール

下記サイトからdebugger.zipをダウンロードします。

http://www.vim.org/scripts/script.php?script_id=1929

ダウンロードしたdebugger.zipを.vimディレクトリに展開します。

$ cd ~/.vim
$ unzip ~/Downloads/debugger.zip 

これでdebugger.vimのインストールは完了です。

debugger.vimが起動することを確認します。
まず、ファイル名の指定なしでVimを起動します。

$ vim

Vimが起動したら、fn + F5を押します。

waiting for a new connection on port 9000 for 5 seconds...

というメッセージが出力され9000ポートでListenが始まればインストールはOKです。

もしListenが始まらない場合は、VimPythonインタフェースが有効になっていない可能性があります。
下記コマンドでPythonインタフェースが有効になっているか確認します。

$ vim --version | grep python
 +persistent_undo +postscript +printer -profile -python -python3 +quickfix

「+python」となっていれば有効になっています。
「-python」となっている場合は有効になっていないので、Vimを再インストールする必要があります。

下記コマンドでVimをアンインストール。

$ sudo port clean vim

そして「+python27」オプションをつけてVimを再インストール。

$ sudo port install vim +python27

インストールが終わるまで時間がかかるので、コーヒーでも飲みながら待っていてください。

インストールが終わったら、Pythonインタフェースが有効になっていることを確認します。

$ vim --version | grep python
 +python -python3 +quickfix +reltime -rightleft +ruby +scrollbind +signs 
リンク: /usr/bin/gcc-4.2   -L. -L/opt/local/lib      -L/opt/local/lib -arch x86_64 -o vim       -lncurses -liconv -lintl -framework Cocoa    -L/opt/local/lib  -fstack-protector  -L/opt/local/lib/perl5/5.12.4/darwin-thread-multi-2level/CORE -lperl -lm -lutil -lc -L/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config -lpython2.7 -framework CoreFoundation -u _PyMac_Error /opt/local/Library/Frameworks/Python.framework/Versions/2.7/Python   -lruby -lobjc  -L/opt/local/lib

今度はOKです。
再度Vimを起動してfn + F5を押してみてください。
今度はListenが始まるはずです。

Xdebugのインストール

Peclで一発で入ります。

$ sudo pecl install xdebug

インストールできたことを確認します。

$ pecl list
Installed packages, channel pecl.php.net:
=========================================
Package Version State
xdebug  2.2.1   stable

次に、php.iniにXdebugの設定を追記します。

[Zend]
zend_extension=/usr/lib/php/extensions/no-debug-non-zts-20090626/xdebug.so

[Xdebug]
xdebug.default_enable = true
xdebug.remote_enable = true
xdebug.remote_host = localhost
xdebug.remote_port = 9000
xdebug.remote_connect_back = false
xdebug.remote_cookie_expire_time = 3600
xdebug.remote_handler = dbgp
xdebug.remote_log = /var/tmp/xdebug.log

設定が有効になっていることを確認します。

$ php -r "phpinfo();" | grep -i xdebug

Xdebug関連の設定がずらずらと表示されれば、とりあえず設定はOKです。

ステップ実行

下記のサンプルプログラムをステップ実行してみます。

index.php

<?php
$food = array(
         'Japanease' => 'Sushi',
         'Italian'   => 'Pizza'
        );

$nation = 'Japanease';

echo "I love $nation[$food]. Because I am $nation.";

上記のindex.phpApacheのドキュメントルートに配置してください。

Vimを起動して9000ポートでListenしている状態で、http://$HOSTNAME/index.php?XDEBUG_SESSION_STARTにリクエストを送信します。
URLに?XDEBUG_SESSION_STARTをつけないとApacheXdebugを使用してくれないので注意してください。

リクエストを送信すると、デバッガが起動します。

fn + F3でステップ実行できます。

fn + F5でプログラムを最後まで実行すると、
ブラウザに「I love Sushi. Because I am Japanease.」と表示されます。

今度はブレークポイントを設定してみます。
Vimでindex.phpを開き、9行目にカーソルをあわせて「:Bp」押し、ブレークポイントを設定します。

index.phpを開いたままfn + F5を押してリクエストを送信すると、ブレークポイント付きでデバッガが起動します。

fn + F5で実行すると、ブレークポイントを設定した行でプログラムの実行が停止します。

ここで変数の値を参照してみます。
「,e」と入力すると、カーソルがWATCH_WINDOWに移動します。
「/*{{{1*/ => eval:」と聞かれるので、「$nation;」と入力しエンターを押すと、$nationの値を表示できます。

変数の値を書き換えることも可能です。
「,e」と入力しWATCH_WINDOWに移動したあと、今度は「$nation = 'Italian';」と入力します。
すると、$nationの値を書き換えることが可能です。

この状態でfn + F5を押してプログラムを実行すると、今度はブラウザに「I love Pizza. Because I am Italian.」と表示されます。

まとめ

Xdebug + VimPHPのWebアプリをステップ実行してみました。
これでデバッグ用のvar_dumpを消し忘れて、女の子に「キモーイ」などと罵倒されることもなくなると思います。

HerokuのCedar stackでパッケージの依存関係がキャッシュされる罠にはまってみた

タイトルのとおりです。
Cedar stackをよく使っている人は知っていて当然のことなのかもしれませんが、自分は見事に罠にはまりました。

node-twitterというライブラリを使ってTwitterのStreaming APIを使ったアプリを作るのに、最初以下のようにpackage.jsonを書いていました。

{
    "name": "twitter-photo-montage"
  , "version": "0.0.1"
  , "private": true
  , "dependencies": {
      "express": "2.5.1"
    , "ejs": ">= 0.0.1"
    , "twitter": "0.1.17"
    , "socket.io": "0.8.7"
  }
}

バージョン0.1.17はnpmでインストールできるtwitter-nodeの最新バージョンです。
しかし、バーション0.1.17にはStreaming APIHTTPS接続するためのパッチが当たっておらず503エラーを返してしまっていたので、package.jsonを変更して、作者とは別の人がGitHubに公開しているパッチ版を使用することにしました。

{
    "name": "twitter-photo-montage"
  , "version": "0.0.1"
  , "private": true
  , "dependencies": {
      "express": "2.5.1"
    , "ejs": ">= 0.0.1"
    , "twitter": "https://github.com/rocketlabsdev/node-twitter/tarball/master"
    , "socket.io": "0.8.7"
  }
}

ところが、上記のpackage.jsonをpushしても503エラーが返ってくる事象が一向に改善しません。
おかしいと思い調べてみたところ、こんな記事を発見しました。

Use versioned github tarballs to update dependencies on heroku cedar stack — Gist

Cedar stackはパッケージの依存関係をキャッシュしていて、パッケージのバージョンが変わっていないとキャッシュしたものを使用するとのこと。
今回の場合、別の人が公開しているパッチ版が未パッチ版と同じバージョン0.1.17として公開されていたため、Cedar stack側が同じパッケージと見なして、キャッシュしたもの使っていたようです。
いったんtwitter-nodeのバーションを0.1.16に下げてpushしたあとで、再度パッチ版に変えてpushしたら、今度は無事パッケージが更新されました。

うーん。Herokuの日本語ドキュメントがほしい。

MinTTYの日本語まわりの設定まとめ

最近このエントリの影響を受けて、Cygwinを窓から投げ捨て、代わりにMinGW + MinTTYをインストールしました。
そのとき日本語まわりの設定で苦労したので、備忘録としてブログに残しておきます。

ロケールを日本語/SJISにする

メニューから「Options」→「Text」の順に選択し、「Locale」を「ja_JP」、「Character set」を「SJIS」に設定します。

lsで日本語ファイル名を表示できるようにする

.profileをホームディレクトリに作成し、下記内容を記述します。

alias ls='ls --color=auto --show-control-chars'
alias ll='ls -l'
alias l='ls -CF'

MinTTYの起動時に~/.profileが読み込まれるように、下記のショートカットアイコンを作成しておくと便利です。

C:\MinGW\msys\1.0\bin\mintty.exe /bin/bash --login -i
Vimを日本語化する

.vimrcをホームディレクトリに作成し、下記内容を記述します。

set enc=japan