オープンソースこねこね

Webプログラミングなどについてあれこれ。

RailsのControllerで使用するparamsは変数でなかった件

Railsの話。最近Rubyを少し触りはじめました。

で、RailsのControllerでリクエストパラメータを取得するのに利用するparamsというHashパラメータがあるのですけど、 メソッドの引数定義はないし、@がついてないのでインスタンス変数でもない。どこで定義されてるのよっこの変数は、と思ってたんですが、StackOverflowに同じ疑問をもつヒトの質問と回答がありましたので以下にリンクと和訳を。

なるほど。メソッドだったのねん。

Rails: "params" はどこで定義されている?

私はparamsを自分のコントローラでこんなふうに使っている。

class ProductsController < ApplicationController
  def create
    @product = Product.new(params[:aircon])
    ...
  end
end

paramsはApplicationControllerのアトリビュートなのか? @のプリフィクスがついてないので違うと思う。 それで、paramsは実際のところなんなの? ProductsController内のカスタムメソッドでも使えるの?

回答

これはActionController::Metalで定義されている。 ApplicationControllerはActionController::Metal を継承しているActionController::Baseを継承している。 Rails API (http://api.rubyonrails.org/) を見ればparamsがrequestオブジェクトのパラメータを 戻す関数であることがわかるよ。

LTSVをyamlぽいフォーマットで色付き表示するPHPスクリプト

LTSVをyamlぽいフォーマットで色付き表示するだけ。 以下の記事のPHP版適当実装。

Code

#!/usr/bin/env php
<?php
/*
 * ltsv viewer
 *
 * ex)
 *   cat foo.log | php ltsview.php
 */

// refereces below.
//   http://d.hatena.ne.jp/naoya/20130206/1360154312
//   http://d.hatena.ne.jp/maru_cc/20080216/1203166254

$data = file_get_contents("php://stdin");
$arr = explode("\n", $data);
foreach ($arr as $v) {
  $ltsv_arr = explode("\t", $v);

  foreach ($ltsv_arr as $ltsv_v) {
    if (strpos($ltsv_v, ":") === false) {
      continue;
    }

    list($key, $value) = explode(":", $ltsv_v, 2);
    echo pack('c',0x1B)."[1;34m".$key.pack('c',0x1B)."[0m";
    echo pack('c',0x1B)."[1;31m".":".pack('c',0x1B)."[0m";
    echo $value."\n";
  }
  echo "---\n";
}

Gistにもあげときました。

RVMでRuby2.0をCentOS6にインストールする

Ruby2.0のインストールなのですが、最初はcheckinstallでソースからRPM化しようとしてみたものの、いろいろ無理があったりうまく行かなかったりしたので見送り。rvmを使います。

rvm公式サイト

ユーザごとの独立した環境

最近はユーザのHOMEディレクトリ配下の独立した環境にインストールするのが流行りらしいです。 個人的には一人でサーバー管理やっていて、常時rootユーザでオペレーションしている身なので、システム(/usr/local配下)にインストールするのがいつものスタイルです。ですが、ここでは推奨される方法にしたがってみます。

依存パッケージのインストール

epelリポジトリを入れておく(いくつかの依存パッケージが標準のリポジトリにないため)

# wget http://ftp.jaist.ac.jp/pub/Linux/Fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
# rpm -ivh epel-release-6-8.noarch.rpm

依存パッケージをインストール

# yum install gcc-c++ patch readline readline-devel
# yum install zlib zlib-devel libffi-devel
# yum install openssl-devel make bzip2 autoconf automake libtool bison
# yum install gdbm-devel tcl-devel tk-devel
# yum install libxslt-devel libxml2-devel
# yum install --enablerepo=epel libyaml-devel

rvmをインストール

以下のコマンドを叩く。

$ curl -L https://get.rvm.io | bash -s stable

ちなみにシステム(/usr/local配下)にインストールするときはrootユーザで作業するか、以下のようにsudoで実行します。

$ curl -L https://get.rvm.io | sudo bash -s stable

rvmのパスを設定するための記述が以下のファイルに追記されるので、確認しておく

$ vim .bash_profile
$ vim .bashrc

再ログインしてbash_profile.bashrcの変更をロードする。

rvmの確認

バージョン確認

$ rvm -v

対応しているRubyの一覧

$ rvm list known

Ruby2.0.0をインストール

以下のコマンドを叩く。

$ rvm install 2.0.0

RubyGemsのインストールでエラーが出力される

Extracting rubygems-2.0.3 ...
Removing old Rubygems files...
Installing rubygems-2.0.3 for ruby-2.0.0-p0.............................................................................................................................
Error running 'env GEM_PATH=/home/kohkimakimoto/.rvm/gems/ruby-2.0.0-p0:/home/kohkimakimoto/.rvm/gems/ruby-2.0.0-p0@global:/home/kohkimakimoto/.rvm/gems/ruby-2.0.0-p0:/home/kohkimakimoto/.rvm/gems/ruby-2.0.0-p0@global GEM_HOME=/home/kohkimakimoto/.rvm/gems/ruby-2.0.0-p0 /home/kohkimakimoto/.rvm/rubies/ruby-2.0.0-p0/bin/ruby -d /home/kohkimakimoto/.rvm/src/rubygems-2.0.3/setup.rb --verbose',
please read /home/kohkimakimoto/.rvm/log/ruby-2.0.0-p0/rubygems.install.log
Installation of rubygems did not complete successfully.

が、害はないらしいので、よしとする。以下参考サイト

バージョン確認しておしまい。

$ ruby -v
ruby 2.0.0p0 (2013-02-24 revision 39474) [x86_64-linux]

MySQL server version for the right syntax to use near 'delimiter ... というエラーで少しハマった件

というMigrationプログラムを自作したのですが、このプログラムでMySQLのトリガを作成するSQLを実行したら、

mysql server version for the right syntax to use near 'delimiter ...

というエラーがでて動作しませんでした。PHPからPDOでMySQLに接続してSQLを実行しているだけなんですが。

で、ちょっと調べたら、トリガ作成の時に使用してるdelimiterというコマンドが問題で、これはSQLではなくて、mysqlのクライアント/usr/bin/mysqlが用意しているコマンドとのこと。は~。どうりで動かんわけだ。

とはいえdelimiterを使えないのはちょっと困るので、SQLの実行をPDOの代わりにmysqlコマンドで行うことができるようにPHPMigrateをアップデートしました。

https://github.com/kohkimakimoto/phpmigrate

以下のように設定してやれば、DBへの接続はPDOの代わりにmysqlコマンドを使用するになります。これでdelimiterも使えるようになります。

MigrationConfig::set('mysql_command_enable',    true);
MigrationConfig::set('mysql_command_cli',       "/usr/bin/mysql");
MigrationConfig::set('mysql_command_tmpsqldir', "/tmp");
MigrationConfig::set('mysql_command_host',      "localhost");
MigrationConfig::set('mysql_command_user',      "user");
MigrationConfig::set('mysql_command_password',  "password");
MigrationConfig::set('mysql_command_database',  "yourdatabase");
MigrationConfig::set('mysql_command_options',   "--default-character-set=utf8");

参考リンク

PHPMigrate - PHPでマイグレーションツールを作った

デプロイツールに引き続き、今度はPHPでMigrationツールを作りましたので、記事を書きます。

PHPMigrate - https://github.com/kohkimakimoto/phpmigrate

フレームワークとMigration

最近のWebアプリケーションフレームワークを使えば、まあだいたいMigrationの機能ついてくるんですよね。でもフレームワークに組み込まれているものだと、Migrationを実行するためにアプリを実行環境にデプロイしないとならなかったりします。Migrationとメインのアプリのコードがひとつのプロジェクトとしてフレームワークで規定された構造にパッケージされているので、Migrationのコードだけデプロイするというのはなかなか難しいものがあります。

Migrationで変更するDBのスキーマは実運用上だいたいカラムの追加やテーブルの追加がほとんどで、カラムにデフォルト値などを適切に設定しておけば、アプリのコードのデプロイより先行してDBのスキーマを変更しても問題ない。

けれどスキーマ変更より先にコードがデプロイされると、存在しないテーブルやカラムを参照してしまいエラーを起こしてしまいます。

デプロイ作業的には

DBスキーマ変更(Migration)→アプリのコードをデプロイ→キャッシュ削除などの後処理。

という順序でいきたいのだけど

アプリのコードをデプロイ→DBスキーマ変更(Migration)→キャッシュ削除などの後処理。

というようになってしまいます。これを避けるためには、Migrationだけを行う専用の実行環境を用意したりと、ちょっと工夫したりしなければならない。

フレームワークにMigrationが組み込まれているとDBの接続情報をアプリとMigrationで一元管理ができて、いいこともあるのだけど、個人的にはMigrationはフレームワークに対して、もうちょっと疎結合に構成されていたほうがいいんじゃないかという考えに至りました。

PHPMigrate

そういうわけで、すごくシンプルなMigrationツールをつくりました。

https://github.com/kohkimakimoto/phpmigrate

使い方

2013/04/22 追記

複数のDBに対してMigrationできるように修正したので、この記事の記述とは設定の書き方が変わりました。READMEを参考にしてください。

プログラムはmigrate.phpというPHPファイル一つだけなので、これをMigrationのタスクを管理するディレクトリにダウンロードします。ダウンロードしたら、エディタで開きます。ファイルの上の方にDBの接続情報を記述する箇所があるので、適宜環境にあわせて修正してください。

MigrationConfig::set('database_dsn',         'mysql:dbname=yourdatabase;host=localhost');
MigrationConfig::set('database_user',        'user');
MigrationConfig::set('database_password',    'password');
MigrationConfig::set('schema_version_table', 'schema_version');  # migarationの版を管理するテーブル名

修正したら

php migrate.php create [任意のマイグレーションタスク名]

というコマンドを打つと、

[タイムスタンプ]_[任意のマイグレーションタスク名].php

PHPファイルが生成されます。

<?php
/**
 * Migration class.
 */
class Dummy
{
  public function preUp()
  {
      // add the pre-migration code here
  }

  public function postUp()
  {
      // add the post-migration code here
  }

  public function preDown()
  {
      // add the pre-migration code here
  }

  public function postDown()
  {
      // add the post-migration code here
  }

  /**
   * Return the SQL statements for the Up migration
   *
   * @return string The SQL string to execute for the Up migration.
   */
  public function getUpSQL()
  {
    return "";
  }

  /**
   * Return the SQL statements for the Down migration
   *
   * @return string The SQL string to execute for the Down migration.
   */
  public function getDownSQL()
  {
     return "";
  }

}

そして、getUpSQLメソッドの戻り値にテーブル追加のSQL、getDownSQLにそれをもとに戻すSQLが戻されるようにコードを修正します。以下のような感じです。

  public function getUpSQL()
  {
     return <<<END

CREATE TABLE `sample` (
  `id` INT UNSIGNED NOT NULL,
  PRIMARY KEY (`id`) )
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_bin;

END;
  }


  public function getDownSQL()
  {
     return <<<END

DROP TABLE `sample`;

END;
  }

テーブルの変更内容はSQLを直接書きます。SelectやInsertを抽象化してくれるORMは好きなのですが、テーブル定義を行うDDLのようなSQLRDBMSの実装の影響も強く受けやすいと思っているので、無理に抽象化しない方針で設計しました。このほうが細かい調整がしやすいし、DBが最終的にどういう定義になるのかがわかりやすいと思っています。

あとは、以下のコマンドでmigrationを実行します

php migrate.php migrate

これで、変更が実行されます(上記の例だとsampleというテーブルが作成される)。そしてMigrationの版管理をするschema_versionというテーブルが自動で作成されてそこにファイルのプリフィクスだったタイムスタンプが登録されます。

戻す場合は

php migrate.php down

で戻せます。また他のコマンドとして

php migrate.php up      # 複数あるマイグレーションタスクで1件だけ進ませる。
php migrate.php status  # 現在の状態の表示。現在の版と、未実施のマイグレーションの一覧を表示。

があります。

またDB定義を書くのに、プログラムであるmigrate.phpを直接修正したくない場合は適当なPHPファイルを作って以下のようにmigrate.phpを読み込めば、定義情報を外出しすることもできます。

<?php
require 'migrate.php';

MigrationConfig::set('database_dsn',      'mysql:dbname=yourdatabase;host=localhost_modify');
MigrationConfig::set('database_user',     'user_modify');
MigrationConfig::set('database_password', 'password_modify');
MigrationConfig::set('schema_version_table',  'schema_version_modify');

Migration::main();

SublimeText - SideBarGitをWindowsで使う

SublimeTextというエディタをいじくり中なのです。

このSublimeText上でのGitオペレーションはSideBarGitというプラグインを使っているのですが、 Windowsだとちょい設定をしないとうまく動かなかったためメモしておきます。

インストール

Sublime Package Control(http://wbond.net/sublime_packages/package_control)を使っていれる。

Windows版のGitもインストールしておく。

設定

Sublimeでの設定はデフォルトのままでいいのですが、PCの環境変数を設定する必要があります。

  • ユーザ環境変数の「HOME」に「 C:\Users\username」のようにユーザのホームフォルダを指定。 ここのSSHの鍵ファイルを置いておく。

  • システム環境変数の「PATH」に「C:\Program Files (x86)\Git\bin」のようなgitコマンドがあるフォルダを設定する

また、操作するローカルのgitリポジトリがWindowsのネットワークごしのパス(\xx.xx.xx.xx\foo\barとか)にあると うまく認識しないので、その場合はネットワークドライブを割り当てて、ローカルHDD同様のアクセスパスを用意します。

参考リンク

https://github.com/SublimeText/SideBarGit/issues/12

Altax PHP Deploy tool - PHPでデプロイツールを作った

PHPでデプロイツールを作ってみました。

なんでこんなものを作ったかというと

を読んで、普段PHP使いの自分としてはPHPで動くシンプルなデプロイツールがほしいかなと思ったからです。 結構まじめにつくりました。

機能

CapistranoみたいにSSHを並列に動かして、定義したタスクを実行します。

1ファイルだけで動作します。

タスクはPHPで記述します。

インストール

インストールスクリプトがあるので、それを実行してください。

$ curl https://raw.github.com/kohkimakimoto/altax/master/installer.sh | sh

PHPのソースファイルを一つにまとめて作られる実行ファイルを/usr/local/bin配下に配置するので、rootユーザの権限が必要です。

※上記コマンドでインストールするファイルは/usr/local/bin/altaxだけなので不要になってアンインストールしたくなったらこのファイルを消すだけでOKです。

インストールできたら、試しにコマンドを実行してみてください。

$ altax

以下のようなコマンドラインオプションなどの情報が表示されます。

Altax is a simple deployment tool running SSH in parallel.

Altax version 1.0.3
Copyright (c) Kohki Makimoto <kohki.makimoto@gmail.com>
Apache License 2.0

Usage:
  altax [-d|-h|-f|-l|-c] TASK [ARGS..]

Options:
  -d         : Switch the debug mode to output log on the debug level.
  -h         : List available command line options (this page).
  -f=FILE    : Specify to load configuration file (default altax.php).
  -l         : List available tasks.
  -c         : List configurations.

Built-in tasks:
  init       : Create default configuration file (altax.php).

使い方

$ altax init

を実行するとカレントディレクトリにaltax.phpというファイルを生成します。このファイルを修正してホストやタスクの定義を書いていきます。

host('foo.sample.com',   'web');
host('bar.sample.com',   'web');
host('hoge.sample.com',  'db');

/**
 * Web 再起動
 */
desc('Restart web.');
task('restart_web',array('roles' => 'web'), function($host, $args){

  run("sudo /etc/init.d/httpd stop");
  run("sudo /etc/init.d/httpd start");
});

/**
 * DB 再起動
 */
desc('Restart db.');
task('restart_db',array('roles' => 'db'), function($host, $args){

  run("sudo /etc/init.d/mysqld stop");
  run("sudo /etc/init.d/mysqld start");

});

サーバ再起動だけのサンプルですけど、まあこんなかんじに。Capistranoっぽくかけるはず。上記のタスク定義ではPHPの無名関数を使っているのでPHP5.3以降でないと動きませんが、functionの定義部分を明示的に関数を作ってコールバック関数として渡すようにしてやれば、それ以前のバージョンでも動くと思います。

SSHの鍵設定や、リモートサーバのsudoの設定も適宜しておきます。

実行は以下のようにタスク名を指定するだけ。

$ altax restart_web

必要に応じてタスクの後ろに引数を渡して、タスク内のロジックで参照することもできます。またrun関数にユーザを指定するオプションをつけることで実行ユーザを変えることもできます。

desc('引数を使う');
task('sample_task', array('roles' => 'web'), function($host, $args){

  if (count($args) < 1) {
    echo "You must pass 1 argument.\n";
    return;
  }

  run("echo $args[0]", array("user" => "root"));

});

実行

$ altax sample_task helloworld

また-dオプションをつけて実行すれば、デバッグレベルのログを出力しますので、うまく動作しないときなどに利用してください。

$ altax -d sample_task helloworld
[2013-02-02T11:06:41+09:00] INFO *** Altax version 1.0.5 ***
[2013-02-02T11:06:41+09:00] INFO Executing task: [sample_task]
[2013-02-02T11:06:41+09:00] DEBUG Processing to fork process.
[2013-02-02T11:06:41+09:00] DEBUG Setup signal handler.
[2013-02-02T11:06:41+09:00] DEBUG Forked child to process to host [127.0.0.1] pid = 25136
[2013-02-02T11:06:41+09:00] DEBUG [127.0.0.1] Processing execute
[2013-02-02T11:06:41+09:00] DEBUG [127.0.0.1] Executing SSH Command [ssh -t 127.0.0.1 " sudo -uroot sh -c 'echo helloworld'"]
helloworld
Connection to 127.0.0.1 closed.
[2013-02-02T11:06:41+09:00] DEBUG [127.0.0.1] Child process 25136 is completed.
[2013-02-02T11:06:41+09:00] INFO [127.0.0.1] Altax process is completed.

で、このログにあるように内部的にはSSHのコマンドラッパーとして動いているので、run関数の処理の実態は

ssh -t 127.0.0.1 " sudo -uroot sh -c 'echo helloworld'"

というSSHコマンドだったりするのもわかります。

Githubに多少ドキュメントも書いたので参考までに。

雑記

マルチプロセスなプログラムは、結構ハマりどころがあって大変だったです。SSHの擬似端末とかの処理でハマったり。このへんも気が向いたら、そのうち書こうかな。