* error_reporting=E_ALL のススメ[PHP]
初歩ではありますが、error_reporting = E_ALL ^ E_PHP_NOTICE な環境に慣れている人のためのコーディングルールを探してもすぐに見つけられなかったため、書いておきます。
□変数は代入してから使うこと
定義済みの(値が入っている)変数のみ参照するべきです。
もしもグローバル変数など、値が入っているかわからない場合は isset() や empty() で存在を確認しましょう。接頭文字 @ を使ってもいいですが、 isset() や empty() のほうが望ましいです。
なお、null を代入した変数も定義済みの変数です。
<?php
// -----------------
// 未定義の変数参照
// -----------------
// NG
echo $foo; // PHP Notice: Undefined variable: foo
// --------
// OK
$foo = "abc";
echo $foo;
□配列、連想配列の要素も存在するもののみ参照すること
配列の要素の一つ一つについても、変数同様に定義済みのもののみ参照しましょう。
リクエスト値など、値が入っているかわからない場合は isset() や empty() で存在を確認しましょう。
<?php
// -----------------
// 未定義の配列要素/連想配列要素の参照
// -----------------
// NG
$id = $_GET['id']; // PHP Notice: Undefined index: id (引数省略リクエストの場合)
// --------
// OK ※省略不可能なパラメータの場合
if(empty($_GET['id'])){ // または if( !isset($_GET['id']) )
// エラー処理
exit;
}
$id = $_GET['id'];
// --------
// OK-2 ※省略可能なパラメータの場合
$id = isset($_GET['id'])? $_GET['id']: null;
□定数の定義方法に注意しましょう。
定数の定義関数は
define(定数名文字列, 値)
です。
第一引数はシンボルではなく文字列です。'〜' や "〜" で括ることを忘れないようにしましょう。
<?php
// -----------------
// 未定義の定数参照(定義時)
// -----------------
// NG
define(FOO, 123); // PHP Notice: Use of undefined constant FOO - assumed 'FOO'
// --------
// OK
define("FOO", 123);
□文字列中の連想配列のキー指定時もクォーテーションにも注意
"{$assoc[key]}"
は key が定数値とみなされます。
連想配列のキーをダブルクォーテーションまたはシングルクォーテーションで囲むようにしましょう。
<?php
// -----------------
// 未定義の定数参照(文字列内での連想配列参照)
// -----------------
// NG
$foo=array();
$foo["name"] = "John";
echo "{$foo[name]}\n"; // Use of undefined constant name - assumed 'name'
// --------
// OK
$foo=array();
$foo["name"] = "John";
echo "{$foo['name']}\n";
* .htaccess で PHP の表示言語を切り替える[Apache][PHP]
php_value, php_flag を使って自動文字コード変換をする tips です。
.htaccess:
# ソースの文字コード(SJIS, EUC-JP, UTF-8 など mb_convert_encoding()で指定する書式)
php_value mbstring.internal_encoding UTF-8
# ヘッダで表明する出力文字コード(Shift_JIS, EUC-JP, UTF-8 など Content-Type で指定する書式)
php_value default_charset Shift_JIS
# 変換に使う出力文字コード(mb_convert_encoding()で指定する書式)
php_value mbstring.http_output SJIS
# 変換文字コードが属する言語
php_value mbstring.language Japanese
# フォームなどの入力文字コードは自動判別させる
php_value mbstring.http_input auto
# 入力の自動変換を有効化(Off で無効化)
php_flag mbstring.encoding_translation On
# 出力の自動変換を有効化(空値で無効化)
php_value output_handler mb_output_handler
なお、動作には
1.CGI モードではなく PHP-CLIモードで動いていること
2. httpd.conf で AllowOverride Options または AllowOverride All など Options の変更ができるように設定されていること。
が必要です。
<?php
// 自動変換処理
mb_language("Japanese");
// ソース文字コード
mb_internal_encoding("UTF-8");
// 出力文字コード
mb_http_output("SJIS");
header("Content-Type:text/html;charset=Shift_JIS");
// mb_output_handler で出力をフィルタリング
ob_start("mb_output_handler");
register_shutdown_function('ob_end_flush');
// 入力をフィルタリング
// (mb_convert_encoding($value, mb_internal_encoding(), "auto, UTF-8");
// を使い、 $_REQUEST, $_GET, $_POST, $_COOKIE など入力変数を変換すればエミュレートできるが
// 長くなるので省略)
// -----------------------------
// あとは普通の処理
echo "はろーわーるど\n";
参考:
AllowOverride ディレクティブ(Apache 2.2 リファレンス)
PHP 出力制御関数(php.net)
* PHP の 5.2.5 が mysql 5.1.23-rc と非互換な件[PHP]
先日 PHP 5.2.5 をビルドしようとしたところ
/usr/src/php-5.2.5/ext/mysqli/mysqli_api.c: In function
'zif_mysqli_change_user':
/usr/src/php-5.2.5/ext/mysqli/mysqli_api.c:420: error: 'NET' has no
member named 'last_errno'
/usr/src/php-5.2.5/ext/mysqli/mysqli_api.c:420: error: 'NET' has no
member named 'last_errno'
/usr/src/php-5.2.5/ext/mysqli/mysqli_api.c:420: error: 'NET' has no
member named 'last_error'
/usr/src/php-5.2.5/ext/mysqli/mysqli_api.c: In function
'zif_mysqli_kill':
以下略
というエラーが出てコンパイルができなかった。
調べたら php bug に投稿がありました。
http://bugs.php.net/bug.php?id=44137
5.1.22-rc ならいけるのに MySQL 5.1.23-rc だとダメらしい。
そして開発チームの回答。
[18 Feb 7:41pm UTC] uw@php.net
A little more info: we are discussing this internally at MySQL. Its
possible that the change which has caused this (unintended) problem will
be reverted soon.
[24 Feb 1:01am UTC] jani@php.net
And it's not PHP problem but Mysql problem since they broke the BC in
their libs/headers. :)
[24 Feb 1:03am UTC] jani@php.net
And please, in the future don't report bugs caused by some 3rd party
library here which isn't even released as stable yet.
曰く、
「mysql が悪い。こんな(不本意な)問題を引き起こしてくれる変更は mysql 開発側がじきに元に戻してくれるかもしれないからそれを待て。
そもそもサードパーティのライブラリに安定リリース版じゃないのを使ってるのはサポート外だから報告しないでね」
っていうことだそうで。
…とはいっても、FreeBSD ports の databases/mysql51-client (実体は mysql51-server) が 5.1.23-rc なのが悩ましいところです。
* セッションフィクセーション問題[PHP]
ITpro の記事で Session Fixation 問題について書かれていたので PHP での対策例を書いておきます。
PHP ではこの問題を解決するために、session_regenerate_id() という関数が提供されています。
<?php
session_start();
$old = session_id();
session_regenerate_id();
$new = session_id();
echo "$old -> $new<br>";
// 結果:
// d4aa3b6cfaedca70dc09262dfbf8c39e -> 53c031b85fe89e00d3b151eddfd91733
具体的な利用方法はこんな感じです。
<?php
session_start();
$algo = "sha256";
// パスワードは生で保存せずハッシュで持つ(5.1.2 以前は hash() 関数を使えないため mhash() または md5() で代用する)
$password_hash = "d74ff0ee8da3b9806b18c877dbf29bbde50b5bd8e4dad7a3a725000feb82e8f1"; // hash($algo, "pass");
$bAuthorized = !empty($_SESSION['authorized']);
if(!$bAuthorized){
if(empty($_GET['p']) || hash($algo, $_GET['p']) !== $password_hash){
// ログインページ表示
?>
未認証(session_id = <?=session_id()?>)<br>
<form action="" method="GET">
<input type="password" name="p">
<input type="submit" value="ログイン">
</form>
<?php
exit;
}
$bAuthorized = $_SESSION['authorized'] = true;
// セッションフィクセーション対策(Session Fixation)
session_regenerate_id(); // ログインが成功した段階でセッションID を更新垢
}
?>
認証済み(session_id = <?=session_id()?>)
このサンプルを実行すると、パスワードに "pass" という文字列を入れて認証すると session_id が書き換わるのを確認できます。
この関数を使うと、透過的にセッションIDが書き換わるので関数呼び出し側はセッションフィクセーション対策を簡単に施すことができます。ただし当然ですが既に何かを
* tips - Net_URL で URL を解決する[PHP]
相対 URL から絶対 URL に変換するには PEAR::Net_URL が利用できます。
URL の要素を分解して取得することができるため、便利そうです。
インストール:
$ pear install Net_URL
HTTP_Request が依存しているので、ネットワーク関係の PEAR パッケージを入れたことがあれば既に入っていることが多いです。
例:とりあえず使ってみる
<?php
// http://example.com/foo/boo/file.php で実行する
require_once "Net/URL.php";
$url =& new Net_URL("next.html");
echo "<PLAINTEXT>";
var_dump($url);
echo $url->getURL();
?>
例: GET クエリで渡された URL が、自サービス内 URL かどうかを調べる
※外部サイトへの踏み台になるのは防ぎますが、内部のリファラチェック回避の踏み台にはなり得る事に注意。
$url =& new Net_URL($_GET['url']);
if($url->host != $_SERVER['SERVER_NAME']){ // 相対パスまたは同じドメインの絶対URLなら HTTP_HOST や SERVER_NAME と一致する。
echo "error!";exit; //
}
echo "ok";
参考:
PEAR::Net_URL(pear.php.net)
* デッドロックのテスト用コード[PHP]
test_lock.php:
<?php
error_reporting(E_ALL);
$f = array();
$f[0] = null;
$f[] = fopen("lock_1", "r+");
$f[] = fopen("lock_2", "r+");
if(@intval($_SERVER['argv'][1]) >= 2){
$task = array(2, 1);
}else{
$task = array(1 ,2);
}
foreach($task as $n){
echo "Locking $n ... ";
echo flock($f[$n], LOCK_EX, $t=true)? "OK": "Fail";
echo "\n";
echo "waiting some seconds...";
sleep (5);
echo "OK\n";
}
echo "Finished!\n";
実行:
$ php test_lock.php &
$ php test_lock.php 2 &
ひとつのみ起動した場合 Finished! と表示されますが、上のように並列で実行した場合は正しく動作しなくなります。
手元の環境では FreeBSD 6.2 と Windows XP SP2 の場合はデッドロックして固まり、Linux 2.4.21-50.ELsmp の場合は 2番目のロックに fail し、デッドロックにはなりませんでした。
参考:
- flock のモード切替え時の挙動は環境によって異なる[2008-01-08-1]
* flock のモード切替え時の挙動は環境によって異なる[PHP]
flock() でロック獲得中にロック状態を切り替える(LOCK_SH から LOCK_EX にする、など)と、環境や状況によってロックが保持されない場合があります。
検証用コード:
A... ロック切り替えプロセス(keeper.php )
<?php
$f=fopen("t.txt","r+");
for($i = 0; $i < 10; $i++){
// ロック切り替えまたは取得(LOCK_EX -> LOCK_SH)
echo "shared($i)\n";
flock($f, LOCK_SH);
sleep(3);
// ロック切り替え(LOCK_SH -> LOCK_EX)
echo "exclusive($i)\n";
flock($f, LOCK_EX);sleep(3);
}
echo "Done! the treasure was protected!\n"; // ここまで theif.php にロックがとられなければ期待通り。
?>
B...ロック奪取プロセス(thief.php)
<?php
$f=fopen("t.txt", "r+");
flock($f, LOCK_EX); // ロック待ち
// ロック奪取成功
echo "lock obtained!\n";
echo "im doing something, hehehe.";
for($i=0;$i<10;$i++){
sleep(1);
echo ".";
}
echo "\nfinished! bye ;P\n";'
fclose($f); // ロック開放
?>
実行:
$ touch t.txt
$ nice php locker.php &
$ php thief.php
結果:
Windows XP SP2 + Cygwin + PHP 5.1.2 (FS: NTFS) ... ロック保持されない(thief won)
Linux 2.4.21-50.ELsmp + PHP 4.4.6 (FS: ext3) ... ロック保持される(keeper won)
FreeBSD 6.2 + PHP 5.2.5 (FS: ufs) ... ロック保持される(keeper won)
Windows XP (FS: NTFS) はロック自体はできますが、LOCK_EX <-> LOCK_SH の切り替えでいったんロックが解除されるという挙動になりました。
なお、PHP マニュアルの flock() の項にもあるとおりファイルシステムが FAT の場合など同じ Windows でも環境によっては結果が変わるため、この結果を鵜呑みにせず必ずご自身の環境でテストしてください。
* CVS 入出力 を支援する PEAR パッケージ[PHP]
PHP5.1 以降には fgetcsv(), fputcsv() という関数が標準で提供されていますが、別の選択肢として PEAR でも File_CSV というクラスが提供されています。
このクラスは File パッケージに含まれています。
pear install File
■利用例(tests/parser.php を改変したもの)
ファイルから読み込む
<?php
require_once 'File/CSV.php';
$file = "test.csv";
$conf = File_CSV::discoverFormat($file); // ファイルからデータ構造を判別(区切文字やクォーテーションなど)
while ($fields = File_CSV::read($file, $conf)) { // 取得したデータ構造を利用して行単位で読み取る
print_r($fields);
}
ファイルから配列に読み込む
<?php
require_once 'File/CSV.php';
$file = "test.csv";
$conf = File_CSV::discoverFormat($file); // ファイルからデータ構造を判別(区切文字やクォーテーションなど)
while ($fields = File_CSV::read($file, $conf)) { // 取得したデータ構造を利用して行単位で読み取る
print_r($fields);
}
CSV ファイルの中身をすべて配列として読み込む
<?php
require_once 'File/CSV.php';
$file = "test.csv";
$conf = File_CSV::discoverFormat($file); // ファイルからデータ構造を判別(区切文字やクォーテーションなど)
while ($fields = File_CSV::read($file, $conf)) { // 取得したデータ構造を利用して行単位で読み取る
print_r($fields);
}
ファイルから配列に読み込む
<?php
require_once 'File/CSV.php';
$file = "test.csv";
$conf = File_CSV::discoverFormat($file); // ファイルからデータ構造を判別(区切文字やクォーテーションなど)
$data = File_CSV::readAll($file, $conf); // 取得したデータ構造を利用し、配列として全て取得する。
var_dump($data);
<<
ファイルに書きこむ例:
<?php
require_once 'File/CSV.php';
$file = "test.csv";
// 書き込む内容
$data = array(
array("foo", "boo", "woo"),
array("fo\"o", "bo,o", "woo"), // エスケープが必要な文字を含むレコード
);
// CSV 書式
$conf = array(
'fields' => count($data[0]), // カラム数
'sep' => ",", // 区切り子
'quote' => '"', // 囲み文字
// 'header' => false, // NULL 以外を指定すると, read 時に一行目をヘッダとみなして各要素のキーとして扱う
);
// 書きこみ実行
foreach($data as $row) {
File_CSV::write($file, $row, $conf); //
}
ファイルの書きこみ制御ができなかったり、設計上気になる点もありますが、
単純に読み書きするのには十分そうです。
[ 固定リンク ]
* 文字列への文字単位のアクセスで波括弧が非推奨にかわった[PHP]
ふと PHP のドキュメントを眺めていたら、次のように書かれていました
PHP: 文字列(English)
注意: $str{42} のように波括弧を使用してアクセスすることも可能です。 しかし、角括弧を使用する方法のほうが推奨されます。 なぜなら、{波括弧} 形式は PHP 6 で廃止される予定だからです。
Note: They may also be accessed using braces like $str{42} for the same purpose. However, using square array-brackets is preferred because the {braces} style is deprecated as of PHP 6.
PHP5 リリース前は
$str = "foo"; echo $str[0];
が非推奨で
$str = "foo"; echo $str{0};
が推奨されていましたが、方針転換をしたようです。
参考:
- 2004 年 3 月のマニュアル同項(English)(archive.org)
波括弧の後に任意の文字をゼロから始まるオフセットで指定することに より、文字列内の文字にアクセスすることが可能です。
注意: 過去の互換性のため、配列括弧を使用することが可能です。しかし、 この構文はPHP 4に依存しています。
Characters within strings may be accessed and modified by specifying the zero-based offset of the desired character after the string in curly braces.
Note: For backwards compatibility, you can still use array-braces for the same purpose. However, this syntax is deprecated as of PHP 4.
以前はこうなっていました。
* バグパターン - switch のデータ型は複数種類にすると予期しない結果になる[PHP]
まずはサンプルコードを見て、結果を予想してみてください。
<?php
$result = true;
switch($result){
case "error": // エラー
echo "failure";
exit;
case "ok": // 成功
case true: //
echo "success";
exit;
default:
echo "unexpected";
exit;
}
?>
一見、このコードを実行すると success が出力されるように思いますが、実際に実行してみると意外なことに failure が出力されてしまいます。
PHP の switch 文は、switch の引数と case で指定した値とを= ではなく) でチェックしているため、switch 引数が複数の種類の型になる場合は型変換などにより予想外の値と一致することになってしまうのが原因です。
実際どの値が何と一致するかについては以下の検証コードを実行してみると分かります。
// 検証コード:
<?php
// values to test
$targets = array(
0,
1,
true,
false,
null,
"",
"0.00e5",
"10f",
"0f",
array(),
array(1),
array("key"=>"foo"),
);
// ----
foreach($targets as $target){
if(is_bool($target))
$s = $target? "true": "false";
elseif(is_null($target))
$s = "null";
elseif(is_string($target))
$s = "'{$target}'";
else
$s = $target;
echo $s ."(".gettype($target).")".":\t";
switch($target){ //"0.00e5"
case 'a': echo "case a"; break;
case 'b': echo "case b"; break;
case '0': echo "case zero"; break;
case 10: echo "case int 10"; break;
case 0: echo "case int 0"; break;
default: echo "default"; break;
}
echo "\n";
}
php 4.4.6 および php 5.2.3 で試したところ、実行結果次のようになりました。
0(integer): case a
1(integer): default
true(boolean): case a
false(boolean): case zero
null(NULL): case int 0
''(string): case int 0
'0.00e5'(string): case zero
'10f'(string): case int 10
'0f'(string): case int 0
Array(array): default
Array(array): default
Array(array): default
同じ empty 値でも 0, null, false, "" で結果が全く異なってしまいました。
また、 == について挙げられる数値文字列の比較時の問題("0.00e5" == "0" が真になる,など)の影響も受けてしまっています。
この問題は、型チェックを厳密にしたり、strval(), intval() などで明示的に文字列,数値キャストを行い、switch 文の引数の型が一種類になることを保証することである程度回避できます。
しかし依然として数値として有効な文字列同士の比較の問題があるため、数値を含む文字列について正確さが必要な場合や、本当に複数型を判別させたい場合は= 演算子を使い if - elseif - else を使うほうが賢明でしょう。
if($result = true || $result= "ok"){= "error"){
echo "success";
}elseif(empty($result) || $result
// empty($result) は以下の式と等価。
// ( !isset($result) || ($result= false || $result= null ||
// $result= "" || $result= 0 )
echo "failure";
}else{
echo "unexpected";
}
参考:
- switch 構文(php.net)
- 比較演算子(php.net)
- strval()(php.net)
* サンプルコード - Cache_Lite のファクトリメソッド(マルチユーザ対応)[PHP]
Cache_Lite は大変便利なのですが、Linux/BSD などで、複数実行ユーザが同じ cacheDir (たとえば /tmp/ )を使った場合に、他ユーザが作ったキャッシュへのアクセス権限が得られないため正しく動作しません。
Cache_Lite の hashedDirectoryUmask オプションと umask() 関数で 0777 を指定すれば十分な権限が得られますが、キャッシュの盗聴やかいざんの可能性が出てくるためセキュリティ的によろしくありません。
簡単な対応として思い付くのは
'cacheDir' => '~/tmp',
のようにユーザのホームディレクトリにフォルダを分けてやることですが、
今回はより汎用的にするため、ファクトリメソッド内部でユーザ判別をしてフォルダを分けることでマルチユーザ対応させてみました。
# 暫定的な実装なので、不備があるかもしれません。
// This source is made available under the terms of the BSD license.
/**
* Cache_Lite 用汎用ファクトリメソッド。
* (PHP 4, 5 対応)
* @version 2007-11-08
*/
class CacheLiteFactory {
/**
* @access private
*/
function CacheLiteFactory(){
// no-op
}
/**
* static.
* @access private
* @param array $newOptions
* @return options
*/
function _defaultOptions($newOptions = null){
// 初期デフォルト設定
static $defaultOptions = array(
// 必要なら設定可。
);
if(!empty($newOptions)){
$defaultOptions = $newOptions;
}
return $defaultOptions;
}
/**
* インスタンス化に使うデフォルトのオプションを設定します。
* 過去に設定していたオプションは全て消去されます。
* 一部のオプションのみ追加、変更したい場合は getDefaultOption() で現在のデフォルトオプションを取得して再設定してください。
*
* static
* @access public
* @param array $newOptions 置き換えるオプション
* @return void
*/
function setDefaultOptions($newOptions){
CacheLiteFactory::_defaultOptions($newOptions);
}
/**
* インスタンス化に使われるデフォルトのオプションを取得します。
* static
* @access public
* @return array 現在設定されているデフォルトのオプション
*/
function getDefaultOptions(){
return CacheLiteFactory::_defaultOptions();
}
/**
* Cache_Lite インスタンスを取得する。
* 引数を指定した場合、デフォルトオプションをベースに引数のオプションを追加,変更します。
*
* static
* @access public
* @param array $overrideOptions 上書きするオプションの連想配列
* @return Cache_Lite
*/
function factory($overrideOptions = array()){
require_once "Cache/Lite.php";
// デフォルト値。
$options = CacheLiteFactory::getDefaultOptions();
if($overrideOptions && is_array($overrideOptions)){
$options = array_merge($options, $overrideOptions);
}
if(empty($options['cacheDir'])){
$options['cacheDir'] = "/tmp/";
}
// 他ユーザのキャッシュはアクセス権限が十分にない(umask 0777 にしても削除はできない)ので
// posix_getuid() で uid が取得できる場合はユーザごとにフォルダを分けるようにしておく。
// ※ Windows 環境または --disable-posix オプション付きでビルドした場合は取得できないことに留意
if(function_exists("posix_getuid")){
if(@mkdir($options['cacheDir'], 0777)){ // PHP4 互換のため recursive なし
@chmod($options['cacheDir'], 0777);
}
$options['cacheDir'].="cache_uid_".posix_getuid().DIRECTORY_SEPARATOR;
}
@mkdir($options['cacheDir']); // PHP4 互換のため recursive はなし
return new Cache_Lite($options);
}
}
// 用例
/* ---
// デフォルトを与えるかは任意
CacheLiteFactory::setDefaultOptions(array(
// 'cacheDir'=>'/path/to/tmp/',
'lifeTime' => 24 * 60 * 60, // set 1 day by default
'hashedDirectoryLevel' => 1,
'automaticCleaningFactor'=>100,
));
--- */
$cache = CacheLiteFactory::factory();
$value = $cache->get("foo");
if($value === false){
$value = date('Y/m/d H:i:s');
$cache->save($value);
}
echo $value . "\n";
* PHP4 と PHP5 の Sigleton Pattern[PHP]
PHP4 での Singleton
error_reporting(E_ALL);
class Foo {
function &getInstance(){
static $_singleton;
if(empty($_singleton)){
$_singleton = new Foo();
}
return $_singleton;
}
var $_n;
function add(){
echo (++$_n)."\n";
}
}
$o1 =& Foo::getInstance();
$o1->add(); // 1
$o2 =& Foo::getInstance();
$o1->add(); // 2
$o2->add(); // 3
$o1->add(); // 4
& と static の使い方が肝です。
static キーワードは、指定したローカル変数のライフサイクルを延長するためのキーワードです(注1)。
メソッド定義とメソッド呼び出しの両方に & を付けないと、参照でなくコピーになってしまい、個別のインスタンスになるために結果が 1,2,1,3 になってしまいます。
PHP5 の場合も上のコードを変更なしで Singleton Pattern として動作させることが可能です。
さらに、メソッドの戻り値が標準で参照になるため、& を外しても singleton を実現することができ、クライアントコードが誤用する可能性がなくなったといえます(注2)。
このように PHP4 ではクライアントに特殊なメソッド呼び出し方をしないと意図しない結果になるため、Singleton な動作が必要な箇所については極力非公開にして内部利用にとどめるほうが賢明かも、です。
注1:
static キーワードには、スコープ(可用範囲)を広げる効果はありません。
ローカル変数がフィールドになるわけではないため、異なるメソッドで同じ名前の変数を指定しても、別個の変数になります。
注2:
ただし、クライアントコードで
$o1 = clone Foo::getInstance();
のように clone を使って意図的にコピーを行った場合、PHP4 で & を付けなかったときと同様の結果になります。
この場合はクライアント側で結果が予測できるため、問題にはならないでしょう。
参考:
- PHP4 でデザインパターン(Do You PHP?)
* symfony - Propel を仲介せずに databases.yml に書かれた設定や DB接続インスタンスを得る[PHP]
sfDatabaseManager クラスを使うことで Propel より低いレイヤーでデータベースの情報を得たり、(Creaole による)データベース接続を行うことができます。
この方法を使うと databases.yml の設定を一時的に変更した上でデータベースアクセスができるようになります。
$manager = new sfDatabaseManager();
$manager->initialize(); // インスタンスに database.yml ファイルを読み込んで初期化する。
$db = $manager->getDatabase("propel"); // "propel:" 以下を参照し、sfDatabase のインスタンスを得る。 引数を省略すると default: を参照する
// database.yml の設定を参照する例:
echo $db->getParameter("phptype")."\n"; // propel: param: phptype: を参照("mysql" など)
echo $db->getParameter("host")."\n"; // propel: param: host: を参照(localhost など)
echo $db->getParameter("database")."\n"; // propel: param: database: を参照
echo $db->getParameter("username")."\n"; // propel: param: username: を参照
// databases.yml の設定を一時的に上書きする例:
$db->setParameter("database", null); // 特定のデータベースに接続しない。
// (データベースが存在しなくてもエラーにならないため
// CREATE DATABASE したい場合やテーブルを一切利用
// しない場合などに有用)
// データベース接続の例:
// databases.yml の設定をもとに Creole のデータベース接続インスタンスを得る
// (vendor/creole/Connelction.php で定義される Connection 抽象クラスのインスタンス)
$con = $db->getConnection();
$rs = $con->executeQuery("SELECT CURRENT_TIMESTAMP AS time"); // クエリを発行。
while($rs->next()){ // ポインタを進める(複数レコード対応)
$row = $rs->getRow(); // レコードを連想配列で取得
echo $row["time"];
}
参考:
- Class sfDatabaseManager(symfony API)
sfDatabaseManager のクラス定義。
- Class sfDatabase(symfony API)
sfDatabaseManager::getDatabase() で得られるインスタンスの定義。
- SymfonyAPI - Connection Interface Reference(cpr.in-berlin.de による symfony の phpdoc)
sfDatabase::getConnection() で得られるインスタンスの定義。
- SymfonyAPI - ResultSet Interface Reference(同上)
Connection::executeQuery() で得られるインスタンスの定義。
* symfony - 単体テストで propel を使う方法[PHP]
機能テスト(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 でのリレーションの仕方[PHP]
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 メモ[PHP]
一部ですが、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
最終更新時間: 2009-01-07 06:15