Page: 1/8 >>
[EC-CUBE]EC-CUBE 2.4.4で商品点数が多い時のチューニング
2010.09.26 Sunday | category:PHP
正直EC-CUBEは綺麗なMVCになっていないのでやりづらく、 あまりカスタマイズしてまで使いたくないんだけど、登録商品が1万点超えてるとあちこち不都合なところが出てくるのでカスタマイズしたメモ。
まさか、こんな無謀な運用する人がほかにいるとも思えませんが、お困りの方がいらっしゃったらと思い、まとめておきます。
*サーバーは専用鯖1台占有以上。
*DBはMySQLでなくPostgreSQLにすべし。できるだけ最新バージョン(速度が速い)。
*カテゴリ別一覧の対処
古いバージョンだとvw_products_allclassの定義が違う(もっと軽い)ので起きないのだが、最新バージョンだとかなり複雑な定義になっているためか、カテゴリ別一覧が表示できない。(いつまでたってもレスポンスが返って来ない、タイムアウト)
SQL文を直接psqlで実行しても結果が返って来ないため、カスタマイズを決断。
どういうわけか、同じビューを使うSQLでもキーワードが指定してある場合(検索)は正常にレスポンスが返って来るので、カテゴリ一覧の時だけ場合分け。
data/class/pages/products/LC_Page_Products_List.php
376行目からのlfDispProductsListというメソッドを
data/class_extends/pages/products/LC_Page_Products_List_Ex.php
にまるまるコピペ。
※以下行番号はEXじゃないほうのクラスファイルでのものです
418 foreach ($names as $val) {
419 if ( strlen($val) > 0 ){
420 $where .= " AND ( name ILIKE ? OR comment3 ILIKE ?) ";
421 $ret = SC_Utils_Ex::sfManualEscape($val);
422 $arrval[] = "%$ret%";
423 $arrval[] = "%$ret%";
424 }
425 }
↓
+++ $flag = false;
418 foreach ($names as $val) {
419 if ( strlen($val) > 0 ){
420 $where .= " AND ( name ILIKE ? OR comment3 ILIKE ?) ";
421 $ret = SC_Utils_Ex::sfManualEscape($val);
422 $arrval[] = "%$ret%";
423 $arrval[] = "%$ret%";
+++ $flag = true;
424 }
425 }
+++ if($flag)
+++ {
+++ //検索モード
+++ $vw_table = 'vw_products_allclass AS allcls';
+++ $idColumn = 'DISTINCT product_id';
+++ }
+++ else
+++ {
+++ //カテゴリ一覧モード
+++ $idColumn = 'DISTINCT dtb_products.product_id';
+++ $vw_table = '(dtb_products LEFT JOIN dtb_product_categories ON dtb_product_categories.product_id = dtb_products.product_id)';
+++ $where = str_replace('category_id', 'dtb_product_categories.category_id', $where);
+++ }
426
427 $arrProduct_id = $objQuery->getCol($vw_table, $idColumn, $where, $arrval);
*カテゴリ別一覧の対処2
対処1でほとんどのカテゴリのカテゴリ別商品一覧は正常に出るようになったが、1カテゴリだけシステムエラーになるカテゴリがあった。
さすがに1ルートカテゴリあたりの商品点数が5000点を超えているというスゴいお店のために起きたことで、普通のショップでは起きないと思うが…。
debugをtrueにしてsite.logを見るとPostgreSQLから
stack depth limit exceeded
というエラーでSQLの実行がエラーで返ってきている。
調べてみるとmax_stack_depthという設定値を上げろというのだが、公式ドキュメントで
http://www.postgresql.jp/document/8.4/html/runtime-config-resource.html
結構怖いことが書いてある。カーネルの制限を超えると云々。
おそらく、product_id IN (...)で数千件のIDが列挙されている部分がひっかかったのだろう。そこでロジックを下記のように変更。
$arrProduct_id(product_idの配列)をループで廻して、連番になっているものに関しては以上・以下でまとめ、INでなくORでつないだWHERE句を出すようにした。
具体的にどう書いたかと言うと…
旧
476 // WHERE 句
477 $where = '0=0';
478 if (is_array($arrProduct_id) && !empty($arrProduct_id)) {
479 $where .= ' AND product_id IN (' . implode(',', $arrProduct_id) . ')';
480 } else {
481 // 一致させない
482 $where .= ' AND 0<>0';
483 }
↓
新
476 // WHERE 句
477 $where = '0=0';
478 if (is_array($arrProduct_id) && !empty($arrProduct_id)) {
479 //$where .= ' AND product_id IN (' . implode(',', $arrProduct_id) . ')';
+++ $wheres = array();
+++ $min = 0;
+++ $max = 0;
+++ $prev = 0;
+++ foreach($arrProduct_id as $id)
+++ {
+++ if($prev>0 && $id - $prev==1)
+++ {
+++ $max = $id;
+++ }
+++ else
+++ {
+++ if($min>0 && $max>=$min)
+++ {
+++ $wheres[] = '(product_id >= '.$min.' and product_id <= '.$max.')';
+++ }
+++ $min = $id;
+++ }
+++ $prev = $id;
+++ }
+++ $where .= ' AND ('.implode(' OR ', $wheres).')';
480 } else {
481 // 一致させない
482 $where .= ' AND 0<>0';
483 }
例えばproduct_id IN (1,2,3,4,5,10,11,12,13,14)になるものを
(product_id >= 1 AND product_id <= 5) OR (product_id >=10 AND product_id<=14)
になるようにしただけなのですが、不思議と速度も改善しました。
まぁやっぱりINのクエリはprimaryのキーだろうと遅いということを実感したわけです。
なお、特殊な環境での利用に向けたカスタマイズですので、本線に提供等は考えていません。
もし使いたいという方はご自由にコピペして持ち帰っていただいて構いませんが、私のコードを持ち帰ったことによる過負荷・サーバークラッシュ等の損害に一切責任は負えませんので、自己責任でお願いします。
まさか、こんな無謀な運用する人がほかにいるとも思えませんが、お困りの方がいらっしゃったらと思い、まとめておきます。
*サーバーは専用鯖1台占有以上。
*DBはMySQLでなくPostgreSQLにすべし。できるだけ最新バージョン(速度が速い)。
*カテゴリ別一覧の対処
古いバージョンだとvw_products_allclassの定義が違う(もっと軽い)ので起きないのだが、最新バージョンだとかなり複雑な定義になっているためか、カテゴリ別一覧が表示できない。(いつまでたってもレスポンスが返って来ない、タイムアウト)
SQL文を直接psqlで実行しても結果が返って来ないため、カスタマイズを決断。
どういうわけか、同じビューを使うSQLでもキーワードが指定してある場合(検索)は正常にレスポンスが返って来るので、カテゴリ一覧の時だけ場合分け。
data/class/pages/products/LC_Page_Products_List.php
376行目からのlfDispProductsListというメソッドを
data/class_extends/pages/products/LC_Page_Products_List_Ex.php
にまるまるコピペ。
※以下行番号はEXじゃないほうのクラスファイルでのものです
418 foreach ($names as $val) {
419 if ( strlen($val) > 0 ){
420 $where .= " AND ( name ILIKE ? OR comment3 ILIKE ?) ";
421 $ret = SC_Utils_Ex::sfManualEscape($val);
422 $arrval[] = "%$ret%";
423 $arrval[] = "%$ret%";
424 }
425 }
↓
+++ $flag = false;
418 foreach ($names as $val) {
419 if ( strlen($val) > 0 ){
420 $where .= " AND ( name ILIKE ? OR comment3 ILIKE ?) ";
421 $ret = SC_Utils_Ex::sfManualEscape($val);
422 $arrval[] = "%$ret%";
423 $arrval[] = "%$ret%";
+++ $flag = true;
424 }
425 }
+++ if($flag)
+++ {
+++ //検索モード
+++ $vw_table = 'vw_products_allclass AS allcls';
+++ $idColumn = 'DISTINCT product_id';
+++ }
+++ else
+++ {
+++ //カテゴリ一覧モード
+++ $idColumn = 'DISTINCT dtb_products.product_id';
+++ $vw_table = '(dtb_products LEFT JOIN dtb_product_categories ON dtb_product_categories.product_id = dtb_products.product_id)';
+++ $where = str_replace('category_id', 'dtb_product_categories.category_id', $where);
+++ }
426
427 $arrProduct_id = $objQuery->getCol($vw_table, $idColumn, $where, $arrval);
*カテゴリ別一覧の対処2
対処1でほとんどのカテゴリのカテゴリ別商品一覧は正常に出るようになったが、1カテゴリだけシステムエラーになるカテゴリがあった。
さすがに1ルートカテゴリあたりの商品点数が5000点を超えているというスゴいお店のために起きたことで、普通のショップでは起きないと思うが…。
debugをtrueにしてsite.logを見るとPostgreSQLから
stack depth limit exceeded
というエラーでSQLの実行がエラーで返ってきている。
調べてみるとmax_stack_depthという設定値を上げろというのだが、公式ドキュメントで
http://www.postgresql.jp/document/8.4/html/runtime-config-resource.html
結構怖いことが書いてある。カーネルの制限を超えると云々。
おそらく、product_id IN (...)で数千件のIDが列挙されている部分がひっかかったのだろう。そこでロジックを下記のように変更。
$arrProduct_id(product_idの配列)をループで廻して、連番になっているものに関しては以上・以下でまとめ、INでなくORでつないだWHERE句を出すようにした。
具体的にどう書いたかと言うと…
旧
476 // WHERE 句
477 $where = '0=0';
478 if (is_array($arrProduct_id) && !empty($arrProduct_id)) {
479 $where .= ' AND product_id IN (' . implode(',', $arrProduct_id) . ')';
480 } else {
481 // 一致させない
482 $where .= ' AND 0<>0';
483 }
↓
新
476 // WHERE 句
477 $where = '0=0';
478 if (is_array($arrProduct_id) && !empty($arrProduct_id)) {
479 //$where .= ' AND product_id IN (' . implode(',', $arrProduct_id) . ')';
+++ $wheres = array();
+++ $min = 0;
+++ $max = 0;
+++ $prev = 0;
+++ foreach($arrProduct_id as $id)
+++ {
+++ if($prev>0 && $id - $prev==1)
+++ {
+++ $max = $id;
+++ }
+++ else
+++ {
+++ if($min>0 && $max>=$min)
+++ {
+++ $wheres[] = '(product_id >= '.$min.' and product_id <= '.$max.')';
+++ }
+++ $min = $id;
+++ }
+++ $prev = $id;
+++ }
+++ $where .= ' AND ('.implode(' OR ', $wheres).')';
480 } else {
481 // 一致させない
482 $where .= ' AND 0<>0';
483 }
例えばproduct_id IN (1,2,3,4,5,10,11,12,13,14)になるものを
(product_id >= 1 AND product_id <= 5) OR (product_id >=10 AND product_id<=14)
になるようにしただけなのですが、不思議と速度も改善しました。
まぁやっぱりINのクエリはprimaryのキーだろうと遅いということを実感したわけです。
なお、特殊な環境での利用に向けたカスタマイズですので、本線に提供等は考えていません。
もし使いたいという方はご自由にコピペして持ち帰っていただいて構いませんが、私のコードを持ち帰ったことによる過負荷・サーバークラッシュ等の損害に一切責任は負えませんので、自己責任でお願いします。
GoogleAnalytics Export APIをPHPから使う GoogleAnalytics.class.php利用時の注意
2010.08.28 Saturday | category:PHP
開発者さんのところからダウンロードしてくると、使えることは使えるんですが2ヶ所NOTICEが出ます。リリースしちゃえば(prodにしちゃえば)気にならないことなんですが、symfonyでdev版で開発してるとどうしても気になるので2ヶ所直して使っています。
■126行目で$dimsは未設定の変数というNOTICE
見てみると
$dims .= $dimension->getAttribute('value');
どうやら1回目のループ実行時はまだ未設定の変数に対して.=を行うことになるのでNOTICEになる模様。
ループの前に$dims = '';と1行足して解決。
■251行目でArray to String convertionのNOTICE
見てみると
250 $header[] = array("application/x-www-form-urlencoded");
251 curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
となっていた。
curl_setoptのマニュアルを見るに第3引数は配列を渡せばいいらしい。
ちゃんと$headerは配列になっているが、$header配列の値自体がarray("application/x-www-form-urlencoded")と配列になってしまっている。
curl関数使ったことないので(汗)詳しいことはわからないが、配列がstringとして評価されてNOTICEが出てるってことなので、$header = array(...)のarray関数を外してみた。
250 $header[] = "application/x-www-form-urlencoded";
251 curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
正直curl関数はまだよくわからないがこれでテスト実行してみてエラー出ず、開発サーバーで実行して見て画面からNOTICEも消えたので良しとする(笑)
後でgapi.class.phpも試してみたい。どっちもPHP5なオブジェクト指向で書かれたlibですが、GoogleAnalyticsの公式に載っているのがgapi.class.phpのほうなので。
■126行目で$dimsは未設定の変数というNOTICE
見てみると
$dims .= $dimension->getAttribute('value');
どうやら1回目のループ実行時はまだ未設定の変数に対して.=を行うことになるのでNOTICEになる模様。
ループの前に$dims = '';と1行足して解決。
■251行目でArray to String convertionのNOTICE
見てみると
250 $header[] = array("application/x-www-form-urlencoded");
251 curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
となっていた。
curl_setoptのマニュアルを見るに第3引数は配列を渡せばいいらしい。
ちゃんと$headerは配列になっているが、$header配列の値自体がarray("application/x-www-form-urlencoded")と配列になってしまっている。
curl関数使ったことないので(汗)詳しいことはわからないが、配列がstringとして評価されてNOTICEが出てるってことなので、$header = array(...)のarray関数を外してみた。
250 $header[] = "application/x-www-form-urlencoded";
251 curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
正直curl関数はまだよくわからないがこれでテスト実行してみてエラー出ず、開発サーバーで実行して見て画面からNOTICEも消えたので良しとする(笑)
後でgapi.class.phpも試してみたい。どっちもPHP5なオブジェクト指向で書かれたlibですが、GoogleAnalyticsの公式に載っているのがgapi.class.phpのほうなので。
Doctrine(1.x)でのLEFT OUTER JOIN実現方法
2010.07.31 Saturday | category:symfony
公式のドキュメント
http://www.doctrine-project.org/projects/orm/1.2/docs/manual/dql-doctrine-query-language/ja#join%E3%81%AE%E6%A7%8B%E6%96%87:on%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89
によると、ONキーワードでリレーション条件がカスタム指定できるようなことが書いてありますが、実際には効きませんorz
試行錯誤の末、無理やりLEFT OUTER JOINぽいことを実行させることができたので忘れないようにメモ。
実験しながらエラーMSGでググると、symfony公式のフォーラムでも世界各国の方々がLEFT OUTER JOINができないと悩んでいた。
なんでONを自分で指定したいかと言うとモデルを次のように定義してあるとして、
なんでこんな変な構造にするか詳細は聞かないでください(笑
【実験1】
まず公式のドキュメント通りに。
$q = Doctrine::getTable("Baker")->createQuery("b")->leftJoin("Able a ON a.id ON b.out_id");
⇒...FROM Baker b, Able a...
ONどこいった??単にFROMにカンマ区切りで入れられると、SQL側でAble.idとBaker.idで勝手にJOINされちゃって都合が悪い。
【実験2】
公式ドキュメントにWITHというのも書いてあるので使ってみた。
$q = Doctrine::getTable("Baker")->createQuery("b")->leftJoin("Able a ON a.id =b.out_id");
⇒...FROM Baker b, Able a...
WITHどこいった??
【実験3】
無理を承知でINNER JOINにしてみる。Linux系OSのMySQLで、MyISAMテーブル(=外部キー制約なし)だとBaker.out_idとA.idのINNER JOINとか普通に成立した記憶が。
$q = Dcotrine::getTable("Baker")->createQuery("b")->innerJoin("Able a ON a.id = b.out_id");
⇒エラー。AbleとBakerの間にリレーションが定義されてないとか何とか。まあ当然。
ここで公式APIドキュメントを読みに行く。
Doctrine_Query_Abstruct::leftJoin($join, $params=array())
色々場合分けはあるが、要は最後に $this->dqlQueryPart['from']に 'LEFT JOIN '.$joinをセットする仕組みのよう。
【実験4】
とりあえずセットした後にちゃんとセットされているかどうかチェックしてみる。
$q = Doctrine::getTable("Baker")->createQuery("b")->leftJoin("Able a ON a.id = b.out_id");
var_dump($q->getDql());
⇒FROM Baker b LEFT JOIN Able a ON a.id = b.out_id
ちゃんとDQLにはセットされていることが確認できた。
が、$q->getSqlQuery()すると相変わらず FROM Baker b, Able aのみ。どうやらDQLからSQLを生成する時にONが無視されてしまう様子。
ここでふと思い出した。Pear::DBを使ってSQLを勉強し始めた頃、FROMにテーブルを羅列してWHEREでJOIN条件書いてもほしいデータが取れてたなー。その後SQL実行速度の問題でちゃんとJOINを書くようになったんだけど。
【実験5】
だめもとで。
$q = Doctrine::getTable("Baker")->createQuery("b")->addFrom("Able a")->addWhere("b.out_id = a.id");
⇒実験3と同じエラー。でも、$q->getSqlQuery()ではちゃんとSQLが出て来るし、それをコピペしてphpmyadminに実行させたらちゃんと想定通りのデータが出た。
【実験6】
実験5ではAbleのデータを入れるコンポーネント(プロパティ)がないよ!って怒られてる気がしてきた。同時にBakerとJOINするAbleまたはCatのデータはBakerに取得用のメソッドを作ってあるのでAbleのデータを最初のSELECTで取らなくてもいいよね、と考える。
$q = Doctrine::getTable("Baker")->createQuery("b")->addFrom("Able a")->addWhere("b.out_id = a.id")->select("b.*");
⇒成功!!
ここまで来るのに休み休み(家事育児やりながら^^;)で4時間近くかかってしまった(汗
【結論】
スマートじゃない&MySQLエンジンがんばって!なSQLだけど、上記の方法でLEFT OUTER JOINっぽいことはできることがわかりました。
http://www.doctrine-project.org/projects/orm/1.2/docs/manual/dql-doctrine-query-language/ja#join%E3%81%AE%E6%A7%8B%E6%96%87:on%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89
によると、ONキーワードでリレーション条件がカスタム指定できるようなことが書いてありますが、実際には効きませんorz
試行錯誤の末、無理やりLEFT OUTER JOINぽいことを実行させることができたので忘れないようにメモ。
実験しながらエラーMSGでググると、symfony公式のフォーラムでも世界各国の方々がLEFT OUTER JOINができないと悩んでいた。
なんでONを自分で指定したいかと言うとモデルを次のように定義してあるとして、
Able:Bakerはout_classの値によりAbleかCatどちらかにJOIN先が変わります。
columns:
id: { type: integer(1), primary: true }
Baker:
columns:
id: { type: integer(1), primary: true }
out_class: string(4)
out_id: integer(1)
Cat:
columns:
id: { type: integer(1) }
なんでこんな変な構造にするか詳細は聞かないでください(笑
【実験1】
まず公式のドキュメント通りに。
$q = Doctrine::getTable("Baker")->createQuery("b")->leftJoin("Able a ON a.id ON b.out_id");
⇒...FROM Baker b, Able a...
ONどこいった??単にFROMにカンマ区切りで入れられると、SQL側でAble.idとBaker.idで勝手にJOINされちゃって都合が悪い。
【実験2】
公式ドキュメントにWITHというのも書いてあるので使ってみた。
$q = Doctrine::getTable("Baker")->createQuery("b")->leftJoin("Able a ON a.id =b.out_id");
⇒...FROM Baker b, Able a...
WITHどこいった??
【実験3】
無理を承知でINNER JOINにしてみる。Linux系OSのMySQLで、MyISAMテーブル(=外部キー制約なし)だとBaker.out_idとA.idのINNER JOINとか普通に成立した記憶が。
$q = Dcotrine::getTable("Baker")->createQuery("b")->innerJoin("Able a ON a.id = b.out_id");
⇒エラー。AbleとBakerの間にリレーションが定義されてないとか何とか。まあ当然。
ここで公式APIドキュメントを読みに行く。
Doctrine_Query_Abstruct::leftJoin($join, $params=array())
色々場合分けはあるが、要は最後に $this->dqlQueryPart['from']に 'LEFT JOIN '.$joinをセットする仕組みのよう。
【実験4】
とりあえずセットした後にちゃんとセットされているかどうかチェックしてみる。
$q = Doctrine::getTable("Baker")->createQuery("b")->leftJoin("Able a ON a.id = b.out_id");
var_dump($q->getDql());
⇒FROM Baker b LEFT JOIN Able a ON a.id = b.out_id
ちゃんとDQLにはセットされていることが確認できた。
が、$q->getSqlQuery()すると相変わらず FROM Baker b, Able aのみ。どうやらDQLからSQLを生成する時にONが無視されてしまう様子。
ここでふと思い出した。Pear::DBを使ってSQLを勉強し始めた頃、FROMにテーブルを羅列してWHEREでJOIN条件書いてもほしいデータが取れてたなー。その後SQL実行速度の問題でちゃんとJOINを書くようになったんだけど。
【実験5】
だめもとで。
$q = Doctrine::getTable("Baker")->createQuery("b")->addFrom("Able a")->addWhere("b.out_id = a.id");
⇒実験3と同じエラー。でも、$q->getSqlQuery()ではちゃんとSQLが出て来るし、それをコピペしてphpmyadminに実行させたらちゃんと想定通りのデータが出た。
【実験6】
実験5ではAbleのデータを入れるコンポーネント(プロパティ)がないよ!って怒られてる気がしてきた。同時にBakerとJOINするAbleまたはCatのデータはBakerに取得用のメソッドを作ってあるのでAbleのデータを最初のSELECTで取らなくてもいいよね、と考える。
$q = Doctrine::getTable("Baker")->createQuery("b")->addFrom("Able a")->addWhere("b.out_id = a.id")->select("b.*");
⇒成功!!
ここまで来るのに休み休み(家事育児やりながら^^;)で4時間近くかかってしまった(汗
【結論】
スマートじゃない&MySQLエンジンがんばって!なSQLだけど、上記の方法でLEFT OUTER JOINっぽいことはできることがわかりました。
[OpenPNE3.x]op_include_yesnoヘルパーがすごい
2010.07.08 Thursday | category:symfony
超便利なヘルパーを見つけた(発掘した)ので感動が冷めないうちにまとめておきます。
何かに書いておかないとまた忘れちゃいそうなのでw
function op_include_yesno($id, $yesForm, $noForm, $options)
#定義はopPartsHelper内にあります。
$id…formを囲むdivに付けたいid名。HTMLのid名なので同一ページ内の他のidと干渉しなければ自由につけられます。例えばスケジュール削除確認の画面ならscheduleDeleteConfirmFormとか。まぁ好みで。
$yesForm…「はい」の場合に送信するformインスタンス。予めアクション側で作ってビューに渡しておく。⇒私はこのyesnoを主に<削除確認画面>で使ってるので、大抵の場合は単なるsfFormなのですが。何かのパラメータをhiddenで渡したいときにはhiddenFieldsを持ってるformを作って値をセットして渡せばOK。
$noForm…「いいえ」の場合に送信するformインスタンス。$yesFormと以下同文。
$options…オプションの連想配列。op_include_formを使ったことがあればほぼ同じオプション内容が使えます。ただし、送信先URLの指定がyes,no2つ分あるのでそれぞれ指定すべし。⇒前述の通り私は<削除確認画面>で使ってるので、yes_urlを削除実行のアクションのURL、no_urlを表示のアクションのURLにしてます。
yes_url…yesの場合($yesForm)の送信先URL
no_url…noの場合($noForm)の送信先URL
title…formを囲むdivのpartsHeading>h3に入るテキスト。無指定だとタイトルなしのブロックになる。⇒<削除確認画面>だと「予定削除の確認」とか。
body…yes,noボタンの上に表示したいテキスト。⇒<削除確認画面>なら「本当に削除しますか?」とか「削除します。よろしいですか?」とか。
yes_button…yesのボタンのvalue。「はい」とか「削除する」とか「出席する」とか。デフォルトは「はい」。
no_button…noのボタンのvalue。「いいえ」とか「中止する」とか「欠席する」とか。デフォルトは「いいえ」
op_include_formとの違いは、view.ymlによるカスタマイズの読み込み機構は付いてないところぐらい?まぁどうしても必要なら足せばいいけど。
すごい便利なのにバンドルされてるプラグインでもほとんど使われてないのが不思議。
皆さんもお試しあれ。
何かに書いておかないとまた忘れちゃいそうなのでw
function op_include_yesno($id, $yesForm, $noForm, $options)
#定義はopPartsHelper内にあります。
$id…formを囲むdivに付けたいid名。HTMLのid名なので同一ページ内の他のidと干渉しなければ自由につけられます。例えばスケジュール削除確認の画面ならscheduleDeleteConfirmFormとか。まぁ好みで。
$yesForm…「はい」の場合に送信するformインスタンス。予めアクション側で作ってビューに渡しておく。⇒私はこのyesnoを主に<削除確認画面>で使ってるので、大抵の場合は単なるsfFormなのですが。何かのパラメータをhiddenで渡したいときにはhiddenFieldsを持ってるformを作って値をセットして渡せばOK。
$noForm…「いいえ」の場合に送信するformインスタンス。$yesFormと以下同文。
$options…オプションの連想配列。op_include_formを使ったことがあればほぼ同じオプション内容が使えます。ただし、送信先URLの指定がyes,no2つ分あるのでそれぞれ指定すべし。⇒前述の通り私は<削除確認画面>で使ってるので、yes_urlを削除実行のアクションのURL、no_urlを表示のアクションのURLにしてます。
yes_url…yesの場合($yesForm)の送信先URL
no_url…noの場合($noForm)の送信先URL
title…formを囲むdivのpartsHeading>h3に入るテキスト。無指定だとタイトルなしのブロックになる。⇒<削除確認画面>だと「予定削除の確認」とか。
body…yes,noボタンの上に表示したいテキスト。⇒<削除確認画面>なら「本当に削除しますか?」とか「削除します。よろしいですか?」とか。
yes_button…yesのボタンのvalue。「はい」とか「削除する」とか「出席する」とか。デフォルトは「はい」。
no_button…noのボタンのvalue。「いいえ」とか「中止する」とか「欠席する」とか。デフォルトは「いいえ」
op_include_formとの違いは、view.ymlによるカスタマイズの読み込み機構は付いてないところぐらい?まぁどうしても必要なら足せばいいけど。
すごい便利なのにバンドルされてるプラグインでもほとんど使われてないのが不思議。
皆さんもお試しあれ。
sfWidgetFormSelectCheckbox, sfWidgetFormSelectRadioの日本語value対応(何年ぶりのsf記事??
2010.06.28 Monday | category:symfony
sfWidgetFormSelectCheckbox, sfWidgetFormSelectRadioでchoicesのvalue値(label値ではなく)としてマルチバイト文字列を使うと、いくつかの選択肢が表示されなくなる現象がありました。
原因としては、inputタグを作るとき(render?)にinputタグのid値をユニークなキーとして使うらしく、そのid値はnameとvalueを「_」で繋いで作るんだけど、sfWidgetForm内でマルチバイト文字列(まぁ日本語の文字列)は「_」に変更されてしまうので、偶々複数のchoicesのvalue値の中に文字列長が同じのがあると、1つにまとまってしまうみたい。
長々と文章で説明するより実例のほうがわかりやすいかな。
$choices = array("ほげ"=>"ほげ", "ふが"=>"ふが");
をcheckboxやradioのchoicesとして指定すると、出力は
checkboxなら □ふが <input type="checkbox" name="check" value="ふが" id="check___" /> ふが
radioなら ○ふが <input type="radio" name="radio" value="ふが" id="radio___" />
のみになります。つまり最後の1個だけ。
で、これを回避するためにはid値の生成メソッド=sfWidgetForm::generateId()を下記のように変更してみました。
/lib/vender/symfony/lib/widget/sfWidgetForm.class.php
255行目〜オリジナル
デメリットはinputのid値を使ってCSSでデザインを指定したいときには厳しい(出力されたinputタグをみて頑張って><)ことぐらい?
まぁ個々のinputにデザイン決めたいぐらいの時はchoicesのkey(value値として渡す値)をアスキー文字だけにして使ってください。
今回の私の案件ように、運営者が自分でフォームの選択肢を編集する(しかも、運営者は、言語ファイル未アップでtitle, bodyとフォームのラベルが表示されたら「プログラムのコードが出ってます!!」と泡食って連絡してくるぐらい英語オンチ)とか、そういう特殊な状況じゃなければ多分使わないと思いますが。
#そういえば、mysqlのenumってマルチバイト文字使えたっけ。enumに日本語文字列を指定して、doctrineのschemaからフォームを自動生成させたらどうなるんだろう。そういう時この修正方法が生きるかも???誰か人柱お願いします(笑)
何年ぶりにsymfony記事を書いたんだろう、自分…。
原因としては、inputタグを作るとき(render?)にinputタグのid値をユニークなキーとして使うらしく、そのid値はnameとvalueを「_」で繋いで作るんだけど、sfWidgetForm内でマルチバイト文字列(まぁ日本語の文字列)は「_」に変更されてしまうので、偶々複数のchoicesのvalue値の中に文字列長が同じのがあると、1つにまとまってしまうみたい。
長々と文章で説明するより実例のほうがわかりやすいかな。
$choices = array("ほげ"=>"ほげ", "ふが"=>"ふが");
をcheckboxやradioのchoicesとして指定すると、出力は
checkboxなら □ふが <input type="checkbox" name="check" value="ふが" id="check___" /> ふが
radioなら ○ふが <input type="radio" name="radio" value="ふが" id="radio___" />
のみになります。つまり最後の1個だけ。
で、これを回避するためにはid値の生成メソッド=sfWidgetForm::generateId()を下記のように変更してみました。
/lib/vender/symfony/lib/widget/sfWidgetForm.class.php
255行目〜オリジナル
// remove illegal characters255行目〜私の修正版
$name = preg_replace(array('/^[^A-Za-z]+/', '/[^A-Za-z0-9¥:_¥.¥-]/'), array('', '_'), $name);
// remove illegal characters一見してわかるとおりマルチバイト文字を「_」×文字数に置換してしまうのではなく、マルチバイト文字列を含むname + _ + value をmd5ハッシュ化してしまっただけです。
//$name = preg_replace(array('/^[^A-Za-z]+/', '/[^A-Za-z0-9¥:_¥.¥-]/'), array('', '_'), $name);
$name = preg_replace('/^[^A-Za-z]+/', '', $name);
if(preg_match('/[^A-Za-z0-9¥:_¥.¥-]/', $name)>0)
{
$name = md5($name);
}
デメリットはinputのid値を使ってCSSでデザインを指定したいときには厳しい(出力されたinputタグをみて頑張って><)ことぐらい?
まぁ個々のinputにデザイン決めたいぐらいの時はchoicesのkey(value値として渡す値)をアスキー文字だけにして使ってください。
今回の私の案件ように、運営者が自分でフォームの選択肢を編集する(しかも、運営者は、言語ファイル未アップでtitle, bodyとフォームのラベルが表示されたら「プログラムのコードが出ってます!!」と泡食って連絡してくるぐらい英語オンチ)とか、そういう特殊な状況じゃなければ多分使わないと思いますが。
#そういえば、mysqlのenumってマルチバイト文字使えたっけ。enumに日本語文字列を指定して、doctrineのschemaからフォームを自動生成させたらどうなるんだろう。そういう時この修正方法が生きるかも???誰か人柱お願いします(笑)
何年ぶりにsymfony記事を書いたんだろう、自分…。
xserverでCodeIgniterアプリを入れたら延々404に[解決済み]
2010.01.06 Wednesday | category:codeigniter
正月早々ですが、また次回ハマったらいやなのでメモ。
xserverに開発済みのCIアプリを入れて、一通りページを表示して確認していたところ、homeコントローラ配下の画面がことごとく404エラーになっていました。
なんでなんでー?とlogレベルをdebugに変更して/index.php/home/indexと直打ちしてアクセス
URI class initialized
404 notfound --> index
どうやらURIクラス初期化までOK、次のROUTERの初期化がうまくいってない(ここで404?)。
で、system/libraries/Router.phpを見る。
Router class initializedをログ出力する前に呼んでいるRouter::_set_routing()内で、URIクラスから渡されたURIを見てみる。…??あれ??/home/indexにアクセスしたのに/indexになってる。そりゃ404にもなる罠。
Routerクラスを元に戻して、system/libraries/URI.phpを見る。
URI::_fetch_uri_string()→URI::_parse_request_uri()と実際の処理部分を探して見る。
$_SERVER['REQUEST_URI']は/home/indexが入っている。OK。
その次、$fc_pathというのと$_SERVER['REQUEST_URI']を/でexplodeしたものを比較して、array_sliceしている。ここアヤシイ!!
$fc_pathってなに?と値を見てみたところ、index.php(フロントコントローラ)のパスが入っていました。
要するに、xserverのドキュメントルートのパスが/home/hogehoge/public_htmlのようになっているのに、デフォルトコントローラ名としてhomeを使ったのが原因。CIアプリをドキュメントルートでなく物理フォルダを切って配置した場合に対するCI側の対策が仇になっていた。
とりあえず、今回のブツは他へ移植する予定のないアプリなので、$fc_pathとの比較&array_sliceしている箇所をURI.php内で勝手にコメントアウト。
私はデフォルトコントローラをhomeとすることが多いのですが、前回xserver使ったときはCI使い初期に作ったアプリでデフォルトコントローラはwelcomeのままだったので、これにひっかからなかった模様。
この問題はURIルーティング方法としてREQUEST_URIを選択した場合のみ出てくるので、他の方法(PATH_INFOとかQUERY_STRING)にすれば問題ない模様。
xserverに開発済みのCIアプリを入れて、一通りページを表示して確認していたところ、homeコントローラ配下の画面がことごとく404エラーになっていました。
なんでなんでー?とlogレベルをdebugに変更して/index.php/home/indexと直打ちしてアクセス
URI class initialized
404 notfound --> index
どうやらURIクラス初期化までOK、次のROUTERの初期化がうまくいってない(ここで404?)。
で、system/libraries/Router.phpを見る。
Router class initializedをログ出力する前に呼んでいるRouter::_set_routing()内で、URIクラスから渡されたURIを見てみる。…??あれ??/home/indexにアクセスしたのに/indexになってる。そりゃ404にもなる罠。
Routerクラスを元に戻して、system/libraries/URI.phpを見る。
URI::_fetch_uri_string()→URI::_parse_request_uri()と実際の処理部分を探して見る。
$_SERVER['REQUEST_URI']は/home/indexが入っている。OK。
その次、$fc_pathというのと$_SERVER['REQUEST_URI']を/でexplodeしたものを比較して、array_sliceしている。ここアヤシイ!!
$fc_pathってなに?と値を見てみたところ、index.php(フロントコントローラ)のパスが入っていました。
要するに、xserverのドキュメントルートのパスが/home/hogehoge/public_htmlのようになっているのに、デフォルトコントローラ名としてhomeを使ったのが原因。CIアプリをドキュメントルートでなく物理フォルダを切って配置した場合に対するCI側の対策が仇になっていた。
とりあえず、今回のブツは他へ移植する予定のないアプリなので、$fc_pathとの比較&array_sliceしている箇所をURI.php内で勝手にコメントアウト。
私はデフォルトコントローラをhomeとすることが多いのですが、前回xserver使ったときはCI使い初期に作ったアプリでデフォルトコントローラはwelcomeのままだったので、これにひっかからなかった模様。
この問題はURIルーティング方法としてREQUEST_URIを選択した場合のみ出てくるので、他の方法(PATH_INFOとかQUERY_STRING)にすれば問題ない模様。
単価って難しい。
2009.07.29 Wednesday | category:ひとりごと
単価を上げるのって難しいですね。
へっぽこWEBデザイナー→付け焼刃WEBプログラマー
とジョブチェンジした直後に比べると知識も増えたと思うし、同じものを作るにしてもスピード速くなったと思うんです。
オブジェクト指向使いこなせるようになってきたし、使い回し用ライブラリの貯金(?)もたくさんできたので。DjangoとかsymfonyとかCIとか、フレームワークにも親しみましたし^^;
でも、発注主さんとの間で都度見積もりとはいえ1人日の単価がほぼ決まってしまってるので、工数の見積もり=案件全体の単価も決定 になってしまう。
いや、これからお初の発注主さんには高くつけられますけどw
最初の最初の「付け焼刃」から一歩ましになったか?程度のころから仕事貰ってた発注主さんだけに「単価上げます、イヤならバイバイー」とは言いづらい。
他のフリーの人で、人日・人月で見積もりしてる人たちはどうやって単価上げてるのかなぁ。
作業量を考えて「ちょっと割に合わなくない?」という請求書を書きながらふと思ったのでした。
へっぽこWEBデザイナー→付け焼刃WEBプログラマー
とジョブチェンジした直後に比べると知識も増えたと思うし、同じものを作るにしてもスピード速くなったと思うんです。
オブジェクト指向使いこなせるようになってきたし、使い回し用ライブラリの貯金(?)もたくさんできたので。DjangoとかsymfonyとかCIとか、フレームワークにも親しみましたし^^;
でも、発注主さんとの間で都度見積もりとはいえ1人日の単価がほぼ決まってしまってるので、工数の見積もり=案件全体の単価も決定 になってしまう。
いや、これからお初の発注主さんには高くつけられますけどw
最初の最初の「付け焼刃」から一歩ましになったか?程度のころから仕事貰ってた発注主さんだけに「単価上げます、イヤならバイバイー」とは言いづらい。
他のフリーの人で、人日・人月で見積もりしてる人たちはどうやって単価上げてるのかなぁ。
作業量を考えて「ちょっと割に合わなくない?」という請求書を書きながらふと思ったのでした。
さくらインターネットpython2.6にアップ&django1.0対応
2009.07.02 Thursday | category:django
ひっさしぶりにdjangoとpythonを触りました。
というのも、さくらインターネットさんがpythonを2.5→2.6とバージョンアップされて、自サイトが万年500エラー状態になってたのを発見したため、virtual-python環境構築しなおしの必要があり、ついでにdjangoのバージョンアップもやってしまえ!という状態になったからです^^
参考URL
http://djangoproject.jp/doc/ja/1.0/releases/1.0-porting-guide.html
ドキュメント翻訳のymasudaさん&その他のdjango-jaの皆様、ありがとうございますm(_ _)m
・文字列をユニコードにする。特に日本語の文字列。("にほんご"→u"にほんご")
・models.pyで使わなれなくなったオプションを削除(edit_inlineとか;admin用のオプションを削れば取りあえずそのまま動くっぽい)
・(contrib.adminを使っている場合のみ)admin.pyを新しく作る
・(contrib.adminを使っている場合のみ)urls.pyを修正
・(form関係を使っている場合のみ)新しいdjango.forms対応で書き直す?
とりあえず公開サイトとadminは元通り(?)動くようになりました。
このサイトはmodel定義5個のみ&formとか使ってないので、修正はmodelとurlsのみで非常に簡単でした。
作業時間15分ぐらい。
感想:
モデル数少なくてジェネリックビューor単純なビューだけなら移行は大したことない。
form使ってたりmodel定義が多いと大変そう。
リクエストとかレスポンスとか、DB APIを直接触ってたりとかすると、もっと大変そうーー;
(まだ0.97preのまま放置中のモデル大量の自サイトがもう一個あるのよね…仕事暇になったらやろう…)
というのも、さくらインターネットさんがpythonを2.5→2.6とバージョンアップされて、自サイトが万年500エラー状態になってたのを発見したため、virtual-python環境構築しなおしの必要があり、ついでにdjangoのバージョンアップもやってしまえ!という状態になったからです^^
参考URL
http://djangoproject.jp/doc/ja/1.0/releases/1.0-porting-guide.html
ドキュメント翻訳のymasudaさん&その他のdjango-jaの皆様、ありがとうございますm(_ _)m
・文字列をユニコードにする。特に日本語の文字列。("にほんご"→u"にほんご")
・models.pyで使わなれなくなったオプションを削除(edit_inlineとか;admin用のオプションを削れば取りあえずそのまま動くっぽい)
・(contrib.adminを使っている場合のみ)admin.pyを新しく作る
・(contrib.adminを使っている場合のみ)urls.pyを修正
・(form関係を使っている場合のみ)新しいdjango.forms対応で書き直す?
とりあえず公開サイトとadminは元通り(?)動くようになりました。
このサイトはmodel定義5個のみ&formとか使ってないので、修正はmodelとurlsのみで非常に簡単でした。
作業時間15分ぐらい。
感想:
モデル数少なくてジェネリックビューor単純なビューだけなら移行は大したことない。
form使ってたりmodel定義が多いと大変そう。
リクエストとかレスポンスとか、DB APIを直接触ってたりとかすると、もっと大変そうーー;
(まだ0.97preのまま放置中のモデル大量の自サイトがもう一個あるのよね…仕事暇になったらやろう…)
さくらインターネットdjangoが突然500エラー!?(Pythonバージョンアップされてた
2009.05.03 Sunday | category:django
なってたのでびっくりしました。
SSHで入ってpython -vしてみて納得。
pythonが2.4.3→2.5.2になってました。いつからだったんだろう…
独自ビルドをせずにvirtual-pythonを使ってたので、
・pythonのバイナリは2.5.2
・各種ライブラリは$HOME/lib/python2.4以下にあるので読めず
な状態になっていた。
1.とりあえず$HOMEでvirtual-pythonを再度実行。
$HOME/lib/python2.5以下に標準ライブラリが配置されます。
2.PIL,pysqlite2を再度インストール。
3.$HOME/django_src/djangoへのシンボリックリンクを、$HOME/lib/python2.5以下に生成しなおし
これでとりあえず回復しました。
どーせだからdjango1.x系に変更しようかと一瞬思ったのですが、連休のため子供達の遊んでコールを無視できず断念……
今のプロジェクト終わったら…といつものセリフを吐いて終わりにします(ダメ人間)
SSHで入ってpython -vしてみて納得。
pythonが2.4.3→2.5.2になってました。いつからだったんだろう…
独自ビルドをせずにvirtual-pythonを使ってたので、
・pythonのバイナリは2.5.2
・各種ライブラリは$HOME/lib/python2.4以下にあるので読めず
な状態になっていた。
1.とりあえず$HOMEでvirtual-pythonを再度実行。
$HOME/lib/python2.5以下に標準ライブラリが配置されます。
2.PIL,pysqlite2を再度インストール。
3.$HOME/django_src/djangoへのシンボリックリンクを、$HOME/lib/python2.5以下に生成しなおし
これでとりあえず回復しました。
どーせだからdjango1.x系に変更しようかと一瞬思ったのですが、連休のため子供達の遊んでコールを無視できず断念……
今のプロジェクト終わったら…といつものセリフを吐いて終わりにします(ダメ人間)
MySQL⇔PHPベンチマーク(mysql_・PDO・mysqli)+おまけ
2009.04.08 Wednesday | category:PHP
ご無沙汰です。
ちょっと思いつきでやってみました。
create table testtable
(
code varchar(10),
name varchar(10),
PRIMARY(code)
)
| code | name |
|00001|hoge |
| … |
|10000|hoge |
というテスト用のデータを用意。
SQLは
select name from testtable where code='09999'
PRIMARYとしてINDEXされているcodeカラムを対象に、1万件のデータを検索してたった1行×1カラムのデータを探します。
(一般的なベンチの手法とはかなり違うと思いますが)
DB接続の時間は含まずSQLを実行して、結果をフェッチして、その結果をvar_dumpするところまでの時間を計りました。
私の場合、WEBアプリでの利用が目的であって、実際にデータを取り出して使い回せる状態にするところまでDBアクセス層のお仕事だと思っているので。
また、自鯖ですがコマンドラインでなくApacheモジュール版PHPをブラウザから呼んで実行させています。
数字はPHPのmicrotime関数の出力を処理したもの。10回やった平均です。
PHP全体・鯖全体の負荷の問題もあるので数値としては全然正確じゃないと思いますが、一応の速度比較にはなるかと。
■結果
mysql関数:73.44マイクロ秒
PDO:113.49マイクロ秒
mysqli(クラス):51.28マイクロ秒
???:28.08マイクロ秒 ←
「???」って何だと思います?
実は、codeをキーに、nameを値に持つ連想配列を作って、$array['09999']が存在するかどうか調べて(isset)、存在すれば返す。という古典的な(?)方法のベンチです。
1万件の配列をメモリに読み込む方が、DBに問い合わせ投げるより早い!というのが意外でした。
(くだらなくてすみません;;)
今回扱いたいモノが、更新頻度があまり高くなく、読み込み頻度が圧倒的に高いデータなもので、こんな比較をしてみました。
更新のほうの処理を書くのがちと面倒ですが、配列ベースで実装しようかな、という結論に至りました。
ちょっと思いつきでやってみました。
create table testtable
(
code varchar(10),
name varchar(10),
PRIMARY(code)
)
| code | name |
|00001|hoge |
| … |
|10000|hoge |
というテスト用のデータを用意。
SQLは
select name from testtable where code='09999'
PRIMARYとしてINDEXされているcodeカラムを対象に、1万件のデータを検索してたった1行×1カラムのデータを探します。
(一般的なベンチの手法とはかなり違うと思いますが)
DB接続の時間は含まずSQLを実行して、結果をフェッチして、その結果をvar_dumpするところまでの時間を計りました。
私の場合、WEBアプリでの利用が目的であって、実際にデータを取り出して使い回せる状態にするところまでDBアクセス層のお仕事だと思っているので。
また、自鯖ですがコマンドラインでなくApacheモジュール版PHPをブラウザから呼んで実行させています。
数字はPHPのmicrotime関数の出力を処理したもの。10回やった平均です。
PHP全体・鯖全体の負荷の問題もあるので数値としては全然正確じゃないと思いますが、一応の速度比較にはなるかと。
■結果
mysql関数:73.44マイクロ秒
PDO:113.49マイクロ秒
mysqli(クラス):51.28マイクロ秒
???:28.08マイクロ秒 ←
「???」って何だと思います?
実は、codeをキーに、nameを値に持つ連想配列を作って、$array['09999']が存在するかどうか調べて(isset)、存在すれば返す。という古典的な(?)方法のベンチです。
1万件の配列をメモリに読み込む方が、DBに問い合わせ投げるより早い!というのが意外でした。
(くだらなくてすみません;;)
今回扱いたいモノが、更新頻度があまり高くなく、読み込み頻度が圧倒的に高いデータなもので、こんな比較をしてみました。
更新のほうの処理を書くのがちと面倒ですが、配列ベースで実装しようかな、という結論に至りました。
⇒ ビギナーシンフォニアン (05/26)
⇒ momo (02/03)
⇒ (02/03)
⇒ もも (01/04)
⇒ もも (01/04)
⇒ bonlife (12/23)
⇒ Tatsu (11/11)
⇒ もも (11/01)
⇒ hfunai (06/25)
⇒ 常山 (06/24)