CakePHP 2.3.5 のメール送信で文字化けをしたら

CakePHP 2.3.5 のCakeEmailはバグっていて、ISO-2022-JPの特定の文字が入っているメールを送信しようとすると文字化け(というか、特定の文字列以降、その行がざっくりなくなる)します。

既に修正パッチを pull-request して、とりこまれているので、2.3.6 以降では治っていることになると思うんですが、取り急ぎ治したい人は以下のファイルを修正してください。

  • lib/Cake/Network/Email/CakeEmail.php の 1257 行目付近

修正前

        foreach ($lines as $line) {
            if (empty($line)) {
                $formatted[] = '';
                continue;
             } 
             if (!preg_match('/\<[a-z]/i', $line)) {

修正後

        foreach ($lines as $line) {
            if (empty($line)) {
                $formatted[] = '';
                continue;
            }
            if (strlen($line) < $wrapLength) {
                $formatted[] = $line;
                continue;
            } 
            if (!preg_match('/<[a-z]+.*>/i', $line)) {

修正ポイントは・・・

  • ISO-2022-JPの「受」のような特定の文字には「
  • このメソッドは第2引数の $wrapLength より長い場合に、その行に対してwrap処理をするためのものだから、$wrapLengthより短いならその行は処理しなくていいはず
  • そもそも「<」にエスケープいらなくね?

そもそも、wordwrap関数をかけてみたり、文字列を $line[$i] というように何バイト目?いうやり方でとってきたりと、マルチバイトで大丈夫か?という処理が行われているので、基本的にメールで送信する1行は 998 バイト以上にしないようにしといたほうがいいです。(998バイト以上になると、いろいろとヤバイ処理を通るので...)

今回はスルーした「その行にタグらしきものが入った場合」についても、おいおい処理内容をチェックして、マルチバイトでも大丈夫にしていこうかなと思っていたり、思ってなかったり...

受け身ではなく手を動かすイベントの楽しさ

もう10月ですねぇ。いよいよ開催が再来週と迫ってきた PHPMatsuri 2011の リレーブログです。

昨日の @tanakahisateru さんの PHPMatsuri 2010 からの1年を振り返って からバトンをうけて書きます。

このリレーブログ、今まで書かれている内容がかなり熱い内容なので、ぜひご一読を!

PHPMatsuriでできること

PHPMatsuriはセミナーや勉強会と違って参加者が手を動かすことを基本としたイベントです。セミナーや勉強会で自分の知らなかった情報を得るのも楽しいのですが、自分でそれを身につけるにはやはり自分で手を動かすことが一番だと思います。(PHPMatsuriではセミナー形式のセッションも並行で行われますが、それを聞くのも良し、聞かずにもくもくとコードを書き続けるもよし、それは参加するあなたの自由です)

また、普段の仕事では使っていないフレームワークやライブラリを使っていろいろ試すということをとことんできる場だと思います。ここ数年symfomy 1.XやCakePHP 1.Xを使って仕事をしてきた人(自分もそうですが)にとって、Symfony 2やSilex、CakePHP 2.XやLithiumといったこれから主力となりそうなフレームワークでいろいろ試す絶好の機会です。

そして、PHPMatsuriではこれらのフレームワークの開発者が来日→参加するので、ちょっとした疑問でもばんばん聞けてしまうというまたとないチャンスとなります。日本にいながらにしてそれぞれのフレームワークの哲学を開発者本人から聞けてしまうというのは本当にすばらしいことだと思います。

昨年のPHPMatsuri 2010では結構夜遅くまで開発者の方々がメインの部屋に残られていたので、ちょっと勇気をだせばいくらでもお話が聞けると思いますよ。

PHPMatsuriの雰囲気

自分は昨年のPHPMatsuriからの参加で、しかも午前中に別の用事があったため、午後からの参加となったのですが、会場に着いたときになんとも言えない雰囲気にびっくりした覚えがあります。全員がノートパソコンを開いて周りと時には会話をしながらそれぞれがコードを書いている姿は今でも忘れられません。

その中に一度飛び込んでしまえば好きなだけコードを書いていられるというか、それぞれ別のことをしてるはずなのに独特の一体感があってコードを書く人にはたまらない雰囲気だと思います。

結局昨年は部屋にはもどらずずっとメインとなった会場で朝までコードを書いていたのですが、もっと時間があればなぁと思うくらいの充実感でした。

関西で行われる意義

個人的には今回のイベントでうれしいと思うところ。関西出身者としては関西でのイベントが少しでもふえてほしいなぁと思っていて、こういうイベントが関西で行われるってのは本当にうれしい。

PHPMatsuriのように超大物をゲストに呼んでとかになるとスタッフの皆さんが大変ご苦労されていると思うのですが、みんなが集まって一晩中コードを書こうぜ!ということなら場所さえあればできちゃうわけで、是非ともいろんなところで開催されてほしいですね。

来年はまた違う場所で行われるといいですよね。

手を動かしたら発表しよう

PHPMatsuriのいいところは、手を動かした結果をみんなの前で発表する場があるってこと。自分が作ったモノなんてたいしたことないなんて思わずに、ひとまず発表してみるってのがいいと思います。自分が普段当たり前だと思っていることが意外と周りの人からすると当たり前でないことが多いわけで、周りの人も何より自分自身でなにか発見するモノがあると思います。

まずは参加してみよう

だらだらと書きましたが、こんな楽しそうなイベント、チケットを購入すればいつでも潜り込むことができますよ。興味を持った方は是非チケットを購入して参加してみてください。

次は @k1Low さんがいらっしゃる Fusic さんのブログにつなぎます。よろしくお願いします。

セットアップ手順(不親切版)

とりあえず試したいという人がいるかもしれないので、ある程度自力でいろいろできる人向けのセットアップ手順を書いときます。(もっと親切なものは後から書くかもしれない)

サーバを準備する

EC2やNiftyクラウド、さくらVPSとかでサーバを準備してください。注意事項としてはPHPバージョンは5.2以上じゃないといけないです。

あと、一つ前のエントリで書いたようにメールが外部に送信できるようにしておいてください。

追記
ローカル環境にセットアップして、Firefox+Firemobilesimulatorとかで動作を試してみるってのがいいと思います。

ソースをgithubからとってくる

適当なディレクトリにソースをとってきてください。(例で使っているディレクトリはあくまで例です)

  cd /home/www
  git clone https://github.com/kunit/yammer.git
  cd yammer
  git submodule init
  git submodule update

YammerからOAuthのConsumer Key/Secretを取得して設定する

YammerのAPIをたたくために以下の手順でアプリケーションの登録を行って、Consumer Key/Secret を取得してください。

  • ガラケーでアクセスしたいネットワークのアカウントでYammerにログインする(※注意1)
  • http://developer.yammer.com/api/ にアクセスする
  • その画面の右上の Register Application のリンクをクリック
  • アプリケーションの情報を入力する。Application名は「Mobile Yammer for xxx」の形式にしていると他のものとかぶらないでいいと思う。(※注意2)
  • 入力してsubmitすると Consumer Key/Secret が発行される

(※注意1) YammerのOAuthの権限はネットワーク毎に発行されます。そのためログインしないとアプリケーションの登録作業ができないです。
(※注意2) このアプリケーション登録情報は今のところ後から変更できないっぽいので適当につけると後で泣きをみます

上記の手順で登録した内容を設定ファイルを作成して記述します。

  cp app/config/yammer.php.default app/config/yammer.php

yammer.php で設定するのは以下の4つです。

Yammer.oauth_mail_from
権限設定メールを送信するときのFrom
Yammer.oauth_mail_subject
権限設定メールを送信するときのSubject
Yammer.oauth_key
取得したConsumer Key
Yammer.oauth_secret
取得したConsumer Secret

CakePHPのセキュリティーの基本設定をする

core.phpをコピーしてつくってください。

 cp app/config/core.php.default app/config/core.php

core.phpの以下の2つの値を変更してください。(これはCakePHPの通常のセットアップでやることと同じなので、なんのこっちゃと思った場合は適当にぐぐってください。すみません)

  • Security.salt
  • Security.cipherSeed

テーブルを作成する

以下のコマンドを実行してテーブルを作成してください。実行するとy/nで答えるものが2つ出ますが、それらはEnterを押してデフォルトの答えのままで大丈夫です。

  cake/console/cake schema create

書き込み権限の付与

app/tmp 以下はWebサーバから書き込みができるように権限を変更してください。

  chmod -R a+w app/tmp

Webの公開設定

app/webroot をDocument RootとなるようにWebサーバの設定をしてください。また、.htaccessで設定が上書きできるようにしてください(このあたりはCakePHPの設定と同じです)

ここまでやれば(たぶん)つかえるはず。

...と書いてみたんですが予想通りとてつもなく不親切だなぁ。ファイル展開したらすぐ使えますとかになればいいんだろうけど、いくつかは設定してもらわないといかんし。これでどれくらいの人ができるかってのを様子を見ながらセットアップ手順の簡略化は考えます。

Yammer for ガラケー でなにをやってるか?

構築手順の説明の前にこのアプリでなにをやっているかを書いときます。

YammerのAPIをたたくためにOAuthを使って権限を取得しないといけないんですが、その取得の流れのURLがガラケーからは現状たたけないので、以下のような流れにしてます。

  1. ユーザにメールアドレスとパスワードをいれてもらう(このときのメールアドレスとパスワードはこの携帯サイトへのログインで使用するためのもので、Yammerのサイトで設定してあるものとまったく別物でかまわない)
  2. 入力してもらったメールアドレス宛にYammerのauthorizeのURLが入ったメールを送信する
  3. そのメールをPCでうけとってもらって、そこに入っているURLにPC側でアクセスして、アプリのアクセス許可をだす
  4. アクセス許可を出すと4桁のverifierが入手できるのでそれを携帯サイト側で入力する

1番目で入力したメールアドレスとパスワードでこのアプリにログインするとYammer APIを使用してメッセージを取得したりできるようになるので、なんかややこしい動きなのはこの権限を取得するところだけになります。

こんな流れになっているので、このアプリに保存される(デフォルトではSQLiteに保存してる)データはこのサイトにログインするためのメールアドレス/パスワードとOAuthで使用する各種キー/シークレットだけです。今のところメッセージやユーザ一覧等はその都度APIでとってきてるので、モバツイみたいにキャッシュして賢くなんやかんややってくれるとかはしてないです。(ユーザやグループの画像はそのまま使うとでかすぎるのでリサイズをする関係上キャッシュしてる形になってます)

アプリのセットアップをするためには携帯からWebアクセスできるだけではだめで、メールを送信して権限設定をするので、サーバからメールが送信できるように設定しておかないといけないです。(今のところそのサーバで外部からメールを受け取れるようにする必要はないです。将来的にはメールを使って画像付きメッセージ投稿とかできるようにするかもしれないので、そのときには必要になると思いますが)

Yammer for ガラケー ソース公開

Twitterでちょこちょこツイートしていたんだけど、震災以後作っていたYammerにガラケーからアクセスするためのゲートウェイとなるアプリケーションのソースをgithubで公開しました。

github - Yammer for Keitai

#いろいろソースを整理したりするつもりだったけど、大江戸Ruby会議から帰ってきて爆睡をしてしまったので、もうそのままでいいやってことでpushしてます...

CakePHPでささっとつくったもので、YammerのAPIをたたいているだけで、そんなにたいしたことはやってないです。

まぁ、今までガラケーでアクセスできなかったYammerがそれなりにつかえるようになるので、自分たち的には便利に使い始めてます。

で、この公開したソースを使ってどのようにサイトを構築するかの手順ですが、さらっと書こうと思ったらなにげにいろいろやらないといけないので、きちんとまとめてあとで書きます。すみません...

ControllerからModelを使用するいくつかの方法 (CakePHP Advent Calendar 19日目)

CakePHP Advent Calendar 2010 はみなさんのノウハウがいろいろと知ることができてほんと楽しいですね。

18日目のhaltさんのauthkittenプラグインで子猫認証 (CakePHP Advent Calendar 2010 18日目) を引き継いで、19日の記事となります。


※本日の記事は CakePHP 1.3.6 がベースになっています。

ControllerからModelを使用する方法ですが、一番手軽なのはControllerの「usesプロパティ」だと思います。

usesプロパティは以下の流れでモデルをロードします。

  • Dispatcher#dispatch (webroot/index.php)
  • Dispatcher#_invoke (cake/dispatcher.php)
  • Controller#constuctClasses (cake/libs/controller/controller.php)

Controller#constructClasses のソースは以下のようになっています。

function constructClasses() {
  $this->__mergeVars();
  $this->Component->init($this);

  if ($this->uses !== null || ($this->uses !== array())) {
    if (empty($this->passedArgs) || !isset($this->passedArgs['0'])) {
      $id = false;
    } else {
      $id = $this->passedArgs['0'];
    }

    if ($this->uses === false) {
      $this->loadModel($this->modelClass, $id);
    } elseif ($this->uses) {
      $uses = is_array($this->uses) ? $this->uses : array($this->uses);
      $modelClassName = $uses[0];
      if (strpos($uses[0], '.') !== false) {
        list($plugin, $modelClassName) = explode('.', $uses[0]);
      }
      $this->modelClass = $modelClassName;
      foreach ($uses as $modelClass) {
        $this->loadModel($modelClass);
      }
    }
  }
  return true;
}

最初のif文をよく見ると「!==」でチェックをしていて、Contrrolerでモデルをロードしたくなければ、厳密に「null」か「array()」でなければならないということがわかります。(モデルを使用しないつもりで false を代入してもダメなのはこのせいです)

usesプロパティが false(usesプロパティを指定してない場合も false になる) だと $this->modelClass を使用してモデルをロードします。($this->modelClass はデフォルトでは Controller 名から作られるもので UsersController なら User になるといった規約になります)

usesプロパティに対して配列指定でモデル指定した場合、1つめに設定した値を特別扱いし、$this->modelClassを上書きしています。複数指定する場合、適当に指定していいのではなくて、1つめは特別な意味を持つことを覚えておく必要があります。

さて、モデルのロードで使用されている Controller#loadModel ですが、これはフレームワーク内部だけで使われるものではなくて、我々が普段作成しているControllerの中でモデルをロードしたいときに使えるメソッドになります。(loadModelはCakeBookの「3.5.4.4.7 loadModel」にきちんと説明されています)

class FooController extends AppController {
  public function index()
  {
   // これで $this->Bar->... という形でモデルが使用できる
    $this->loadModel('Bar');
  }
}

Controllerで複数モデルを使用するときにusesに指定してしまうと、モデルを使用しないActionでも全てのモデルをロードしてしまうので、usesに指定するのは全Actionで使用するものだけにし、それ以外は loadModel でロードするのが効率がいいということになります。

Controller#loadModel のソースを見ると、ClassRegistry#init を使用してモデルクラスを生成しています。ClassRegistry#init 内でモデルクラスを生成しているソースの部分を抜き出してみます。(内部的にいろんなことをやってくれてるんですが、今回は説明に必要な部分だけ抜き出します。ClassRegistryに関しては id:hiromi2424 さんの「ClassRegistry徹底解剖」が参考になります)

  if (class_exists($class) || App::import($type, $pluginPath . $class)) {
    ${$class} =& new $class($settings);
  } elseif ($type === 'Model') {
    if ($plugin && class_exists($plugin . 'AppModel')) {
      $appModel = $plugin . 'AppModel';
    } else {
      $appModel = 'AppModel';
    }
    $settings['name'] = $class;
    ${$class} =& new $appModel($settings);
  }

ここで重要になってくるのが elseif の方で、うまくクラス生成ができなくて、さらにそれが Model だった場合、AppModel クラスとして生成されるということです。

usesプロパティに存在しないモデルを指定('User'と指定しないといけないのに'Users'にしてたとか)して、バリデーションがうまく動かずに困ったことがある人は結構いると思うんですが、それはここで AppModel が生成されるからになります。

ということで Controllerでモデルを使用する場合の注意点をまとめると...

  • uses プロパティには必要最低限のモデルを指定する
  • 追加でモデルをロードするには loadModel を使う
  • モデルを使用しない場合は null か array()
  • uses プロパティに存在しないモデルを指定すると AppModel になる

Modelのロードはそれなりに重たい処理なので、不必要なモデルは極力ロードしないように気をつけましょう!

ということで、明日の担当は connvoi_tyou さんです。よろしくお願いします!

symfony1/Symfony2の起動シーケンスの違い

この記事は、Symfony アドベントカレンダー 2010 に参加しています。


Symfony2のソースを初めて見たのは先月行われた第1回 Symfony2 勉強会が初めてだったんですが、大幅に変わった起動シーケンスにびっくりして、さらにその素直なシーケンスに大変感動しました。

というわけで、今日はsymfony1とSymfony2の起動シーケンスの違いを簡単に比較したいと思います。

まずは、symfony1(symfony 1.4)ですが、web/index.php は以下のようになっています。

<?php


require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');

$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', false);
sfContext::createInstance($configuration)->dispatch();

流れとしては以下のようなものになっています。

  • 設定ファイルを読み込む(フレーム本体のfactories.yml/アプリケーションのfactories.yml)
  • 読み込んだ設定ファイルからFrontControllerを決定し、そのdispatchメソッドを読み込む

アプリケーション側のfactories.ymlで上書きしなければ、sfFrontWebController#dispatch が呼ばれます。その中では、リクエストパラメータからモジュール名とアクションが決定されるという形になります。

sfFrontWebController#dispatch は以下のようなものです。

  public function dispatch()
  {
    try
    {
      // reinitialize filters (needed for unit and functional tests)
      sfFilter::$filterCalled = array();

      // determine our module and action
      $request    = $this->context->getRequest();
      $moduleName = $request->getParameter('module');
      $actionName = $request->getParameter('action');

      if (empty($moduleName) || empty($actionName))
      {
        throw new sfError404Exception(sprintf('Empty module and/or action after parsing the URL "%s" (%s/%s).', $request->getPathInfo(), $moduleName, $actionName));
      }

      // make the first request
      $this->forward($moduleName, $actionName);
    }
    catch (sfException $e)
    {
      $e->printStackTrace();
    }
    catch (Exception $e)
    {
      sfException::createFromException($e)->printStackTrace();
    }
  }

その後決定されたモジュール/アクションに応じてフィルターの構築が行われ、フィルター実行の一環としてアクションが実行されるというものでした。(この流れですが、symfony1の元となったMojaviの流れとあまり変わりません。それはもともとMojaviを使っていた人がsymfonyに移行しやすかった要因の一つともいえると思います)

さて、Symfony2の方はどうなっているかというと、先日でたPR4のweb/app.php は以下のようになっています。

<?php

require_once __DIR__.'/../app/AppKernel.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('prod', false);
$kernel->handle(new Request())->send();

Symfony2ではHttpKernelInterfaceを実装したクラスがベースの処理を受け持ちます。これはHTTPのシーケンスを素直にマッピングしたようなものになっており、上記のファイルの最後のメソッドも以下のように読むことができます。

  • リクエストを受け取り、
  • それをハンドリングしてレスポンスを作成し、
  • その結果をクライアントに送信する

具体的に追っかけていくと、AppKernel#handleは Symfony\Component\HttpKernel\Kernel#handle なので以下のような処理です。

    public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
    {
        if (false === $this->booted) {
            $this->boot();
        }

        return $this->container->get('http_kernel')->handle($request, $type, $catch);
    }

この「$this->boot()」の処理は8日目のDIコンテナの起動とエクステンション - ゆっくり*ゆっくりにあるようにDIContainerの初期化を実行しています(詳しくはそちらをご覧ください)。その後、DIContainerに http_kernel として登録された Symfony\Component\HttpKernel\HttpKernel#handle を呼び出します。

    public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
    {
        $masterRequest = HttpKernelInterface::MASTER_REQUEST === $type ? $request : $this->container->get('request');

        $this->container->set('request', $request);

        $response = parent::handle($request, $type, $catch);

        $this->container->set('request', $masterRequest);

        return $response;
    }

ここでも親クラスのhandleの処理結果のレスポンスを返すという素直な実装になってます。さて、親クラスは Symfony\Component\HttpKernel\BaseHttpKernel なので、そのhandleは以下のようになっています。

    public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
    {
        try {
            return $this->handleRaw($request, $type);
        } catch (\Exception $e) {
            if (false === $catch) {
                throw $e;
            }

            // exception
            $event = new Event($this, 'core.exception', array('request_type' => $type, 'request' => $request, 'exception' => $e));
            $this->dispatcher->notifyUntil($event);
            if ($event->isProcessed()) {
                return $this->filterResponse($event->getReturnValue(), $request, 'A "core.exception" listener returned a non response object.', $type);
            }

            throw $e;
        }
    }

で、BaseHttpKernel#handleRawは以下のようになってます。

    protected function handleRaw(Request $request, $type = self::MASTER_REQUEST)
    {
        // request
        $event = new Event($this, 'core.request', array('request_type' => $type, 'request' => $request));
        $this->dispatcher->notifyUntil($event);
        if ($event->isProcessed()) {
            return $this->filterResponse($event->getReturnValue(), $request, 'A "core.request" listener returned a non response object.', $type);
        }

        // load controller
        if (false === $controller = $this->resolver->getController($request)) {
            throw new NotFoundHttpException('Unable to find the controller.');
        }

        $event = new Event($this, 'core.controller', array('request_type' => $type, 'request' => $request));
        $this->dispatcher->filter($event, $controller);
        $controller = $event->getReturnValue();

        // controller must be a callable
        if (!is_callable($controller)) {
            throw new \LogicException(sprintf('The controller must be a callable (%s).', var_export($controller, true)));
        }

        // controller arguments
        $arguments = $this->resolver->getArguments($request, $controller);

        // call controller
        $retval = call_user_func_array($controller, $arguments);

        // view
        $event = new Event($this, 'core.view', array('request_type' => $type, 'request' => $request));
        $this->dispatcher->filter($event, $retval);

        return $this->filterResponse($event->getReturnValue(), $request, sprintf('The controller must return a response (instead of %s).', is_object($event->getReturnValue()) ? 'an object of class '.get_class($event->getReturnValue()) : is_array($event->getReturnValue()) ? 'an array' : str_replace("\n", '', var_export($event->getReturnValue(), true))), $type);
    }

処理されるControllerを決定しているのは「$this->resolver->getController($request)」になります。(メインのControllerをcall_user_func_arrayで実行しているというのは軽く衝撃をうけました)

リクエストを受け取って、処理した結果のレスポンスを出力するというとっても素直な流れというのはわかりやすいですし、またこのわかりやすい流れはフレームワーク本体にがっちり組み込まれているわけではなく、FrameworkBundleという外部部品を使用して構成されているという作りにも拡張性の高さを感じます。

Symfony2はMojavisymfonyという流れをいったん断ち切って全く新しいものとなっています。そういう意味では今までその流れで使っていた人にはちょっと取っつきにくいかもしれませんが、それはそれ Fabien っぽいコードになれていればさらさら読めると思います。

また、今までsymfonyはまったく触ったことがなくてという方も今までのベースは全く必要ない新しいフレームワークになっているので、是非Symfony2を触ってみてはいかがでしょうか!

Symfony Advent 2010であなたの記事を公開してみませんか?

Symfony Advent 2010では12月1日から12月24日までを使って日替わりでsymfonyで イイなと思った小さなtipsから内部構造まで迫った解説などをブログ記事にして公開していくイベントです。

参加についてはATNDで参加表明の上、Google GroupのSymfony Advent 2010に追加リクエストを送信ください。

Symfony Advent 2010チーム一同、あなたの参加をお待ちしております。

日本Symfonyユーザー会 Symfony アドベントカレンダー2010

Symfony Advent 2010はsymfony好きな有志で集まったチームです。