symfony – 単体テストで propel を使う方法

機能テスト(functional test) やバッチスクリプト(batch)で Propel を使う方法については以前こちらの記事

sfContext::getInstance();

を最初に実行すれば動作する、と書きました。
 
しかし単体テストで Propel を使う際は、単純に

require_once(dirname(__FILE__).’/../bootstrap/unit.php’);
sfContext::getInstance();

とするだけでは

Fatal error: Class ‘sfContext’ not found in …

のように、sfContext クラスが見付からずエラーになります。
ここで単純に sfContext を include しても、sfCore や myUser などが存在しないと言われたり、様々なエラーがでて動作しません。
前述の記事の参考URLにあるように、次のようなコードが必要になります(注: 参考元の投稿コードから僅かに手を加えています)。

// TODO: 要変更 – テスト対象のアプリケーション名
$app_name = “frontend”;
 
//begin initialise database code
include(dirname(__FILE__).’/../bootstrap/unit.php’);
include(dirname(__FILE__).’/../../config/config.php’);

require_once($sf_symfony_lib_dir.’/util/sfCore.class.php’);
sfCore::initSimpleAutoload(array(SF_ROOT_DIR.’/lib/model’ // DB model classes
                                ,$sf_symfony_lib_dir // Symfony itself
                                ,dirname(__FILE__).’/../../apps/stageselect/lib’ // Location app lib
                                ,SF_ROOT_DIR.’/plugins’)); // Location plugins
set_include_path($sf_symfony_lib_dir . ‘/vendor’ . PATH_SEPARATOR . SF_ROOT_DIR . PATH_SEPARATOR . get_include_path());
 
define(‘SF_ENVIRONMENT’, ‘test’);
define(‘SF_APP’, $app_name);
define(‘SF_DEBUG’, true);
 
sfCore::bootstrap($sf_symfony_lib_dir, $sf_symfony_data_dir);
sfContext::getInstance();
Propel::setConfiguration(sfPropelDatabase::getConfiguration());
Propel::initialize();
// end initialise database code
 
// 初期化終わり、以下テストコードが続く…
 
$lime = new lime_test();
 
// :

 
原因についてはエラーや対策のコードを見れば想像がつくと思いますが、単体テストでは機能テストと違い bootstrap (unit.php, functional.php)を include するだけで symfony プロジェクトをまるごと読み込んだり初期化しないようになっているためです。

symfony – propel でのリレーションの仕方

schema.yml:

Author:
  name: varchar(255)
Article:
  title: varchar(255)
  author_id:

のように、カラム名に <主テーブル名>_id という名前を付けると、参照テーブル(Article) には,主テーブル名(Author)への foreign key が作られ、 propel のモデルクラスでそれぞれのリレーションに対する専用のメソッドが提供されて簡単にリレーションができるようになります。
 
前述の Author, Article ならばモデル生成時に次のようなメソッドが自動生成されます。

class BaseAuthor {
    public function getArticles();
    public function addArticle($article);
}
class BaseArticle{
    public funtcion getAuthor();
    public function setAuthor($author);
}

この場合, addArticle() という名前のメソッドを呼び出すと、外部キーが適切に設定されるだけでなく、メソッドを呼んだオブジェクトが save() された場合に、このメソッドで引数にしてあるオブジェクト全てに対して save() を要求してくれます。複数指定可能なので set でなく add になっているわけです。
また、getArticles() メソッドを使うと、実行インスタンス(の元になるレコード)を参照している、 Article テーブルの全てのレコードの Article クラスインスタンスを取得できます。
 
逆に、参照元のテーブルである Article クラスのインスタンスから getAuthor() を呼ぶことで、そのインスタンスが参照している Author クラスのインスタンスを得ることができます。
 
add メソッドの利用例:

// 著者登録
$author = new Author();
$author->setName(“Ernest Miller Hemingway”);
 
// $author への作品登録
$article = new Article();
$article->setTitle(“The Old Man and the Sea”);
$author->addArticle($article);
 
// $autho への作品登録その2
$article = new Article();
$article->setTitle(“For Whom the Bell Tolls”);
$author->addArticle($article);
 
// 全て書き込み
$author->save();

結果:

Author:
id | name
1 | Ernest Miller Hemingway
Article:
id | name | author_id
1 | The Old Man and the Sea | 1
2 | For Whom the Bell Tolls | 1

参照例:

 
参照時には
参考:
Relationships(propel.phpdb.org)

symfony – sfBrowser メモ

一部ですが、sfBrowser で使えるメソッドをメモしておきます。
 
sfBrowser click(string $name [, array $arguments])
 $name で指定したボタンをクリックする。

<input type=”submit” value=”$name”>

 のように、value が $name で指定した値と一致するボタンをブラウザでクリックしたときの動作をエミュレートします。setField() とあわせて使います。
 
sfBrowser followRedirect()
 レスポンスヘッダの Location で指定されたURLにリダイレクトする
 
sfBrowser get(string $uri [, array $parameters])
 指定した URL へのアクセスをエミュレートします。
 例: モジュール foo のアクション index にアクセスします。

$browser->get(“/foo/index”);

 
sfWebResponse getResponse()
 HTTP レスポンスインスタンスを取得する。
 > echo $browser->getResponse()->getContent();
 とすれば、HTML ソースなどレスポンスの本体部分を表示できます。
 
sfBrowser setField($name, $value)
 フォーム入力のエミュレート。指定した名前の入力欄に $value の値を入力します。click() とあわせて使います。
 
参考:
タグで分類するシステムの作り方(symfonyで開発日記)
Class sfBrowser(symfony API)

文字列を比較演算子で比較したときの挙動

“abc” < "def"

のように、文字列を比較演算子(>, <, >=, <=)比較すると、辞書順での比較になります。
 
例:

if(“abc” < "bbb") echo "true";

結果:

true

 
ただし、両項が完全に数値とみなせる文字列であれば、数値同士の比較になります。

if(“100” < "0100.5") echo "true";

結果:

true

一文字であろうと数値としてみなせない文字が含まれると、文字列同士、つまり辞書順の比較になります。

if(“100a” < "0100.5"){ echo "true"; }else{ echo "false" }

結果:

false

symfony – YAML ファイルを配列に読み込む

array sfYaml::load($file)

を使うことで YAML -> array 変換をします。このクラスには array -> YAML 変換メソッドもあります。
複数の YAML ファイルを合成し配列化する

$files = array(‘file1.yml’, ‘file2.yml’, ‘file3.yml’);
$result = array();
$yaml = new sfYaml();
foreach($files as $file)
  $result = array_merge($result, $yaml->load($file));
var_dump($result);

symfony – No connection params set for propel

(この記事は symfony ver. 1.0.6 で確認したものです)
 
functional test で sfPropelData を使った DB へのテスト用データの投入をしようとしたら

PropelException: No connection params set for propel

なんて言われてしまった。
databases.yml は all で設定してるし、web 経由の DB アクセスでは問題ないのに。
 
対策を調べてみたところ、「lime unit-test with Propel: not working (symfony forum message #26072)」を参考に以下のようにすることで期待通り動作しました。

include(dirname(__FILE__).’/../../bootstrap/functional.php’); // functional test のお約束
 
sfContext::getInstance(); // ここが肝。
 
// データ投入部分
$data = new sfPropelData();
$data->loadDataFromArray(array(
   ‘UserTable’ => array(
       array(‘id’=>1, ‘name’=>’John Doe’, ‘age’=>25),
       array(‘id’=>2, ‘name’=>’Jane Doe’, ‘age’=>18),
   ),
));
 
// 以下テストが続く…

 
ただし、注意点として、挿入したデータは実際にデータベースに記録されるため、テストごとにデータをクリアする必要があります。
 
また、このエラーは batch (`symfony batch default appname` で作成したもの)でも同様に発生しましたが、同じ対策方法で対応できました。

symfony – sfDomCssSelector のセレクタの書式

object sfTestBrowser::checkResponseElement(string $selector [, mixed $value [, array $options]])

にも使われる、

array sfDomCssSelector::getTexts(string $selector)

の引数の文法について、symfony のドキュメントであまり触れられていないようなので、簡単にまとめてみました。
 
■基本要素(DOM Element)

<タグ名>

例: body タグの中身(<body>〜</body>)を取得する

$cssSelector->getTexts(“body”)

 
■属性指定要素

<タグ名>[<属性名1>=”<値1>“][<属性名2>=”<値2>“][…]

例: id が foo の div タグの中身(<div id=”foo”>〜</div>)を取得する

$cssSelector->getTexts(‘div[id=”foo”]’)

 
■要素の階層構造

<要素1> <要素2> <要素3> …

例1: (より正確に)ページタイトルを取得する

$cssSelector->getTexts(‘html head title’)

例2: フォームの投稿ボタンが「送信」という表示になっているか確かめる

$texts = $cssSelector->getTexts(‘form input[type=”submit”][value=”送信”]’);
if(count($texts) > 0){
  echo “フォーム中に <input type=”submit” value=”送信”> が存在します\n”;
}

ちなみに、sfTestBrowser::checkResponseElement() の第二引数を省略、あるいは bool 値にした場合はこの例のように存在の有無をテストします。
 
追記:
(2007-08-30) symfony WiKi にもセレクタの書式が書かれていました。
lime functional testing hints

symfony – sfTestBrowser の使い方(目的別)

■ フォワード(forward()) のテスト

object isForwardedTo(string $moduleName, string $actionName [, string $position])

モジュール $ModuleName のアクション $actionName に遷移したかをテストします
例: foo/index が boo/index にフォワードしたかを確認する

$t->get(“foo/index”)->isForwardedTo(“boo”, “index”);

 
■ リダイレクト(redirect()) のテスト

object isRedirected([boolean $boolean])

リダイレクトの要求があったかどうかをテストする。$boolean に false が指定された場合、失敗を期待する。

object followRedirect()

要求するリダイレクト先に遷移する。リダイレクト要求がないのに行った場合、例外が投げられる。
 
例: foo/index が boo/index にリダイレクト要求するかを確認する

$browser->get(“foo/index”)->
  isRedirected()->followRedirect()->
  isRequestParameter(‘module’, ‘boo’)->
  isRequestParameter(‘action’, ‘index’);

 
間違ったリダイレクトテスト方法:

object isResponseHeader($key, $value)

を使って

$browser->isResponseHeader(“Location”, “/module/action”)

とするのは間違いです。Location ヘッダは http://frontend-test/index.php/module/action のような絶対 URL になっている上、action が index の場合 action の指定が省略されるため、完全一致チェックの isResponseHeader() メソッドは不適です。

PHP Xdebug を Windows で使う

Xcache は pecl に登録されているのですが, windows だと

pecl install xdebug

としてもビルドできませんし、PHP 公式サイトの pecl モジュールのバイナリ版パッケージダウンロードにも含まれていません。
 
配布サイトを探したところ、Xdebug 公式サイトにはちゃんと Windows 版バイナリが配布されていました。
 
参考:
Xdebug Instration(xdebug.org)
Re: xdebug install error(xdebug-general ML)