Displaying posts filed under

CakePHP

Super Awesome Advanced CakePHP Tips 訳と註2

誰が作成・修正したのか、自動的に追跡する

……
Super Awesome Advanced CakePHP Tipsの翻訳2回目です。内容的には前回の続きになっています。

Automatically Tracking Created/Modified By註

Created/Modified By
この章に出てくる created/modified by は、そのままでは日本語に訳すことができない。ある程度説明的に訳すしかない。
establish
細かいことだが現在形。つまり著者はこの本が先頭から読まれるものだとは必ずしも想定していないということになる。
the previous section
翻訳上は全く問題無いのだが、この the previous section が具体的に何を指しているのかはちょっと興味深い。というのも、この本の中で見出しが付けられた内容のまとまりを何と呼ぶべきかに関わるからだ。
もしこの section が『Getting the Logged In User from Anywhere』全体を指すのであれば(文脈からはそれが最も自然だろう)、この本の中で最も大きなまとまりは「section = 節」と呼ばれていることになる。しかし個人的には、節では少し小さく感じる。「章 = chapter」と呼んだ方が相応しいような気がする。そして実は作者もブログの中で「Chapter」という呼び方をしている。Check out Chapter 2: “How to Learn CakePHP”. だがもし『Getting the Logged In User from Anywhere』や『Automatically Tracking Created/Modified By』を章と呼ぶのであれば、ここの the previous section は何を指すのだろうか?前の章の最後の節である『Full Source』だろうか?意味の上ではあり得なくもないが、ちょっと変に感じる。
Now we can apply this to setting the created by …
この文はうまく訳せなかった。わずかな単語にいろいろな意味が圧縮されているので、日本語だと説明的になりすぎて文にならない。created by and modified by fields で「誰が作成・更新したのか記録するためのフィールド」、setting は他の用例から考えれば「(フィールドに)値を設定する」という意味。for various database records は構造の異なる様々なレコード(テーブル)に対して、という意図だろう。
Model beforeValidate Callback
この段落全体は著者の勝手な仮定によるお遊びで、それゆえ仮定法が用いられている。「こんな文を書くくらいなら素直に beforeValidate コールバックの例を書けば良いじゃないか!」と読者に思わせるための、ツッコミ待ちの文章。
make this into a behavior
もちろん「これをビヘイビアの中に入れる」と直訳しても意味は通じるが、「ビヘイビア化する」という日本語がぴったりだろう。
Then
この Then は「すぐに」の意味だろうか。
feel mildly foolish
feel foolish で「きまりが悪い」「ばつが悪い」といった意味。
justified in believing …
be justified in doing … で「…は正当である」「…するのももっともである」といった意味になるのだが、主語と動詞が欠けている。文脈からすれば主語は多分 you だろう。
So I won’t insult your intelligence …
最後のこの文は仮定法ではない。「あくまで仮定」という装いで散々書いておきながら、最後に「私はそういうことはしないよ」と言っている。読者からのツッコミ待ち。
We’re making a bit of a leap here, in believing that …
非常に訳しにくいので訳文はごまかした。すいません。leap はここでは多分「論理の」飛躍という意味で、段落冒頭の The logic と対応している。「『idが設定されていれば更新だ』と考えるのは論理が飛躍している」ということで文脈にも合う。さらに進行形なので、「今論じているこのロジックの中で、論理の飛躍を犯しつつある」といった感じか。また前置詞 in は時間または状況の中という意味にとって、「このロジックの中の、具体的には…と考えているところで、論理の飛躍を犯しつつある」という風に解釈するのが最も自然だろうか。
If you wanted to be diligent
仮定法。その前の文の outside possibility と対応しており、著者はidの偽装が問題となる可能性は非常に低いと見ている。そもそも「誰が作ったか」を自動追跡することが必要になるアプリケーションというのは、かなり限られたユーザだけが使うログイン制のシステムだから、だろうか?
less
この less は excluding と同じ意味で、「〜を除いて」。

Super Awesome Advanced CakePHP Tips 訳と註1

任意の場所からログイン中のユーザを取得する

……
Matt Curry氏が無償公開されている電子書籍『Super Awesome Advanced CakePHP Tips』の翻訳を始めました。既にbobchin氏が全体を翻訳完了されている(と公開直前に知った)のですが、私自身の勉強も兼ねて、なぜそう訳したのかという註釈を含めて拙訳を公開していきます。原文を読まれる際にお役立てください。また間違いや疑問などありましたら教えていただけると嬉しいです。

第一弾は16ページからの『Getting the Logged In User from Anywhere』です。私が興味を持った順に訳していきます。とても時間がかかるので、翻訳完了時期は今のところ未定です。

Getting the Logged In User from Anywhere註

クリックすると該当行をハイライトして表示します。

from Anywhere
日本語で「どこからでも」と言うと譲歩の意味が強過ぎるので、ちょっと堅苦しい表現だが「任意の場所から」とした。
This is a problem that comes up often.
This はタイトル「Getting the Logged In User from Anywhere」の内容を指し、それが「よく生じる問題である」、ということで「よく問題となる」といったところか。
modified user
素直に読めば「修正されたユーザ」としか読めないのだが、それでは意味が通らない。文脈からすればこれは次章「Automatically Tracking Created/Modified By」の内容を示していることは明らかなので、modified user ひとまとまりで「修正を行ったユーザ」と理解するのが正しいのだろう。Web検索でも同様の用例が見つかる。
beforeFilter
モデルには beforeFilter コールバックは存在しない(次章の内容からすれば実装上正しいのは beforeValidate)。なぜここで beforeFilter と言ったのか。(1)コントローラの beforeFilter を指す。(2)単なる間違い。(3)主処理の前に実行されるコールバックを抽象的に beforeFilter と呼んでいる。多分(3)?
It would be great if you could …

前の文は単なる仮定だが、この文は仮定法。つまり handle 以下は現実には不可能だと認識している。「なぜ不可能なのか」に相当する内容は書いていないが、前段落までビューとコントローラからセッションにアクセスする方法について述べているのだから、ここではモデルからセッションにアクセスする方法が”無い”ことを仮定法によって暗示していると見なせる。
You can always cheat …
always が悩ましい。本当に「いつでも」という意味だと理解すると、ビューでもコントローラでも使えばいいじゃないか、という話になって次の文とのつながりが苦しくなる。「必要ならいつでも」くらいに訳すべきか。さらに次の文の註参照。
Plus, now we are using three …
この文は翻訳では何か言葉を補わないとうまく繋がらない。まず文頭の Plus が何に対して「加えて」いるのかが問題になる。always の註に書いた通り、前の文に対する付加だとすると意味が通じない。これは冒頭の problem に対する付加と見なすべきだろう。
つまり作者の問題意識は2つある。一つ目は Model からセッションに直接アクセスする方法がないこと。二つ目はアクセスする方法がバラバラであること。よってこの2つを同時に解決する方法を提示するのがこの章全体の主題となっている。章の冒頭のこの節ではまず前者について述べ始めたのだが、最後になって後者を付け加えた。あるいは書いている途中で自分の2つ目の問題意識に気付いたのかも知れない。
but for the User::get syntax to work it has to …
but は単なる接続詞。for は不定詞 to work の主語を示す。語順を変えると「but it has to go in the model for the User::get syntax to work.」
Deal with it.
「うまく対処してくれ」→「なんとか我慢してくれ」くらいの意味になるようだ。参考。直訳だとちょっと様にならないので意訳を採用。
instance
これ以後 instance という語が頻繁に使われるのだが、その意味は日本語オブジェクト指向用語の「インスタンス」とは厳密には一致しない(必ずしも何らかのクラスに属するオブジェクトではない)。しかしWikipediaを参照する限り英語オブジェクト指向用語の「instance」も日本語の「インスタンス」とほとんど変わらない意味だから、やはりこの文章における用例とは一致しない。多分、instance が元々持っている「具体事例」というような意味と、オブジェクト指向用語 instance が持つ語感とを混ぜ合わせたような意味で用いられているのだと思う。
Before you can access … you need to …
「…するためには…しなければならない」「…して初めて…することができる」
get the instance and pull out a specific piece of information from the user array.
この and を単純な並列にとると意味が通じない(User::get は $path パラメータが必須で、「インスタンス」を取得するためのものではない)。やはり the instance と the user array は同じ物を指していると考え、「インスタンスを取得して、そして一部を取り出す」というように前後関係を表していると見るべきだろう。
inspired
敢えてカタカナで「インスパイア」の方が雰囲気が出ると思う。パクリじゃない、インスパイアだ!
the syntax isn’t as appealing.
後ろに「as that of User::get()」が省略されている。
Why not just use the same setup for …
意味は鮮明だが訳しにくい。「Why not A …, but B …」なぜBを除きAしないのか(すれば良いではないか)。思いっきり分かりやすく言うと、 User::get() を Configure に対する Wrapper にすれば良いじゃないか、ということ。
Again, certainly an option.
うまく訳せなかった。 短い文は難しい。 an option は「User::get() を Configure に対する Wrapper にする」という(読者からの想定)提案を指し、Again はその前の「Configure クラスをそのまま使う」という案に対するもの。だと思うのだが、自信がない。
withing
within の typo だろう。

recursiveで悩むな、Containableを使え

CakePHPのModelが備える recursive 機能は分かりにくい上に効率が悪い。代わりにContainableビヘイビアを使うべき。今後新たに作り始めるならAppModelに次のように書いても良いくらい。

class AppModel extends Model {
    var $recursive = -1;
    var $actsAs = array('Containable');
}

このビヘイビアは、一言で言えば ActiveRecord(Rails) の include オプションのようなもの。一度の find で取得する関連を明示的かつ再帰的に指定することができる。

/*
Group hasMany User
  User hasOne Profile
*/
$rows = $this->Group->find('all', array('contain' =>
                                        array('User' => 'Profile')))

他にも取得する field を指定したり conditions を指定したりすることもできる。詳しくはCookbook和訳)参照。

ちなみにrecursiveの値そのものは、大まかには「(CakePHPのモデル実装が)追加で必要とするクエリの回数」を表しているものと思われる。そのルールそのものは Containable ビヘイビアを使ったとしても変わらない。すなわち belongsTo や hasOne のデータは基準となるテーブルにjoinして取得されるので追加のクエリは不要、よってrecursive=0。hasMany と hasAndBelongsToMany は追加で1回必要になるのでrecursive=1。recursive=2以降は…正直よく分からない(参考。2以降はルールが違うらしい)。Containableを使った方が絶対に良い。

paginateで複雑な検索を行う

joinやサブクエリ、集合演算などを含む複雑な検索を行う場合、

  1. まず検索条件にマッチする全行のidを求め、
  2. Model.id IN (1で求めた全id) という条件で一覧表示に使うデータを取得する

という手順を踏んだ方が合理的なことがある。

この場合、1の段階で「検索条件にマッチする全行数」が判明するため、Paginationのために改めてCOUNTクエリを発行するのは完全に無駄である。CakePHP 1.2のPagination機能でこの無駄を回避するには、コントローラの $paginate プロパティに適当なキー(下例では numberOfRows)で件数を保存しておき、

$allIds = $this->Foo->find('all', array('fields' => 'DISTINCT Foo.id',
                                        'conditions' => $complexCond));
$this->paginate['numberOfRows'] = $this->Foo->getNumRows();

対象となるモデルの paginateCount メソッドでその数値をそのまま返すようにする。

class Foo extends Model
{
  function paginateCount($conditions=null, $recursive=0, $extra=array()) {
    if(isset($extra['numberOfRows'])) {
      return $extra['numberOfRows'];
    }
    $parameters = compact('conditions');
    if ($recursive != $this->recursive) {
      $parameters['recursive'] = $recursive;
    }
    return $this->find('count', array_merge($parameters, $extra));
  }
}

$paginate の中で認識されないキーは、Model::paginate や Model::paginateCount の最後の $extra 引数として渡ってくる。

メソッドチェーンでHTMLを組み立てるView Helper

メソッドチェーンを用いて複雑なHTMLを組み立てることができるMarkup Helperを公開しました。(実用上)PHP 5.2以上が必須です。

cakephp-markup-helper – GitHub

例1:単純なdiv,pタグを出力

echo $markup->div('section')->p->text('これは<テスト>です')->end->end;
/*
出力されるHTML:
<div class="section">
 
これは&lt;テスト&gt;です
</div>
 
*/

例2:HtmlHelperと併用し、「ユーザ一覧」テーブルを構築

echo $markup->div('list')
->table
->thead
->html($html->tableHeaders(a('id', 'ユーザ名', '操作')))
->end
->tbody->nl;
 
foreach ($users as $user) {
  echo $markup->tr
    ->td->text($user['User']['id'])->end
    ->td->text($user['User']['username'])->end
    ->td('actions')
    ->html($html->link('編集',array('action'=>'edit', $user['User']['id'])))
    ->end
    ->endtr->nl;
}
 
echo $markup->endAllTags;

使い方は上の例からほとんど想像がつくだろうと思います。特徴は次の通りです。

  • ほとんど全てのメソッドがインスタンス自身を返すので、延々メソッドチェーンを繋げていくことでどんな複雑なHTMLも構築することができます。
  • 内部で「どのタグがまだ閉じられていないか」という情報を管理しているため、タグ名を明示しなくてもタグを閉じることが出来ます。逆に敢えてタグ名を明示して、構造が正しいことを確認することもできます。
  • 生成されたHTMLは内部のバッファに蓄えられ、__toString で文字列として返却されます。PHP 5.2以降ではオブジェクトが文字列として使用される際に自動的に __toString が呼ばれるため、違和感なく echo(またはその他の関数)で出力することができます。

省略記法の種類

できる限り短く、書きやすくするため、__get と __call を活用した省略記法が多数用意されています。

  省略記法 実際のメソッド呼び出し
プロパティ $markup->{method_name}; $markup->{method_name}();
メソッド $markup->{tag_name}(arg1, arg2, …); $markup->startTag({tag_name}, arg1, arg2, …);
$markup->end{tag_name}(); $markup->endTag({tag_name});
$markup->nl(); $markup->newline();
$markup->end(); $markup->endTag();

これらの省略記法を用いることで、例えば次のような記述が

$markup->div('css-class')->p->end->enddiv->nl;

次のようなメソッド呼び出しに変換されます。

$markup->startTag('div', 'css-class')
  ->startTag('p')->endTag()->endTag('div')->newline();

主要なメソッド

細かい動作についてはユニットテストも参考にしてください。

startTag($tag, [$attrs, $content $escapeContent]) $tagの開始タグを生成してバッファに追加します。$contentが与えられた場合は終了タグも追加します。

$tag string 必須 タグ名
$attrs string or array null HTML属性。文字列が与えられた場合はclass属性の値と見なされる
$content string null 要素の内容
$escapeContent boolean true trueの場合は$contentをHTMLエスケープする
endTag([$tag]) 終了タグを生成してバッファに追加します。$tagが与えられなかった場合は最も内側のタグを、$tagが与えられた場合は最も内側の$tagタグとその内側の全てのタグを閉じます。$tagが与えられ、かつ閉じるべき$tagが存在しない場合はE_USER_WARNINGとなります。

$tag string null タグ名
endAllTags() 全てのタグの終了タグを生成してバッファに追加します。
text([arg1, arg2, ...]) 任意の数の文字列をHTMLエスケープしてバッファに追加します。
html([arg1, arg2, ...]) 任意の数の文字列をそのままバッファに追加します。
newline() 改行(0x0A)をバッファに追加します。
renderElement([arg1, arg2, ...]) エレメントをレンダリングしてバッファに追加します。下記の説明を参照してください。

エレメントのレンダリング

エレメントをレンダリングするための専用メソッドとして、renderElement が用意されています。

echo $markup->div('search-result')->renderElement('search/result')->end;

もちろん html メソッドを使って、View の elementメソッドの戻り値をそのまま埋め込むこともできます。

echo $markup->div('search-result')->html($this->element('search/result'))->end;

ただしこの方法では MarkupHelper が内部で保持している文脈情報がそのままエレメント内に引き継がれます。エレメント内で endAllTags メソッドなどを呼んだ場合、意図しないタグまで閉じられてしまうことがあります。

MarkupHelper には文脈情報を隔離するために pushNewContext/popContext メソッドが用意されています。renderElement メソッドでは自動的にそれらのメソッドを呼び出し、エレメントのレンダリングが独立した文脈の中で実行されるようになっています。

echo $markup->div;   //
<div>
 
$markup->pushContext();
echo $markup->p->strong->text('foo')->endAllTags; //
 
<strong>foo</strong>
 
$markup->popContext();
 
echo $markup->end;  //</div>

他のヘルパーの中から使う

この pushNewContext/popContext メソッドを活用することで、他のヘルパーの中でも安全に MarkupHelper を使用できます。「生成したHTMLをバッファ変数に溜めておいて、最後にreturnする」という典型的なヘルパーの処理は、全て MarkupHelper が肩代わりしてくれます。

<?php
class YourHelper extends AppHepler
{
  var $helpers = array('markup');
  /**
   * 定義リスト
<dl>..</dl>
 
 を作るヘルパーメソッド
   */
  function defList($data)
  {
    $this->Markup->pushNewContext->dl->nl;
    foreach($data as $term => $desc) {
      $this->Markup
        ->dt->text($term)->end
        ->dd->html($desc)->end->nl;
    }
    return $this->Markup->endAllTgs->popContext;
  }
}
 
/* 使用例 */
echo $your->defList(aa('用語1', '<strong>説明1</strong>', '用語2', '説明2'));
 
/* 出力(改行もこの通りになります)
<dl>
<dt>用語1</dt>
<dd><strong>説明1</strong></dd>
<dt>用語2</dt>
<dd>説明2</dd>
</dl>
 
*/

AuthComponentを使いやすくするビヘイビア

CakePHP 1.2 の AuthComponent はとても便利なのですが、POSTされたパスワードの値を自動的に暗号化(hash)してしまうため、ユーザの create/update を行う際のバリデーションが困難になります。そこで AuthComponent を補助する AuthModel ビヘイビアを作ってみました。

auth_model.php – gist:188066

このビヘイビアを使うことで

  • 標準のバリデーション機能を使ってパスワード入力値を検証できるようになります
  • 「再入力」による確認を行うための sameValue バリデータが使えるようになります
  • データ更新時に「パスワード欄が空ならパスワードを変更しない」という動作を簡単に実現できます

以下、password カラムを備えた users テーブル(すなわち User モデル)を例に使い方を説明していきます。

まず User モデルに AuthModel ビヘイビアを登録してください。いくつか使用可能なオプションがありますが最後に説明します。

class User extends AppModel {
  var $actsAs = array('AuthModel');
}

次にユーザ登録用のビューを作ります。AuthComponent による自動変換処理を回避するため、パスワード入力用のフィールドには本来のカラム名とは異なる名前を使用してください。AuthModel ビヘイビアはデフォルトで password_input という名前を使用するので、特にこだわりがなければこの名前を使うことをおすすめします。またパスワード再入力確認用のフィールドも用意しておきます。こちらは名前は何でも構いません。下の例では password_confirmation にしています。

<?php
echo $form->create('User');
echo $form->input('username', array('label' => 'ユーザ名'));
echo $form->input('password_input',
                  array('type' => 'password',
                        'label' => 'パスワード',
                        'value' => ""));
echo $form->input('password_confirmation',
                  array('type' => 'password',
                        'label' => 'パスワード(再入力)',
                        'value' => ""));
echo $form->end('作成');
?>

更新用のビューもほぼ同じ構成です。パスワードを空にした場合は変更されない、という説明書きを加えておきます。

<?php
echo $form->create('User');
echo $form->hidden('id');
echo $form->input('username', array('label' => 'ユーザ名'));
echo $form->input('password_input',
                  array('type' => 'password',
                        'label' => 'パスワード',
                        'value' => ""));
echo $form->input('password_confirmation',
                  array('type' => 'password',
                        'label' => 'パスワード(再入力)',
                        'value' => ""));
echo $form->end('更新');
?>
※パスワードを空欄にした場合は更新されません

そしてこの password_input フィールドに対するバリデーションを User モデルに設定していきます。以下の例では3つの設定を行っています。

  • 入力は必須(requiredルール)
  • 文字数は6文字以上(lengthルール)
  • 「再入力」フィールドと値が一致(confirmルール)

最後のルールは AuthModel ビヘイビアが提供する sameValue バリデータを使います。このバリデータに対する唯一のオプションは再入力用フィールドの名前です。

class User extends AppModel {
  var $actsAs = array('AuthModel');
 
  var $validate = array('password_input' =>
    array('required' =>
          array('rule' => '/.+/',
                'required' => true,
                'allowEmpty' => false,
                'last' => true,
                'on' => 'create',
                'message' => 'パスワードを入力してください'),
          'length' =>
          array('rule' => array('minLength', 6),
                'allowEmpty' => true,
                'last' => true,
                'message' => 'パスワードは6文字以上で入力してください'),
          'confirm' =>
           array('rule' => array('sameValue', 'password_confirmation'),
                 'allowEmpty' => true,
                 'message' => 'パスワードと再入力の値が一致しません')));
}

それぞれのルールにおける required, allowEmpty, on, last オプションの設定が少し複雑ですが、よく分からなければCookbookを参照してください。

最後に UsersController です。beforeFilter 中で setAuthComponent メソッドを呼び出し、AuthComponent を登録しておきます(実は AuthComponent の中でデフォルトのhashアルゴリズムを使用している限り、この操作は必要ありません。しかし将来的に互換性の問題が発生する可能性もあるので、Userモデルのcreate/updateを行うコントローラでは必ず実行しておいてください)。

ユーザの追加/更新を行うadd/editアクションの中では特別な操作は必要ありません。

class UsersController extends AppController {
  function beforeFilter() {
    parent::beforeFilter();
    $this->User->setAuthComponent($this->Auth);
  }
 
  function add() {
    if (!empty($this->data)) {
      if ($this->User->save($this->data)) {
        $this->Session->setFlash('ユーザを作成しました。');
        $this->redirect('index');
      } else {
        $this->Session->setFlash('エラーを修正してください。');
      }
    }
  }
 
  function edit($id = null) {
    if(empty($this->data)) {
      if (!$id || !($row = $this->User->findById($id))) {
        $this->redirect('index');
      }
      $this->data = $row;
    } else {
      if($this->User->save($this->data)) {
        $this->Session->setFlash('ユーザ情報を更新しました。');
        $this->redirect('index');
      } else {
        $this->Session->setFlash('エラーを修正してください。');
      }
    }
  }
}

注意

このビヘイビアを使用すると直接パスワード用カラムに値を設定して保存することができなくなります(セキュリティ上の配慮)。この制約を回避するには save メソッドの validate オプションを無効にして実行してください。

  //ユーザ名 = パスワード = admin のユーザを追加
  function install() {
    $initialUser = array('User' =>
                         array('username' => 'admin',
                               'password' => $this->Auth->password('admin')));
    $validate = false;
 
    if($this->User->find('count') == 0) {
      $this->User->save($initialUser, $validate);
    }
    $this->redirect('/');
  }

応用

データ更新時の「パスワード入力欄が空ならパスワードを変更しない」動作が不要な場合、required ルールの on => create オプションを取り除いてください。さらに、滅多にないことだとは思いますが、空のパスワードを許可する場合は下記の allow_blank オプションを有効にしてください。

オプション

キー デフォルト値 説明
column string password パスワードを保存するカラムの名前
input string password_input パスワード入力用のフィールド名
allow_blank bool false 空のパスワード使用可否
hash_function callback array(Security,hash) パスワードのハッシュを求める際に使用するコールバック

全てのオプションを設定した場合の例:

class User extends AppModel {
  var $actsAs = array('AuthModel' =>
                      array('column' => 'secret',
                            'input'  => 'secret_input',
                            'allow_blank' => true,
                            'hash_function' => 'md5'));
}

参考にしたサイト