symfony でのテスト(単体テスト/機能テスト)

symfony でのテストは機能テスト(functional test)、単体テスト(unit test)が標準でサポートされており、それぞれ以下の特徴があります。
 
□機能テスト
– モジュール生成時に test/functional に自動生成される。
– test/{$app}/{$module}ActionsTest.php というファイル名($app,$module はそれぞれテスト対象のアプリケーション名とモジュール名)
– テスト用仮想 symfony サイトブラウザ クラスの sfTestBrowser を使ったモジュール単位のテストが中心。
– 最初からデフォルトのテスト実装がある
  (ver 1.0.6 時点のデフォルトテスト実装は、モジュールの index アクションが初期のままではないかをテストするというもの)
– テスト実行は

symfony test-functional <appname>

  もしくは

symfony test-all

  で行う
 
□単体テスト
– 機能テストのような自動生成はされないが、test/bootstrap/unit.php をインクルードすることで容易にテストを作成可能。
– test/unit/{$testName}Test.php というファイル名($testName は任意の名前)
– 汎用単体テストクラス lime_test を使った、ライブラリ等のクラスを対象にした個別のテストが中心。
– テスト実行は

symfony test-unit

  もしくは

symfony test-all

  で行う
 
なお、単体、機能どちらのテストも

php test/functional/appname/fooActionsTest.php

のように PHP スクリプトとして実行すると、そのテストだけを実行することができます。
このようにすることで、失敗したテストがどの項目で失敗したかを調べることができます。
 
■sfTestBrowser クラス
sfBrowser クラスのテスト用実装版(sfBrowser の子クラス)。
sfBrowser クラスはTCP 等ネットワーク通信を伴わずに symfony サイト内ページのブラウジング可能な仮想ブラウザ実装です。
 
■lime_test クラス
lime の汎用単体テストクラス。
lime は symfony チーム開発のユニットテストユーティリティです。
 
備考:
 どちらのクラスについても、メソッドの戻り値いがオブジェクト自身のインスタンスを返してくれるので

$t->get(…)->is…(…)->is…(…)

のようにテストのチェインができるため、SimpleTest などの xUnit 互換のものより記述が容易になっています。
参考:
sfTestBrowser (symfony API)
sfBrowser (symfony API)
lime_test class
 (lime_test クラスの phpdoc ドキュメント)
新しく採用されたテスティングフレームワークlimeの紹介(symfony で開発日記)
 lime_test クラスメソッドの日本語説明など。
Chapter 15 – Unit And Fanctional Testing(The Definitive Guide to symfony)
15章 単体テストと機能テスト(上のリンクの日本語訳版)

APD の pprofp 引数一覧 (pprofp -h)

APD(Advanced PHP Debugger)、便利ですよね。
 
APD の使い方について書いてあるページは多々あれど、pprofp についてはあまり触れられていなかったので、手前味噌ですが pprofp -h を日本語訳+捕捉してみました。
 
書式(APD ver. 0.9):

pprofp <オプション> <解析するファイル>

 
並び順を指定するオプション:
  -a クラス、関数名のアルファベット順に表示する
  -l 関数の呼び出し回数順に表示する
  -m 関数呼び出し中のメモリ使用量順に表示する
  -r 関数の実際の処理時間(real time)順に表示する
  -R 関数の実際の処理時間順に表示する(子関数−関数内での関数呼び出し−の処理時間も含む)
  -s 関数のシステムモードでの処理時間(system time)順に表示する
  -S 関数のシステムモードでの処理時間順に表示する(子関数の処理時間も含む)
  -u 関数のユーザモードでの処理時間(user time)順に表示する
  -U 関数のユーザモードでの処理時間順に表示する(子関数の処理時間も含む)
  -v 関数の平均処理時間順に表示する
  -z 関数のユーザモード、システムモードの処理時間の合計順に表示する(省略時のデフォルト)
  
表示形式についてのオプション:
  -c 関数のコールツリーの横に実際の経過時間をつけて表示する(-t または -T とともに指定)
  -i PHP ビルトイン関数を出力に含めない
  -O <数値> 関数のリスト表示で最大何個の関数まで表示するか(デフォルトは 15 個)
  -t 関数のコールツリーを短縮して表示する
  -T 関数のコールツリーを短縮せず表示する
 
– – – –
捕捉:
処理を追う場合はコールツリー(-t あるいは -T)を使うと便利です。
-t と -T の違いは、
-T は

    doSomething
       define
       define
       define
       define
       echo

のように、同じ関数呼び出しが続いた場合もそのまま表示しますが、-t にすると

     doSomething
       define (4x)
       echo

のように、連続する関数呼び出しを一行に縮めて表示することで出力を短縮します。
 
参考:
PHP用デバッガ

PEAR – HTTP_Client で proxy を利用するには

HTTP_Request には setProxy() がありますが、HTTP_Client にはプロキシ指定メソッドが存在しません。
しかし、HTTP_Client のマニュアルには直接書かれていませんが、次のようにすることで HTTP_Client でも proxy を利用することができます。

$client =& new HTTP_Client(array(
    ‘proxy_host’ => $host, // proxy ホスト
    ‘proxy_port’ => $port, // proxy ポート(省略時 8080)
    ‘proxy_user’ => $user, // proxy の認証ユーザ名(省略時認証なし)
    ‘proxy_pass’ => $pass // proxy の認証パスワード(省略可)
));

内部的には第一引数が HTTP_Request のコンストラクタに渡され、HTTP_Request のコンストラクタで proxy を設定する、という流れになります。
参考:
PEAR::Manual – HTTP_Client のコンストラクタ
http://pear.php.net/package/HTTP_Request/docs/latest/HTTP_Request/HTTP_Request.html#methodHTTP_Request“>PEAR::Manual – HTTP_Request のコンストラクタ(API Docs)

symfony – Criteria で SQL の interval が使えない

日付範囲指定をするのに interval が使いたくて
Criteria のドキュメントやソースを追ったところ、
 
Criteria::CURRENT_DATE,
Criteria::CURRENT_TIME,
Criteria::CURRENT_TIMESTAMP
 
という ANSI SQL 関数は定義されていたのですが、SQL で日付の加減指定をする方法はないようです。
 
次のように

$c = new Criteria();
// 本日更新されたレコード
$c->add(FooTablePeer::UPDATED_AT,
    Criteria::CURRENT_DATE, Criteria::GREATER_EQUAL);

と単体で指定するのは有効ですが、

$c = new Criteria();
 // 過去 24 時間以内に更新されたレコードのつもり
$c->add(FooTablePeer::UPDATED_AT,
    Criteria::CURRENT_TIMESTAMP . ” – interval 1 day”,
    Criteria::GREATER_EQUAL);

のように、値に式を含めた場合、生成される SQL 文は

SELECT * FROM foo_table WHERE foo_table.updated_at >= ‘CURRENT_TIMESTAMP – interval 1 day’

と、エスケープ/クオート処理の対照になってしまい、望む結果を得ることはできません。
 
代替の方法として
A. Propel::getConnection() で得られる接続インスタンスに対して生のクエリを発行する
B. php の date(), time() 関数で日時を計算する
 
の二つの方法があります。
 
前者については Definitive Guide to symfony の8章の「生のSQLクエリを使う」の項を参照すると具体的な手順が書いてありますが、今回はデータベースサーバとWebサーバの時間差を気にしなくていい環境だったので、簡単に後者で対応しました。
 

$c = new Criteria();
// 過去 24 時間以内に更新されたレコード
$c->add(FooTablePeer::UPDATED_AT,
    date(‘Y-m-d H:i:s’, time() – 24 * 60 * 60),
    Criteria::GREATER_EQUAL);

symfony + Ajax で Helloworld

symfony で Ajax を使って HellowWorld を表示するまでの手順は以下のようになります。
(前提として Apache, PHP, symfony がインストールされているものとします)
 
1. まずは symfony のお約束、project, application, module 作成から。

$ pwd
 /usr/local
$ mkdir sf_ajax
$ cd sf_ajax
$ symfony new sf_ajax
$ symfony app helloworld
$ symfony module helloworld ajax

プロジェクト名、アプリケーション名、モジュール名は任意の名前で構いませんが、ここではそれぞれ sf_ajax, helloworld, ajax としました。
 
ここで、プロジェクトフォルダの web/ に sf ディレクトリがない場合は

$ cp -pr /usr/local/share/pear/data/symfony/web/sf ./web/

または

$ ln -s /usr/local/share/pear/data/symfony/web/sf ./web/

のように、symfony のインストールフォルダからコピーするかシンボリックリンクを張ります。
 
2. プロジェクトフォルダの web/ を web からアクセスできるようにします
(注: Apacheの Rewrite エンジンを使ってアプリケーション名を判別させる場合は web/ を DocumentRootにする必要があります)。
 

$ # /var/www が Apache の DocumentRoot の場合
$ ln -s /usr/local/sf_ajax/web /var/www/sf

 
この時点で

http://localhost/sf/helloworld_dev/ajax

(DocumentRootにした場合は http://localhost/helloworld/ajax )
のようにして web ページを開くと

Module “ajax” created

という symfony のデフォルトページが表示されるはずです。
 
3. ajax モジュールのアクションを編集する

$ cd apps/hello
$ cd apps/helloworld/modules/ajax/
$ vi actions/actions.class.php

 
actions.class.php: (編集前)

class ajaxActions extends sfActions
{
  /**
   * Executes index action
   *
   */
  public function executeIndex()
  {
   // $this->forward(‘default’, ‘module’);
  }
}

actions.class.php: (編集後)

class ajaxActions extends sfActions
{
  /**
   * Executes index action
   *
   */
  public function executeIndex()
  {
    // $this->forward(‘default’, ‘module’); ←コメントアウト
  }
  // ↓ 追加
  public function executeTime()
  {
    return $this->renderText(“Hello world!! (at “.date(“Y-m-d H:i:s”).”)”);
  }
}

 
4. ajax モジュールのテンプレートを編集する

$ vi templates/indexSuccess.php

 
indexSuccess.php:

<?php use_helper(“Javascript”);?>
<div id=”res”></div>
<?= link_to_remote(“Click!!”,array(‘url’=>”ajax/time”, ‘update’=>”res”,)) ?>

1 行目: symfony で Ajax を使うために JavaScriptHelper を読み込みます。
2 行目: 結果表示先のタグ。
3 行目: Javascript を実行するアンカータグを生成します。
       url: クリック時に HTTP リクエストを行うURLのモジュール以降の部分を指定します。GET パラメータを指定する場合は ajax/time?key=value のようにします。
       update: url で指定したページの読み込みが完了した際に表示先にするDOM の id を指定します。
       
 
ここで再び先程のページ(http://localhost/sf/helloworld_dev/ajax)を開き、 Click!! という文字をクリックすれば, 押すたびに

Hello, world!! (at 2007-07-05 23:30:12)

の日時の部分が更新されて表示されるはずです。
 
5. さらに link_to_remote() の引数の配列パラメータを追加することでよりインタラクティブな動作を実現できます。

<?php use_helper(“Javascript”);?>
<div id=”res”></div>
<div id=”error” style=”color:red”></div>
<div id=”loading” style=”display:none”>Now loading…</div>
<?= link_to_remote(“Click!!”,array(
    ‘url’=>”ajax/time”,
    ‘update’=> array( // update は失敗時と成功時で反映先を変えることができます
         ‘success’ => ‘res’, // HTTP コード 2XX
         ‘failure’ => ‘error’, // それ以外
       ),
    // 以下コールバックイベント指定
    ‘loading’ => “Element.show(‘loading’)”, // 読み込み開始時に実行する JavaScript
    ‘success’ => “Element.hide(‘loading’)”, // 読み込み結果 HTTP コードが 2XX の場合実行する JavaScript
    ‘failure’ => “Element.show(‘error’); alert(‘Error!!’)”, // 読み込み結果 HTTP コードが 2XX 以外の場合実行する JavaScript
    ‘complete’ => visual_effect(‘blind_down’, ‘res’, array(‘duration’=>0.5)), // 読み込みの成否にかかわらず、読み込み完了後(success,failure 判別,実行後)に実行する JavaScript
)) ?>

コールバックイベントには他にも before, after, loaded, interactive, 404 といったものがあります。詳しくはsymfony bookの Javascript ヘルパー – コールバックの項を参照してください。
  
参考:
symfony book 日本語ドキュメント Javascript ヘルパー
 Javascript ヘルパーの使い方について。
script.aculo.us Documentation
  symfony で利用している Javascript クラスライブラリ scriptaculous のドキュメントページ。symfony に最初から組み込まれているので、 use_helper(“Javascript”) とすればそのまま使えます。
symfony Code Snippets – ajax
symfony を使った実用ソースコード集。

GD でメモリエラー

GD でピクセルサイズの大きい JPEG や PNG などの画像を開こうとすると

Fatal error: Allowed memory size of 8388608 bytes exhausted (tried to allocate 6400 bytes) in …

のようにエラーが出ます。
例えばファイルサイズが 500KB 程度で php.ini の memory_limit に対して十分小さく見えても、ピクセルサイズが 1600×1200 のように極端に大きいとエラーになります。
 
原因は GD 内部でリソースとして読み込む際に、圧縮がとけるためのようです。
元画像のピクセルサイズが大きい程、読み込み時のサイズが大きくなる可能性が高くなるため、対策として php.ini で memoly_limit に十分な値を指定するだけでなく、GetImageSize() によりピクセルサイズをチェックするのが賢明です。

$size = filesize($file);
list($w, $h) = GetImageSize($file);
if($size > 1*1024*1024 || $w > 1200 || $h > 1200){
    throw new Exception(“ファイルサイズが大きすぎます”);
}

 
または、ImageMagick ならば同様のケースでもエラーなく処理ができるため、状況によって使いわけるといいでしょう。
 
参考:
BBS – PHP の基礎体力

symfony の基礎 – セッションとスコープ

Action クラスにて

スコープ

get 方法

set 方法

用途

インスタンス スコープ(ページ スコープ)

echo $this->foo;

$this->foo = “boo”;

View でローカル変数のように参照可能

リクエストスコープ

$this->getRequest()->getAttribute(“key”)

$this->getRequest()->setAttribute(“key”,$value)

リダイレクト時などアクション間でのデータ参照に利用

ユーザスコープ($_SESSION と同等)

$this->getUser()->getAttribute(“key”)

$this->getUser()->setAttribute(“key”, $value)

ショッピングカートなどページ遷移のあるトランザクションで利用

J2EE でのスコープと照らし合わせて symfony での変数(データホルダ)の種類をリストアップしました。
(アプリケーションスコープについては必要性が薄いので割愛)
 
参考:
パラメータホルダー(symfony book 日本語ドキュメント)

symfony – データベースを扱うには?

symfony ではデータベース処理に Propel を使っています。
次の手順を踏むことで、テーブルをオブジェクトとして扱うことが可能になります。
 
1. propel.ini の設定

<project_dir>/config/propel.ini

 

$ symfony propel-*-*

で利用するデータベース接続の設定.
propel.database.* の設定を変える以外はデフォルトでよい。

; unix socket 接続(TCP接続ならデフォルトにならえばよい)
propel.database.createUrl = mysql://root:dbpass@unix+/
propel.database.url = mysql://root:dbpass@unix+/dbname

 
2. databese.yml の設定

<project_dir>/config/database.yml

web,batch から利用する symfony の設定

all:
  propel:
    class: sfPropelDatabase
    param:
# dsn: mysql://root:dbpass@localhost/dbname
# unix socket 接続
        phptype: mysql
        hostspec:
        database: dbname
        username: root
        password: dbpass
        port:
        encoding: utf8
        persistent:

3-1. DB, テーブルを設定ファイルから新しく作る場合,
こちら(symfony で開発日記)を参考に、

config/schema.yml

を以下のように設定する

propel:
  table1:
  table2:
    _attributes: { phpName: FooTable }
    table1_id:
    name: varchar(255)
    amount:
       type: integer
       unsigned:true
       notnull:true
    created_at:
    updated_at:

説明..
  propel: … 接続名。
  table1 が最低限のテーブル構造例(id カラムのみ持つ)。
  table2 については以下のカラムを定義している
     id … int not null auto_increment primary key。デフォルトで定義される。
     table1_id .. int 型。 <table_name>_id とすることで table_name に Foreign key を張る
     amount … amount int unsigned not null というカラム定義。
     created_at … DATETIME 型。symfony がレコード作成日時を自動挿入してくれる
     updated_at … DATETIME 型。symfony がレコード更新日時を自動挿入してくれる。
  また、table2 はモデルクラスにマッピングする時(後述)に、
   __attributes: { phpName: FooTable } で指定した名前(FooTable)でマッピングされます。
 
3-2. propel.ini で設定した名前の空の DB を作成する。

symfony propel-build-db

 
3-3. schema.yml から CREATE TABLE の SQL 文を生成する

symfony propel-build-sql

 
3-4. (3-3) で生成した SQL 文を実行してテーブルを作成する。

symfony propel-insert-sql

 
Note: 3-1 とは逆に既存 DB のテーブルから schema.yml を作成するには

symfony propel-build-schema

を実行します。
 
4. symfony プロジェクトで利用できるように schema.yml からモデルクラスを生成する

symfony propel-build-model

ここで生成したモデルクラスは

<project_dir>/lib/model/

以下に作成されます。
 
5. (4)で作成したモデルクラスを使い、symofony プロジェクト内で参照する

apps/app_name/modules/model_name/actions/actions.class.php

等で以下のようにして参照します(require,include は不要)。

public function executeIndex()
{
   // 最も簡単なレコード挿入の例
    // 新規レコード用インスタンスを生成
    $table1 = new Table1();
    // データベースに実際にレコードを書き込む
    $table1->save();
 
   // カラムに値を設定して挿入する例
    $fooTable = new FooTable();
    
    $fooTable->setTable1Id(1); // カラム table1_id に 1 を指定
    $fooTable->setName(“milk”); // カラム name に milk を指定
    $fooTable->setAmount(123); // カラム amount に 123 を指定
    // 書き込む
    $fooTable->save();
 
   // プライマリキーからカラムを取得し、削除する例
    // プライマリキーカラム(id)が 1 のレコードのインスタンスを取得する
    $fooTable = FooTablePeer::retrieveByPK(1);
    if($fooTable){
        // id = 1 のレコードがあれば削除
        $fooTable->delete();
    }
     
    // 更新の場合, 既存レコードを取得して変更後 save() すればよい。
     $fooTable = FooTablePeer::retrieveByPK(1);
     if($fooTable){
         $fooTable->setAmount(10);
         $fooTable->save();
     }
    
    // プライマリキー以外からレコードを得る場合は *Peer::doSelect(), doSelectOne() を使う
     // 単純に SELECT した場合の一番始めのレコードを取得
     $fooTable = FooTablePeer::doSelectOne(new Criteria());
     
     // name が milk で, 作成日時の古い順に全てのレコードを配列で取得
     $c = new Criteria();
     $c->add(FooTablePeer::NAME, “milk”); // 条件
     $c->addAscendingOrderByColumn(FooTablePeer::CREATED_AT); // ORDER
     $tables = FooTablePeer::doSelect($c);
     
}

Criteria クラスの利用方法については
The Definitive Guide to symfony
8 章(日本語)の Criteria についての項に詳細に書かれています。
なお、Criteria クラスの実体は symfony インストールフォルダの

vendor/propel/util/Criteria.php

で定義されています。
  
参考:
symfony フレームワークテスト置場
  おおまかな手順、設定ファイルの設定の仕方、それぞれの意味など。
symfony book 日本語ドキュメント コマンドラインインターフェイス

 symfony propel-*-*

についてはこちらを参照。
schema.yml がどこまで楽させてくれるか(symfony で開発日記)
  schema.yml の書き方についてまとめられています。
MySQL4.1以降+symfonyのときのdatabase.yml
  MySQL4.1 以降では encoding: utf8 を指定しないと化ける、とのこと。

symfony で XCache の管理ページ認証が出る問題

XCache が有効な環境で symfony を使うと、時々(初回実行時やキャッシュクリア後等に)
XCache Administration というメッセージで HTTP 認証が出ます。
パスワードが違っていると勿論

XCache Auth Failed. User and Password is case sense

となります。
 
このままでは都合がとても悪いので原因と対策を調べてみたところ
symfony framework forum に、この問題についてコメントがありました。
http://www.symfony-project.com/forum/index.php/m/26145/

i tried XCache, APC and eAccelerator. What i discovered is, when the first hit occure (after symfony cc) there is a call for sfProcessCache::clear() in the checkConfig() method (line 201) of the sfConfigCache class. This happens even when using of ProcessCache is not enabled in the configuration.
 
All three of them require a user and password setting to use methods for clearing the cache. eAccelerator use ini settings, xCache require authentication. Thats bad, but not symfonys fault. The result is either a php ‘warning’ (eAccelerator) orthe authentication box popup (xCache) when you hit your app the first time.
 
I helped myself by rewriting the sfProcessCache::clear() and adding few lines. For eAccelerator i’m setting the proper ini settings right before the call of eA’s clearing method, and for xCache by setting $_SERVER[‘PHP_AUTH_USER’] and $_SERVER[‘PHP_AUTH_PW’] before calling xcache_clear_cache(). I read those settings out of the app config YAML, where i added them manually.

とのことで、
sfProcessCache::clear() で xcache_clear_cache() を呼ぶ際に xcache の管理権限が必要なことが原因のようです。
ここに書かれているように自分で

$_SERVER[‘PHP_AUTH_USER’] = ‘XCacheの管理ユーザ名’;
$_SERVER[‘PHP_AUTH_PW’] = ‘ XCache の管理パスワード’;

<pearディレクトリ>/symfony/cache/sfProcessCache.class.php の sfProcessCache::clear()

や、あるいは

<symfonyプロジェクトルート>/web/index.php

などに書き加える事で対処できます。
 
もう一つの解決方法として、XCache 1.2.1 RC1 からの新しい php.ini の設定で

xcache.admin.enable_auth = Off

とすれば XCache の管理認証なしで xcache のキャッシュクリアができるようになります。認証できなくして管理機能を使えなくするのではなく、認証なしで xcache の管理操作ができるようになる事に注意してください。
なお、セキュリティを考慮してなのか .htaccess で

php_flag xcache.admin.enable_auth off

としても無視されます。
 
– 前者はセキュリティ面で少しマシだが、管理面で手間
– 後者は XCache に関して内部のセキュリティを犠牲にするが、楽
 
なので、状況によって使い分けるのがよさそうです。

PHP5 MVC フレームワークの新スタンダード? symfony

http://www.symfony-project.com/
 
本日の「今更」その2。
 
CakePHPMojavi より symfony がいいかも。
 
と聞いていたので、とりあえずsymfony のインストールだけ。

$ pear channel-discover pear.symfony-project.com
$ pear install symfony/symfony

インストールの確認

$ symfony -V

と打てば

symfony version 1.0.3

のようにインストールしたバージョンが表示されます。
楽ちん。
 
なお、PHP ソースの configure 時に –enable-ctype をつけていないと
いざ使おうとした時に

PHP Fatal error: Call to undefined function ctype_alpha()

のようにエラーがでてしまいます。
(Windows の場合は ctype は標準で組み込まれているので問題ありません)
 
参考:
symfony.jp symfony の日本語情報 WiKi
CodeZine: symfonyで始めるPHPフレームワーク