Archive
Macのローカル開発環境にメールの送信テストをするための環境を構築するまとめ
概要
Web系のシステムを作っていると、ユーザのアクティベーションのためにメールを送ったり、 何らかのイベントが起きたときに通知メールを送ったりなど、メール送信を絡めた機能がちょくちょく必要になります。 ステージング環境にはテスト用のメールサーバを動かしているプロジェクトも多いですが、ローカル開発環境まで面倒をみてくれることはまれです。Macならメールサーバを比較的簡単に導入できるので、Macを開発機に使ってる人はメール送信テスト用の環境を作ることをお勧めします。
諸注意
メールサーバを起動した状態でメール送信機能を起動すると当然ながら、メールが宛先となっているメールアドレスまで飛んでいってしまいます。手順の中でメールサーバをローカル配信限定の設定にしてインターネット側にメールが飛ばないようにしていますが、万が一ということもありうるので、リアルなメールアドレスはなるべく使わないようにしましょう。
メールサーバの起動/再起動/停止
メールサーバには postfix を使います。基本的なコマンドは次の通り。
$ sudo postfix start $ sudo postfix reload $ sudo postfix stop
メール送信
まずはメールサーバの動作チェックをするために自分のメールアドレスにメールを送信してみます。postfix を起動してメールを送信してください。メールを送信するには mail コマンドを使います。
$ mail -s 'subjects' mail@example.com メールテストです。入力がそのまま本文になります。 「CTRL + D」キーを押すと本文入力終了です。 CTRL + D
「-s」でサブジェクトを、その後に送信先のメールアドレスを指定します。 本文を入力し、「CTRL + D」キーを押すと本文の入力が終了しメールが送信されます。 メールボックスを確認するとメールが届いているはずです。(GMailにはスパムメール扱いされる可能性が高いです)
ローカル配送専用設定
メールの送信テストができたらメールサーバをローカル配信専用に設定します。 僕の環境では次の設定を加えると外部にメールが飛ばなくなりました。 設定にはあんまり自信がないので各自要チェックお願いします。
/etc/postfix/main.cf
mynetworks_style = host relayhost = $mydomain
設定できたら postfix を再起動します。 mailコマンドを使ってメールが外部に飛ばないことを確認しましょう。
$ sudo postfix reload
メールキューの確認
飛ばしたメールはメールキューに溜まっていくのでとりあえずメールキューを見てみましょう。
$ mailq
一覧表示されたと思います。
キューの内容確認
メールキューの内容を確認してみましょう。 -q のオプションにはメールキュ一覧から取得できるキューIDを指定します。
$ sudo postcat -q AF5F92D2547
メールが表示されたと思います。
キュー削除
メールキューがたくさん溜まりすぎるとあまりよろしくないので適当なタイミングで削除しましょう。 キューIDを指定して削除したり、一括で削除することもできます。
$ sudo postsuper -d AF5F92D2547 $ sudo postsuper -d ALL
ISO-2022-JP メールの表示
メールキューの内容を確認したときに気づいたかもしれませんが、ISO-2022-JPでエンコードされたメールは文字化けして表示されます。文字化けせずに表示するには、文字コード変換プログラムのnkfを使います。
$ brew install nkf $ sudo postcat -q AF5F92D2547 | nkf -m
文字化けせずに表示されましたね!
送信されたメールを流し続ける
とりあえず送信されたメールを文字化けせずに表示するところまでできました。 ここまででもテスト自体は可能ですが、メールを送信するたびに postcat するのは面倒です。 メールが送信されてきたらメールの内容をダダ流しできるようにしましょう。
ポイントは次の3つです。
- 開発時はメールの送信先がすべて root@localhost になるようにシステムを改修する
- root に届いたメールを自分のメールボックスに転送する
- 自分のメールボックスを tail -f で監視する
開発時はメールの送信先がすべて root@localhost になるようにシステムを改修する
システムによりまちまちだと思いますが、CakePHPなら次のように実装することができます。app/config/core.php に実行環境を定義する環境変数を設定して、メール送信時に環境を判別して開発環境ならメールの送信先が root@localhost に上書きされるようにします。
app/config/core.php
Configure::write('environment', 'development');
app/controllers/components/user_mail.php
$environment = Configure::read('environment');
if ($environment == 'development') {
$mail = 'root@localhost';
}
root に届いたメールを自分のメールボックスに転送する
root 宛のメールが自分のメールボックスに転送されるようにエイリアスを設定します。
/etc/postfix/aliases
#root: you root: cohakim
エイリアスを設定したら、エイリアスファイルを再構築して postfix を再起動します。
$ sudo newaliases $ sudo postfix reload
自分のメールボックスを tail -f で監視する
/var/mail/USER_NAME が自分のメールボックスになります。
nkf -u で出力をバッファせずにすぐに出力させることができます。
$ tail -f /var/mail/cohakim | nkf -u -m
終わりに
以上でローカルにメールのテスト環境を構築できたと思います。
Macを使うと簡単ですね。Windowsの人は頑張れ!
[CakePHP] Capistranoを使って本番/開発環境のデータベースを切り替える
概要
Web開発において開発環境と本番環境でデータベースを切り替えるという運用は一般的ものだと思います。 CakePHPはデータベースの設定を複数持つことはできるのですが、データベースの設定を実行環境に応じて自動で適切に切り替えるということができません。 データベース設定の自動切り替えについては色々なハックが公開されていますが、今回はCapistranoを使ってデータベースの自動切り替え機能を実装してみました。
ポイント
- database.php にあらかじめ本番用と開発用のDB設定を記述しておく
- CakePHPのConfigureからデータベースの設定を切り替えられるようにする
- デプロイ時にDB接続環境設定用のコードを埋め込む
コード
app/config/database.php に下記のようなコードを記述します。 本番用と開発用のDB設定を記述し、コンストラクタをオーバーライドしてConfigureからDBの接続環境を取得しています。
app/config/database.php
<?php
class DATABASE_CONFIG {
var $default = array(
'driver' => 'mysql',
'persistent' => false,
'host' => 'localhost',
'login' => 'root',
'password' => 'root',
'database' => 'fanarts',
'prefix' => '',
'encoding' => 'utf8'
);
var $production = array(
'driver' => 'mysql',
'persistent' => false,
'host' => '192.168.100.100',
'login' => 'fanarts',
'password' => 'fanarts',
'database' => 'fanarts',
'prefix' => '',
'encoding' => 'utf8'
);
public function __construct() {
$connection = Configure::read('environment');
if (!empty($this->{$connection})) {
$this->default = $this->{$connection};
}
}
}
デプロイ時にDB接続環境設定用のコードが埋め込まれるようにします。
config/deploy.rb
after "deploy:symlink", :roles => [:app] do
run %Q!echo "Configure::write('environment', 'production');" >> #{current_path}/app/config/core.php!
end
CapistranoでCakePHPアプリをデプロイするときの設定
概要
Capistranoを使ってCakePHPアプリをデプロイする機会があったので設定をメモ。CapistranoのデフォルトタスクはRailsに最適化されています。そのまま使うと要所要所でエラーがでます。
ポイント
- public/images, public/stylesheets などのディレクトリの作成をキャンセルします
- 余計なタスクをスキップします
- ログやキャッシュディレクトリは共有ディレクトリへのシンボリックリンクを張ります
設定
config/deploy.rb
set :normalize_asset_timestamps, false
namespace :deploy do
task :restart do end
task :start do end
task :stop do end
task :migrate do end
task :migrations do end
end
after "deploy:symlink", :roles => [:app] do
run "mkdir #{current_path}/app/tmp"
run "ln -s #{shared_path}/log #{current_path}/app/tmp/logs"
run "ln -s #{shared_path}/system/cache #{current_path}/app/tmp/cache"
end
[CakePHP]業務システム用にエラーハンドリングを実装するまとめ
概要
rails でいう rescue_from。 各コントローラで発生し処理されなかった例外を、 AppController で一括で補足して独自の例外処理を行うためのメモ。Cakephp のバージョンは 1.3 を想定しています。
ポイント
- エラーハンドリングの方針を決める
- 例外ハンドラをオーバーライドして独自の例外処理を定義できるようにする
- データベースへの問い合わせに失敗した場合、例外を投げてログにエラーメッセージを記録する
- プロダクションモードで例外ハンドラが正常に動作しない対策を行う
エラーハンドリングの方針を決める
基本的に復帰できない例外をハンドリングするために使用します。(必要であるなら)エラーメッセージをログに記録し、汎用的なエラー画面を出力するというのが主な機能です。 画面に出力するエラーメッセージは、小規模な業務システムを想定し、以下のようなポリシーにします。
- 生のエラーメッセージを出さない。
- 復帰できないエラーであることを明示する。
- サポートへの連絡先を記載する。
生のエラーメッセージを出さない。
システムに詳しくないユーザがみる画面なので、エラーメッセージはマイルドなものにします。Webサーバが出力する素のエラー画面(白地にInternal Server Errorなどのメッセージ)はもちろん、PHPから出力されるエラーメッセージなども表示しないほうが無難です。
→ 仕様外の入力などの場合でも、素のエラーメッセージがでてしまうとシステムの瑕疵だと一方的に決めつけられる場合があります。
復帰できないエラーであることを明示する。
バリデーションエラーなど復帰可能なエラーとは明確に区別します。
→ 復帰可能と見せかけてそうでなかったことが判明した場合、非常に苦しい立場に追いやられます。
サポートへの連絡先を記載する。
運用窓口への連絡先を記載しておきます。
→ たらい回しにされたと感じているユーザは非常に辛辣です。
例外ハンドラをオーバーライドして独自の例外処理を定義できるようにする
例外ハンドラはAppControllerに定義します。 app/app_controller.php に以下のコードを追加します。
app/app_controller.php
// ----------------------------------------------------------
// exception handler
// ----------------------------------------------------------
public function __construct() {
parent::__construct();
set_exception_handler(array($this, 'exception_handler'));
}
public function exception_handler($e) {
$exception_class_name = get_class($e);
$this->exception_info_log_write($e);
switch ($exception_class_name) {
case 'QueryFailureException':
$this->cakeError('error500');
break;
default:
$this->cakeError('error500');
break;
}
}
public function exception_info_log_write($e) {
$this->log('['.get_class($e).'] '. $e->getFile().' on line '.$e->getLine()."\n".$e->getMessage());
}
QueryFailureException は自前で定義した例外です。 問い合わせが失敗したときはクエリのエラーログをとりたいので、例外の種類を特定できるように独自の例外を定義しました。
app/app_controller.php
class QueryFailureException extends Exception {
}
データベースへの問い合わせに失敗した場合、例外を投げてログにエラーメッセージを記録する
「存在しないカラム名を指定した」などクエリが不正だった場合のエラーを、SQLエラーと名付けることにします。 CakePHPはバリデーションエラーは簡単に取得できるのですが、SQLエラーのエラーメッセージを取得するには若干面倒な手順が必要です。
まず、モデルにSQLエラーのエラーメッセージを取得するメソッドを定義します。エラーメッセージはDBオブジェクトから直接取得するしかないようです。メソッドはAppModelやビヘイビアに定義するのがベターなのですが、下記メソッドをモデル外に定義したらDBオブジェクトのインスタンスをうまく取得できなかったため、全モデルにメソッドを定義しています乙。
app/models/customer.php
function lastError() {
$db =& ConnectionManager::getDataSource($this->useDbConfig);
return $db->lastError();
}
コントローラに例外処理を追加します。saveメソッドの戻り値ではバリデーションエラーとSQLエラーを区別できません。明示的にバリデーションした後にsaveすることでエラーの種別を区別するようにしました。 エラーメッセージ付きの例外を投げることでエラーメッセージがログに記録されます。
app/controllers/customer_controller.php
function create() {
$this->Customer->set($this->data);
$result = $this->Customer->validates();
if (!$result) {
$this->render('new_');
return;
}
$result = $this->Customer->save();
if (!$result) {
throw new QueryFailureException($this->Customer->lastError());
return;
}
$id = $this->Customer->getLastInsertId();
$this->setFlash('notice', '顧客情報を登録しました。');
$this->redirect("/customers/{$id}");
}
プロダクションモードで例外ハンドラが正常に動作しない対策を行う
プロダクションモード(Configure::write(‘debug’, 0))の時、$this->cakeError(‘error500′) が正しく動かないぽいのでそれの対応をいれます。症状はどのエラーでも $this->cakeError(‘error404′) が呼び出されるという症状です。よくわかりません。 エラーハンドリングの直前に Configure::write(‘debug’, 1) を呼び出すことで対応しました。
app/app_error.php
class AppError extends ErrorHandler {
function __construct($method, $messages) {
Configure::write('debug', 1);
parent::__construct($method, $messages);
}
}
あとがき
以上でベーシックな一括エラーハンドリングができたと思います。 改善点などありましたらぜひ教えてください。 CakePHPむつかしい。><
バリデーションエラーを一括して出力する
概要
Railsの error_messages_for 的なヘルパメソッドがなかったので作ってみた。 画面の上部に全てのバリデーションエラーを簡単に出力できます。
コード
app/views/helpers/application.php
function error_messages_for() {
$view = ClassRegistry::getObject('view');
$errors = $view->validationErrors;
$messages = '';
if (!empty($errors)) {
$messages .= '<div id="errorExplanation">';
foreach($errors as $error) {
$messages .= join('<br />', $error);
}
$messages .= '</div>';
}
return $messages;
}
CakePHPでRails風flashを使う
概要
CakePHPのflashってなんか使いにくくね?と思ったので定義し直した。
コード
app/app_controller.php
function setFlash($context, $message) {
$this->Session->setFlash($message, 'default', array(), $context);
}
app/controllers/hoge_controller.php
$this->setFlash('error', 'エラーだよ!');
app/views/helpers/application.php
function result_messages() {
if ($this->Session->check('Message.notice')) {
$this->Session->flash('notice');
}
elseif ($this->Session->check('Message.warning')) {
$this->Session->flash('warning');
}
elseif ($this->Session->check('Message.error')) {
$this->Session->flash('error');
}
}
app/views/hoge/fuga.ctp
$application->result_messages();
補足
「見栄えに関する設定はヘルパに書くべきだと思います!」という思いから再定義してみました。 今回は素のメッセージを出力してますが、使うときはヘルパで別途タグを組み立ててくだされ。
ヘルパからビュー変数を参照する
概要
ヘルパからビュー変数を参照する方法です。 パラメータを渡してもいいのですが、直接参照する方法もあります。
コード
function hoge() {
$view = ClassRegistry::getObject('view');
$user_id = $view->getVar('user_id');
}
DBのデータからselectの要素を生成する
概要
DBに登録してあるデータからoption要素を一括生成! Railsでいう options_from_collection_for_select です。
コード
ヘルパ
function options_for_job() {
$MstJob = ClassRegistry::init('MstJob');
$jobs = $MstJob->find('all');
$opts = Set::Combine($jobs, '{n}.MstJob.id', '{n}.MstJob.name');
return $opts;
}
ビュー
$form->select('mst_job_id', $application->options_for_job());
CakePHPでfindしたときにオブジェクトを返すようにする
概要
CakePHPが全然オブジェクト指向じゃないとお嘆きのあなた。 モデルが配列ばかり返して、オブジェクトを返してくれないことに失望したあなた。 あるんです!オブジェクトを返す方法が!
コード
app/app_model.php
class AppModel extends Model {
function afterFind($results, $primary = false) {
if($primary == true && !is_object($results) && !empty($results)) {
return Set::map($results);
}
return $results;
}
}
補足
これでオブジェクトが返るようになりました。 た・だ・し!全くおすすめしません。
テストが通るか未確認。APIの互換性が保証されてるか確認していません。このコードは他サイトにあったコードの一部を改修したものですが、コピペ元サイトのコードではcountしたときに返る配列もオブジェクト化してしまったため、API互換が崩れてしまっていました。ぼくもほとんどテストしてません。
stdClassが返る。stdClassが返るということで、結局値をプロパティとして取得するくらいにしか使えません。自分のモデル名を書かなくてすむということと、配列で取得するよりかはアロー演算子を使った方がほうが簡潔に書けるので、なんぼかタイピング量を節約できるというくらいです。find結果の配列が保持している自クラス名を使ってキャストするという手もありますが、実際やってみるとかなりの処理コストがかかり、常用するには厳しそうです。
遅延評価できない。もともと配列をオブジェクトのプロパティに変換しただけなので、全て静的に保持しています。必要になってから初めて処理を行うというような使い方はできません。
というわけでオブジェクトで返すメリットはほとんどありません。 無念です。
CakePHP 1.2.9 でNotice出力を制限する
概要
PHPには標準的なエラー出力機能が備わっており、レポートを出力するかどうかをエラーレベルごとに設定することができるのですが、おせっかいにもCakePHPはこの設定を強制的に上書きし、独自の基準でエラー出力してくれやがります。 今回はそんなおせっかいにノーサンキューをつきつける方法です。
手順
PHPでは error_reporting() 関数を使って出力するエラーレベルの設定を切り替えます。 当初は app/config/core.php に設定を書き加えることで設定できるかもと考えましたが、やはりライブラリ内部で上書きしているようで、うまく動作しませんでした。 しかたないので、この関数を全文検索し該当箇所を書き換えていくことにしました。
error_reportingで検索するとcake/libs/configure.phpの294行目あたりに見つかると思うので、これを書き換えましょう。 今回はDEPRECATEDとNOTICEをエラーレポート対象外に設定します。
cake/libs/configure.php
function write($config, $value = null) {
$_this =& Configure::getInstance();
if (!is_array($config)) {
$config = array($config => $value);
}
foreach ($config as $names => $value) {
$name = $_this->__configVarNames($names);
switch (count($name)) {
case 3:
$_this->{$name[0]}[$name[1]][$name[2]] = $value;
break;
case 2:
$_this->{$name[0]}[$name[1]] = $value;
break;
case 1:
$_this->{$name[0]} = $value;
break;
}
}
if (isset($config['debug'])) {
if ($_this->debug) {
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE); // この行を書き換え
if (function_exists('ini_set')) {
ini_set('display_errors', 1);
}
if (!class_exists('Debugger')) {
require LIBS . 'debugger.php';
}
if (!class_exists('CakeLog')) {
require LIBS . 'cake_log.php';
}
Configure::write('log', LOG_NOTICE);
} else {
error_reporting(0);
Configure::write('log', LOG_NOTICE);
}
}
}
これでOKかと思いきや、まだ出力が抑制されません。 error_reporting関数を使っている箇所は他にないようなので、別の用語で検索する必要があるようです。 今度はエラーレベルの定数である E_NOTICE で検索してみましょう。 cake/libs/debugger.phpの195行目に見つかりました。これをコメントアウトしてみましょう。
cake/libs/debugger.php
function handleError($code, $description, $file = null, $line = null, $context = null) {
if (error_reporting() == 0 || $code === 2048 || $code === 8192) {
return;
}
$_this = Debugger::getInstance();
if (empty($file)) {
$file = '[internal]';
}
if (empty($line)) {
$line = '??';
}
$file = $_this->trimPath($file);
$info = compact('code', 'description', 'file', 'line');
if (!in_array($info, $_this->errors)) {
$_this->errors[] = $info;
} else {
return;
}
$level = LOG_DEBUG;
switch ($code) {
case E_PARSE:
case E_ERROR:
case E_CORE_ERROR:
case E_COMPILE_ERROR:
case E_USER_ERROR:
$error = 'Fatal Error';
$level = LOG_ERROR;
break;
case E_WARNING:
case E_USER_WARNING:
case E_COMPILE_WARNING:
case E_RECOVERABLE_ERROR:
$error = 'Warning';
$level = LOG_WARNING;
break;
//case E_NOTICE: // コメントアウト
case E_USER_NOTICE:
$error = 'Notice';
$level = LOG_NOTICE;
break;
default:
return false;
break;
}
$helpCode = null;
if (!empty($_this->helpPath) && preg_match('/.*\[([0-9]+)\]$/', $description, $codes)) {
if (isset($codes[1])) {
$helpCode = $codes[1];
$description = trim(preg_replace('/\[[0-9]+\]$/', '', $description));
}
}
echo $_this->_output($level, $error, $code, $helpCode, $description, $file, $line, $context);
if (Configure::read('log')) {
CakeLog::write($level, "{$error} ({$code}): {$description} in [{$file}, line {$line}]");
}
if ($error == 'Fatal Error') {
exit();
}
return true;
}
出力されなくなりました! うまくいきましたね!





