忍者ブログ
[PR]
×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。



2024/04/27 01:24 |
PHP5.3 マジックメソッド__autoload()
ってタイトルを書いたはいいけど__autoload()はメソッドではなく関数です。
http://php.net/manual/ja/language.oop5.autoload.php

適当なクラスを呼び出したが、そのクラスが存在しない場合に自動的に呼び出される関数です。
<?php
	//__autoloadをセット
	function __autoload($className){
		require_once('Zend/Loader.php');
		try{
			@Zend_Loader::loadClass($className);
		}catch(Exception $e){}
	}
	
	//インクルードしていないクラスをインスタンス化
	new Zend_Filter_Word_DashToSeparator('/');
インクルードされていないクラスZend_Filter_Word_DashToSeparatorを呼び出した際、通常であれば
Fatal error: Class 'Zend_Filter_Word_DashToSeparator' not found.
のエラーが発生しますが、関数__autoload()があった場合はその直前に呼び出されます。
この関数内でどうにかしてZend_Filter_Word_DashToSeparatorをインクルードすることができれば、エラーが発生することなく続きの処理を行うことができます。
関数__autoload()を通ってもクラスが見つからなかった場合には、普段のとおりFatal errorが発生します。

今回はZend_Loader::loadClass()で手っ取り早く実装しました。
たったこれだけでPearやZendクラスを使用する際にrequire_once()を並べる必要が一切なくなります。
もちろんPearライブラリにインクルードパスが通っている必要はありますが。
PR


2011/05/13 21:48 | Comments(0) | TrackBack() | PHP
PearとOpenPearのパッケージが被る
ZendのPearパッケージは全てZendディレクトリ以下に配置されます。

クラス名もZend_Captcha_Figletなどと全てZend_プレフィックスがついています。

symfonyのPearパッケージは全てsymfonyディレクトリ以下に配置されます。
クラス名はsfMessageSource_gettextなどと、Pearの命名規則には沿っていないのですが全てsfプレフィックスがついています。

ところがOpenPearにはプレフィックスがありません。
配置もPearパッケージと同レベルに配置されます。

Services_ShitarabaServices_Bloggingなんかと同階層に置かれちゃうのはちょっとどうかという気がしないでもない。

それはまだいいんですが問題は名前が被ってるパッケージ。
既にServices_TwitterとNet_IRCが被っています。

http://pear.php.net/package/Services_Twitter
http://openpear.org/package/Services_Twitter

http://openpear.org/package/Net_IRC
http://pear.php.net/package/Net_IRC

OpenPear側がどちらもリリースされていないため現在は問題が起こりませんが、今後両方のパッケージが増えてくると問題になるのではないかと思われます。


2011/05/09 21:37 | Comments(0) | TrackBack() | PHP
fclose()の動作がPHP5.3.2から変更になっている件
fclose()の関数リファレンスには変更履歴は何もありません。
http://www.php.net/manual/ja/function.fclose.php

ところがflock()の変更履歴にこんなことが書いてあります。
http://www.php.net/manual/ja/function.flock.php

> 5.3.2
> ファイルのリソースハンドルを閉じたときにロックを自動的に解放する機能が削除されました。
> ロックの解放は、常に手動で行わなければなりません。 

つまり、
	$fp = fopen('hoge.txt','a');
	while(flock($fp, LOCK_EX)===false){
		sleep(1);
	}
	fwrite($fp, 'hoge');
	fclose($fp);
	
	//時間のかかる処理
	sleep(100);
	print('おわり');
この書き方はPHP5.3.2になった途端に破綻するということです。

なんでこの機能削除したんでしょう?
けっこう重大な問題になりそうなところがありそうな気がするんですが。

削除したはいいんですがfclose()の更新履歴にも書いてほしいですね。
まあ、よく見たらNotesに書いてありますけどね。


2011/05/06 21:29 | Comments(0) | TrackBack() | PHP
ZF1.11 Zend_DbでOracleに空文字列をインサートできない
テーブル'table'の'text'列にはNOT NULL制約を設定しています。

	$db = Zend_Db::factory('Oracle', $params);
	$insert_array = array(
		 'id'=>1
		,'text'=>''
	);
	$db->insert('table', $insert_array);

> ORA-01400: ("table"."text")にはNULLは挿入できません。

なにそれふざけてるの。


何処で例外が発生するかというと
 Zend_Db_Statement_Oracle::_executeのoci_execute()です。
が、実際に問題となる部分は
 @oci_bind_by_name($this->_stmt, $name, $params[$name], -1)
です。
ここで
 @oci_bind_by_name($this->_stmt, ':hoge', '', -1)
という関数が発行され、バインドする値として''が設定されます。

本来NOT NULL制約のカラムに''は当然インサートできるのですが、Oracleは
Oracle Databaseは現在、長さが0(ゼロ)の文字値をNULLとして処理します。
という信じ難い出鱈目な実装が行われているため、''をインサートすることができません。


最初Zend_Dbのアダプタか設定オプションの問題かと思ってたら実はOracleそのものの欠陥だったでござる。

アダプタを介さずに、直接手動でSQLを発行してもORA-01400エラーが発生します。
これはNOT NULL制約が使い物にならんな…


2011/04/26 23:45 | Comments(0) | TrackBack() | PHP
ZF1.11:Zend_Log_Writer_Streamはファイルをロックしていない
簡単にログを保存できるZend_Logですが、使用法によっては大問題に。

公式リファレンスでは
	$writer = new Zend_Log_Writer_Stream('/path/to/logfile');
	$logger = new Zend_Log($writer);
	$logger->info('Informational message');
というサンプルが書かれているわけですが、この書き方は全くお薦めできません。
何故ってZend_Log_Writer_Streamのソース見ればわかります。
以下はZend_Log_Writer_Streamの概要。
	Zend_Log_Writer_Stream{
	function __construct(){
		$this->_stream = fopen('/path/to/logfile', 'a');
	}
	function _write(){
		fwrite($this->_stream, $string));
	}
	function shutdown(){
		fclose($this->_stream);
	}
}
何が問題って何処にもflock()が無え!

試してみましょう。
<?php
	session_start();
	
	//Zend_Log
		require_once('Zend/Log.php');
		require_once('Zend/Log/Writer/Stream.php');
		$writer = new Zend_Log_Writer_Stream('/path/to/logfile');
		$logger = new Zend_Log($writer);
		
	//開始
		$logger->info('開始:'.session_id());
	//時間のかかる処理
		sleep(5);
	//終了
		$logger->info('終了:'.session_id());

複数のブラウザで同時に実行します。

   2011-01-01T00:00:00+09:00 INFO (6): 開始:p987n76eq664t68mk6grangmm7
   2011-01-01T00:00:01+09:00 INFO (6): 開始:osp8sov2orkfq0gaeir09b7vp3
   2011-01-01T00:00:05+09:00 INFO (6): 終了:p987n76eq664t68mk6grangmm7
   2011-01-01T00:00:06+09:00 INFO (6): 終了:osp8sov2orkfq0gaeir09b7vp3

見事にロックされていません。
結果として、アクセスが集中するとログが消えたり壊れたりする可能性があります。
といって外からflock()をかける手段もありません。
これは欠陥といっていいのでは。


2011/04/22 22:24 | Comments(0) | TrackBack() | PHP
ZF1.11 Zend_Cloudその2
先日唯一とか言いましたが、もうひとつ無料で使えそうなサービスがあったので使ってみます。
Zend_Cloud_QueueService_Adapter_ZendQueueというのがそれで、日本語情報はこれを書いている段階では完全に0です。
どうやって使うかというとまあ要するにキューです。

zendQueue.php
<?php
	
	//require
		require_once('Zend/Cloud/QueueService/Adapter/ZendQueue.php');
	
	//設定
		$config = array('adapter' => 'array');
	
	//Zend_Cloud_QueueService_Adapter_ZendQueue
		$zfQueue = new Zend_Cloud_QueueService_Adapter_ZendQueue($config);
	
	//キュー
		$queueIDTest1 = $zfQueue->createQueue('test1', array(Zend_Queue::TIMEOUT=>100));
		$queueIDTest2 = $zfQueue->createQueue('test2', array(Zend_Queue::TIMEOUT=>1));
	
	//キューにメッセージを保存
		$zfQueue->sendMessage($queueIDTest1, 'message1');
		$zfQueue->sendMessage($queueIDTest1, 'message2');
		$zfQueue->sendMessage($queueIDTest2, 'message3');
		
	//キューを取得
		$queueList = $zfQueue->listQueues();
	
	//メッセージを取得
		$messages = $zfQueue->receiveMessages($queueIDTest1, 10);

使用法は簡単で、まずcreateQueueメソッドでキューを作成し、sendMessageでキューに対してメッセージを送信するだけです。
ちなみにZend_Cloud_QueueService_Adapter_ZendQueueは内部でZend_Queueを使用しており、MemcacheやDBなども使用可能です

さて、最後に取得したメッセージなんですが、$queueIDTest1から取得しているので$queueIDTest1に突っ込んだ'message1'と'message2'だけが取得されるのかと思いきや何故か'message3'も一緒に帰ってきました。
キュー作った意味ないじゃん。
なんだこれ。


2011/04/18 21:07 | Comments(3) | TrackBack() | PHP
ZF1.11:Zend_Registry
Zend_Registryとは、一言で言うとまああれだ、グローバル変数。
クラス内とかメソッド内とかスコープを気にせず好きなところでZend_Registry::set('hoge', 'fuga');ってやって、それ以降の好きなところでZend_Registry::get('hoge');ってすると'fuga'を取得できます。

使いどころは何処かというと、スクリプトのパスやデータベースのパスワード等、最初に定義したら以後変更する必要のないものとなります。
大抵そういうのはdefine()で定義していると思いますが、define()は配列やオブジェクトを入れられないという欠点があります。
その点Zend_Registryは配列やオブジェクトをそのまま突っ込むことができるので、使い回しが格段に楽になります。
	$link = mysql_connect('localhost', 'mysql_user', 'mysql_password');
	Zend_Registry::set('link', $link);
とすると、以後はDBに接続する必要があればZend_Registry::get('link')でDB接続オブジェクトをシングルトン的に取得できます。

ただZend_Registryはsetで簡単に上書き可能なため、結局はグローバル変数を使用するのと同じ危険性をはらんでいます。
気がつかないところで上書きされるとエラーの温床になるので、運用にはルールを義務づけるとよいでしょう。

実は、$_REQUESTあたりのスーパーグローバル変数に値を突っ込むとクラスとかスコープとかを完全に無視してどこからでも読むことができるので、完全にZend_Registryの代用になったりするのですが秘密です。
まあ、そういう行儀の悪いことをしてはいけません。


2011/04/11 23:19 | Comments(0) | TrackBack() | PHP
PHP:file()のFILE_IGNORE_NEW_LINESの動作が不可解な件
先日
	$file = @file('hoge.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
ってやったのに行末の改行が消えたり消えなかったりしてあれえ?ってなりました。

とりあえず検証。

中身が「text\r\n」だけのテキストファイルを用意し、「file.txt」「file.csv」という名前で保存します。

file_ignore_new_lines.php
<?php
	$fileTxt = @file('file.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
	$fileCsv = @file('file.csv', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
	print('<pre>');var_dump($fileTxt[0],$fileCsv[0]);print('</pre><hr>');
一分の隙もない完璧な検証実験だ。

ローカルのWindows用XAMPP、Apache2.2.14、PHP5.3.1
	string(4) "text"
	string(4) "text"
色々おかしな設定のRHEL、Apache2.0.63、PHP5.2.1
	string(4) "text"
	string(5) "text
	"
サーバ単位で動作が違うとかならまだしも拡張子で動作が違うってどういうこった?

よく見てみたらアップ後のファイルサイズが、「file.txt」は5バイト、「file.csv」は6バイトになっていました。
実は前者はテキストモード、後者はバイナリモードで転送していたっていう。
Windowsの改行は\r\n、Linuxの改行は\nなので、最近のFTPソフトはこの改行を自動変換してくれる機能があります。
「file.txt」はテキストモード転送で\r\nが勝手に\nになっていたので改行が出なくなっていただけでした。

両者ともバイナリモードで転送したところ、無事に
	string(5) "text
	"
	string(5) "text
	"
となりました。
5バイトなので最後の\nが削除され、\rが残っています。

元々FILE_IGNORE_NEW_LINESは\nだけを対象としたもので、\rは対象外なのです。
ではなんで\r\nのままのはずのローカルでは改行コードが外れてるんだという話ですが、

file_ignore_new_lines.php
<?php
	$fileTxt = @file('file.txt');
	$fileCsv = @file('file.csv');
	print('<pre>');var_dump($fileTxt[0],$fileCsv[0]);print('</pre><hr>');

	string(6) "text
	"
	string(6) "text
	"
6バイトになっており、\r\n共に消えていません。
Windows用PHPなので気を利かせて\r\nどちらも消してくれたのかもしれません。
まあこんなふうに挙動が違うせいでバグが発生したりするんですがね!

ところでauto_detect_line_endingsという設定項目があります。

> onにした場合、PHPは fgets() および file() により読み込まれたデータを評価し、UNIX、MS-DOS、Machintoshの行末表記を使用しているかどうかを調べます。
> これにより、PHPがMacintoshシステムと相互運用できるようになりますが、 デフォルトはOffとなっています。


つまり…これを設定すれば改行コードを気にしなくてもよくなるってことなんだよ!

<?php
	ini_set('auto_detect_line_endings',1);
	$fileTxt = @file('file.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
	$fileCsv = @file('file.csv', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
	print('<pre>');var_dump($fileTxt[0],$fileCsv[0]);print('</pre><hr>');

XAMPP
	string(4) "text"
	string(4) "text"
RHEL
	string(5) "text
	"
	string(5) "text
	"

あれえ?


2011/04/07 22:52 | Comments(0) | TrackBack() | PHP
EC-CUBE SC_DB_DBFactory_MYSQL::getWhereConverter()を使用する
EC-CUBEは、MySQLの場合Viewを使用せず毎回テーブルをJOINしています。
View非対応の古いバージョンのMySQLのためにそのような作りになっているようですが、そのせいで恐ろしい弊害が発生します。


EC-CubeをMySQLで実装すると、遅すぎて使い物にならなくなります。
どのくらい酷いかって中野区立図書館を遙かに超えるレベル。

商品数わずか1000件程度でストレスを感じるほどの遅延が出るようになり、10000件登録しようものならTimeoutをいじらないといけないというどうしようもない出来映えです。

どうしてこんなことになってるのか/class/db/dbfactory/SC_DB_DBFactory_MYSQL.phpを見てみましょう。
SC_DB_DBFactory_MYSQL::viewToSubQuery()とかな。
なんだよこれ。

読んでみると、サブクエリで全件取得して、その後でWHERE句で絞り込みを行うというとんでもなく非効率な書き方のSQLとなっています。
なんかもうちょっとどうにかならんのか、と見ているとサブクエリ中に'&&noncls_where&&'という謎の文字列を発見しました。

具体的なSQLは長大すぎて引用できませんが、構造的には次のような作りです。
'SELECT * FROM ( SELECT * FROM dtb_products &&noncls_where&& ) AS T1 WHERE (検索)'

ビューを参照するSQLを実行すると、

SC_Query::select()
SC_DB_DBFactory::getInstance()
SC_DB_DBFactory_MYSQL::sfChangeMySQL()
SC_DB_DBFactory_MYSQL::sfChangeView()
SC_DB_DBFactory_MYSQL::viewToSubQuery()
SC_DB_DBFactory_MYSQL::getWhereConverter()

という順番でSQLが変換されます。
SC_DB_DBFactory_MYSQL::viewToSubQuery()でビューに相当するSQLに書き換えられ、SC_DB_DBFactory_MYSQL::getWhereConverter()'&&noncls_where&&'のような文字列を置換して返します。

つまり、'&&noncls_where&&'に抽出範囲を狭める条件文をねじ込めば、SQLの実行速度が速くなりそうです。
サブクエリの返り値が10000件と1件では圧倒的な差が生まれることは間違いありません。

で、具体的なSC_DB_DBFactory_MYSQL::getWhereConverter()はどうなってるかというと、
 
    function getWhereConverter() {
        return array(
            "&&crscls_where&&" => "",
            "&&crsprdcls_where&&" =>"",
            "&&noncls_where&&" => "",
            "&&allcls_where&&" => "",
            "&&allclsdtl_where&&" => "",
            "&&prdcls_where&&" => "",
            "&&catcnt_where&&" => ""
        );
    }

実装されていません。

さらに、コントローラからSC_DB_DBFactoryを直接触る術がないため、外部から値を注入することもできません。
selectするたびに毎回SC_DB_DBFactoryインスタンスを作成してるんだよねこれ。

どうにかするためにはSC_Queryを直接書き換える必要があります。

/eccube/data/class/db/dbfactory/SC_DB_DBFactory_MYSQL.php
    function getWhereConverter() {
        return array(
            "&&crscls_where&&"    => $this->where['crscls_where'],
            "&&crsprdcls_where&&" => $this->where['crsprdcls_where'],
            "&&noncls_where&&"    => $this->where['noncls_where'],
            "&&allcls_where&&"    => $this->where['allcls_where'],
            "&&allclsdtl_where&&" => $this->where['allclsdtl_where'],
            "&&prdcls_where&&"    => $this->where['prdcls_where'],
            "&&catcnt_where&&"    => $this->where['catcnt_where']
        );
    }

/eccube/data/class/SC_Query.php
    function select($col, $table, $where = "", $arrval = array()){
        $sqlse = $this->getsql($col, $table, $where);
        $dbFactory = SC_DB_DBFactory_Ex::getInstance();
        $dbFactory->where = $this->where;
        $sqlse = $dbFactory->sfChangeMySQL($sqlse);
        $ret = $this->conn->getAll($sqlse, $arrval);
        return $ret;
    }

以上で形ができました。
あとはSELECTを実行する際に、
    $objQuery->where['noncls_where'] = ' WHERE product_id = '.mysql_real_escape_string($product_id);
    $arrRet = $objQuery->select("*", "vw_products_nonclass AS alldtl", "product_id = ?", array($product_id));

などとすれば動作します。

上記はあくまで最低限の形なので、NOTICEが出たりコントローラに直接mysql_real_escape_string()があったりする残念な作りです。
SC_Query::escape()Pear_DB::escapeSimple()あたりを拾ってくるようにしたりした方がよいでしょう。
あと'&&noncls_where&&'以外の置換文字列はそもそも存在すらしていないので、追加するなり削除するなりしてしまいましょう。


ちなみにPostgresの場合は相当すっきりしたSQLになっていますが、これは単にCREATE VIEWに面倒ごとを突っ込んでいるだけです。
つうかMySQLにviewが実装されたのは2005年なんだからview非対応への対応とかもういいよ。


2011/03/21 19:37 | Comments(0) | TrackBack() | PHP
ZF1.11:Zend_Db_Adapterの解説が間違っている件
http://framework.zend.com/manual/ja/zend.db.adapter.html
> 行の削除
> 三番目の引数に配列の配列を提示すると、 値は自動的に引用符で囲まれてキーに入れられます。
> そしてこれらは条件として結合され、 AND 演算子で区切られます。


嘘です。
Zend_Db_Adapter_Abstract::delete()は引数を二つしか見ないので、第二引数を指定しないと全部削除されます。

上の
> データの更新

の部分からのコピペミスです。

見事に騙されたorz


2011/03/16 22:44 | Comments(3) | TrackBack() | PHP

<<前のページ | HOME | 次のページ>>
忍者ブログ[PR]