PHPでmysql_queryの結果オブジェクトは、mysql_fetch_row()しても良いですが、mysql_fetch_array()した方が便利ですよね。
※mysql_fetch_row()では、結果を通常の配列で取得しますが、mysql_fetch_array()では、カラム名をキーにした連想配列で取得できます
EthnaでAppObjectを使わずにEthna_DBを直接たたく場合、結果オブジェクトはPEAR::DB_Resultオブジェクトです。
これはfetchRow()のメソッドを持ちますが、fetchArray()は存在しません。
http://pear.plus-server.net/package.database.db.db-result.fetchrow.html
カラム名をキーにした連想配列が欲しい場合、queryを投げる前にsetFetchMode(DB_FETCHMODE_ASSOC)を実行しておく必要があります。
http://pear.plus-server.net/package.database.db.db-common.setfetchmode.html
EthnaのデフォルトDBアダプタ、DB_PEARは、内部にPEAR::DBを持っているので、以下のようなコードを書けばOKです。
$db = $this->backend->getDB();
$db->db->setFetchMode(DB_FETCHMODE_ASSOC);
$res = $db->query($sql);
print_r($res->fetchRow());
※http://ethna.jp/ethna-document-dev_guide-db.htmlに
Ethnaの デフォルトの DB接続クラス(Ethna_DB_PEAR) はPEAR::DBを継承しているので
と書いてありますが、継承ではなくて委譲なので、$db->db->setFetchMode()となります。
PHPのfputcsv関数は便利です。
しかし、以下のような点が不便です。
エンコーディングを変換できない
主にCSV出力の目的になるExcelでは、Shift-JIS以外のCSVを開けません。
Excel 2007ならBOMを付けるとか、データインポートで実行するといった回避策はありますが、日本語のみならShift-JISで出力したいことが多いと思います。
このためにはあらかじめShift-JISに変換しておく必要があり、
$sjis = array_map(create_function(’$str’, ‘return mb_convert_encoding($str, “Shift-JIS”, “UTF-8″);’), $line);
といった処理をやっていましたが、正直めんどうです。
ダブルコーテーションのエスケープにバグがある
カンマが含まれる場合、ダブルコーテーションで囲ってくれます。
ダブルコーテーションが含まれる場合、ダブルコーテーションを二重にしてエスケープしてくれます。
しかし、\” (バックスラッシュ+ダブルコーテーション)があった場合、なぜかこの部分のダブルコーテーションは二重にしてくれません。
ということで、結局自前でやった方が便利です。
mb_str_replaceは、http://fetus.k-hsu.net/document/programming/php/mb_str_replace.htmlを使わせて頂きました。
function _fputcsv($fp, $data, $toEncoding='Shift-JIS', $srcEncoding='UTF-8') {
require_once 'mb_str_replace.php';
$csv = '';
foreach ($data as $col) {
if (is_numeric($col)) {
$csv .= $col;
} else {
$col = mb_convert_encoding($col, $toEncoding, $srcEncoding);
$col = mb_str_replace('"', '""', $col, $toEncoding);
$csv .= '"' . $col . '"';
}
$csv .= ',';
}
fwrite($fp, $csv);
fwrite($fp, "\r\n");
}
ちゃんとテストしていないのでミスがあるかもしれませんが、このくらいシンプルで十分そうですね。
1年くらい前に社内MLに投げた、「MySQLでSET NAMESを使ってはいけない理由」をコピペしてみます。手抜きです、はい。
赤字は注釈です。
今更ながら、「MySQLで SET NAMES を使ってはいけない」の根拠のお話です。
下記のPHPスクリプトでは、入力値を元にSQL文を生成し、検索クエリを投げています。
※sqltestというDBには、カラムnameを持つuserテーブルが存在します。
GETで渡された値はきちんとmysql_real_escape_stringをかけているので、SQLインジェクションは出来ないように見えます。
しかし、
http://localhost/sqltest/index.php?name=%95%5c’%20OR%201=1%20–%20
にアクセスすると、全部のデータが見えてしまいます。
下にあるPHPスクリプトを、localhost/sqltest/index.php として配置してください。
“SET NAMES SJIS” を実行すると、MySQLのエンコードがShift-JISになりますが、mysql_real_escape_stringはUTF-8のまま動作します。
16進数で 95 5c 27 20 は、
UTF-8: (謎の文字)(バックスラッシュ)(シングルコーテーション)(スペース)
Shift-JIS: (表)(シングルコーテーション)(スペース)
になります。
mysql_real_escape_stringは、バックスラッシュとシングルコーテーションそれぞれをエスケープします。
95 5c 5c 5c 27 20
MySQLは、Shift-JISとして動作するので、 (表)(\)(\)(シングルコーテーション)(スペース) と認識します。
つまり、\が一つ余分に入ることで、入力値のシングルコーテーションがエスケープされなくなります。
マルチバイト非対応のエスケープ関数を使うのと同じ理屈で、SET NAMES は危険です。
基本的に文字コード中に5Cが入るShift-JISが危険ですが、他の文字コードでも似たようなことが起こる可能性があります。
mysql_set_charset(’SJIS’);
なら、mysql_real_escape_stringもShift-JISとして動作するようになるので、安全です。
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>SQLテストページ</title>
<head>
</head>
<body>
<form action="<?php echo $_SERVER['SCRIPT_NAME'];?>">
<input type="text" name="name" />
<input type="submit" value="search" />
</form>
<?php
//DBに接続
if (! $db = mysql_connect('localhost', 'sqltest', 'sqltest'))
{
echo 'CONNECT ERROR';
exit;
}
mysql_select_db('sqltest', $db);
mysql_query('SET NAMES SJIS');
// mysql_set_charset('SJIS');
//SQLを生成
$name = mysql_real_escape_string($_REQUEST['name']);
$sql = "SELECT * FROM user WHERE name='$name'";
echo $sql . '<br />';
//実行
if (! $res = mysql_query($sql))
{
echo "QUERY ERROR <br />";
echo mysql_error();
exit;
}
echo "<pre>";
while ($row = mysql_fetch_array($res)) {
print_r($row);
}
echo "</pre>";
?>
</body>
</html>
PHPでMySQL 5をUTF8にして使う場合、文字が全部????になってしまう問題を防ぐため、
default-character-set=utf8
character-set-server=utf8
collation-server=utf8_general_ci
skip-character-set-client-handshake
を指定する、というのが定石になっています(よね?)。
しかし、共用サーバなどでmy.iniを編集できない場合、mysql_set_charset() などを使うと思います。
CakePHPでは、bootstrapでDB設定を読んで手動で設定するのも面倒だな、と思っていたら、さすがはCakePHP。便利な設定が。
app/config/database.php
var $default = array(
'driver' => 'mysql',
'persistent' => false,
'host' => 'localhost',
'login' => 'baba',
'password' => 'hoge',
'database' => 'testdb',
'prefix' => '',
'encoding' => 'utf8',
);
このようにencodingを指定するだけで、勝手にSET NAMESやmysql_set_charset()を実行してくれます。便利便利。
XAMPP はPHPやMySQLなどもいっぺんに入ってとても便利ですが、その分バージョン違いで混乱することも多いですね。
最新版1.7.3 では、PHPも5.3.1になっていて、以下の点で既存システムが動かなくなりました。php.iniのデフォルトが変わったようです。
エラー表示をE_ALLにしたら、deprecatedが大量に出る
php.ini で error_reporting = E_ALL & ~E_DEPRECATED にすればOK
cURLエクステンションが無効で、PEAR::XML_RPC2が動かない(エラーも出ない)
php.ini で extension=php_curl.dll のコメントアウトを外せばOK
特定の環境に慣れていると、開発したシステムの動作環境を正確に把握・記述することを怠ってしまうので、注意しないといけませんね。
CakePHPのpaginatorは、ページングの他にソートもやってくれて大変便利です。
しかし、たまに「ソートはするけどページングしたくない」ということもありますよね?
そんなとき、$this->paginate['limit'] = null のようにやると、Zero Divide でエラーになります。paginatorの中ではそんなことは想定されていません。
ゼロからソート部分を作るのも面倒なので、paginatorのソート部分だけを無理矢理使わせてもらいましょう。
コントローラで以下のように処理します。
//手動でsort
$order = null;
if (isset($this->passedArgs['sort'])) {
$direction = null;
if (isset($this->passedArgs['direction'])) {
$direction = strtolower($this->passedArgs['direction']);
}
if ($direction != 'asc' && $direction != 'desc') {
$direction = 'asc';
}
$order = array($this->passedArgs['sort'] => $direction);
}
//paginate処理はするが、そのデータは使わない
$this->paginate();
//全件を表示
$result = $this->Model->find('all', array('order' => $order)));
この方法なら、ビューでは、
$paginator->sort(’なまえ’, ‘User.name’);
のように、普通にpaginatorを使うときの同じようにかけます。
強引であまり褒められた方法ではありませんが、これで目的は達成できました。
開発時のテスト用に、呼ぶたびに2つの値(緯度・経度)の値が0.1ずつ増えて表示されるようなPHPスクリプトが欲しくなりました。
セッションが使えれば良いのですが、今回はクライアント側が特殊で上手く使えないため、サーバ上のファイルに保存してしまいます。
要するに値が2つ保存されるアクセスカウンタと同じです。とにかく、例外処理などを省いて簡単にすることだけを考えてみます。
//初期値 (BPSの位置です)
//35.508642
//139.442771
$latitude = 35.508642;
$longitude = 139.442771;
//前回のデータを読み込む
$data = eval('return ' . file_get_contents('data.txt') . ';');
if (is_array($data)) {
extract($data);
}
$latitude += 0.1;
$longitude += 0.1;
//新しいデータを保存
file_put_contents('data.txt', var_export(array('latitude' => $latitude, 'longitude' => $longitude), true));
//表示
echo $latitude . '\n' . $longitude;
突っ込みどころ満載ですが、スクリプト言語なんだから、たまにはevalが使いたいなーと思って書いただけですので・・・
WordPressでも、ページを上手く使えばTOPページなどの各種ページを作って、それぞれにテーマを割り当てることが出来ます。
これらのページをCMS管理する際、ページコンテンツをすべて1ページにしてしまうと、編集者がうっかりHTMLを書き換えて、レイアウトが崩れてしまう恐れがあります。
そこで、「新着情報」「ごあいさつ」のように1ページ内の各種コンテンツごとにページを用意すれば、安全に編集できます。
テーマファイル内で特定ページの内容を表示するには、以下のようにします。
<?php
$page = query_posts('pagename=news');
echo $page['page_content'];
?>
このようにquery_postsを使えばOKなのですが、ページID指定の p=2 といった書式では、ページを取得できません(投稿のみ対応しているようです)。
かならず、pagename=news のようにページスラッグを指定する必要があります。
テーマファイルとページが強く結合してしまい、美しい使い方とは言えませんが、WordPressで綺麗さを追求すると痛い目を見るので・・・
Ajaxを使う際、PHP側からJSON形式の値を出力したいことは良くあると思います(入力はだいたいPOSTで事足りますね)。
PHPでJSON形式の値を出力したい場合、いちばん簡単なのはjson_encode()です。
//本来はheaderなども正しく処理した方が良いが省略
$list = array('apple', 'orange', 'banana');
echo json_encode($list);
ただし、json_encode()はPHP 5.2.0以上の関数なので、それ未満の環境だとundefined functionでエラーになります。
http://php.net/manual/en/function.json-encode.php
リリース後にPHPのバージョン違いに気がついた可哀想な人には、Zend_Jsonがおすすめです。
まずはZend FrameworkのページからZend Frameworkをダウンロードします(minimalでOK)。
そして、回答したフォルダのZend/Json.phpと、Zend/Jsonフォルダをinclude_pathが通ったフォルダにコピーします。
あとは、コード内で
require_once 'Zend/Json.php';
$list = array('apple', 'orange', 'banana');
echo Zend_Json::encode($list);
とやるだけ。
関数の名前が変わるだけで、同じ使い勝手が実現できていますね。これなら一括置換も簡単。
XML_RPCのクライアントを開発するとき、ダミーでサーバが必要になります。
手っ取り早いダミーとして、PHPのPEAR::XML_RPC2が便利です。以前クライアント側は紹介しましたが、今回はサーバの超基本的な使い方。
ダウンロード
http://pear.php.net/package/XML_RPC2からダウンロードできます。
使い方
適当な場所に設置したら、以下のようにincludeして、各アクションに対応する関数を定義します。
そして、XML_RPC_Serverでアクション名と実行するアクションの対応を配列で渡せばOKです。
require_once("XML/RPC.php");
require_once("XML/RPC/Server.php");
$GLOBALS['XML_RPC_defencoding'] = "UTF-8";
function version($params) {
return new XML_RPC_Response(new XML_RPC_Value('1.0', 'string'));
}
function average($params) {
$v1 = $params->getParam(0)->scalarval();
$v2 = $params->getParam(1)->scalarval();
return new XML_RPC_Response(new XML_RPC_Value($v1 + $v2, 'int'));
}
new XML_RPC_Server(array(
"Test.version" => array("function" => "version"),
"Test.average" => array("function" => "average"),
));
とってもかんたん。