honto から Amazon.co.jp のカスタマーレビューを開くブックマークレット

javascript:void(function(d,w){r=d.evaluate('//li[contains(.,"ISBN:")]/text()',d,null,XPathResult.STRING_TYPE,null);if(r&&r.stringValue){w.open('http://www.amazon.co.jp/gp/search/?field-isbn='+encodeURIComponent(r.stringValue.substr(5).trim())+'#customerReviews');}})(document,window)

これに ISBN 検索で結果が一件だったら商品ページにリダイレクトする user.js を組み合わせて使用しています。

if (document.getElementById('s-result-count').firstChild.data.indexOf('1件') === 0) {
    location.href = 'http://www.amazon.co.jp/dp/' +
    document.getElementById('result_0').getAttribute('data-asin') +
    location.hash;
}

なんかやり口が古そうなのでつっこみ歓迎。

配列のキーキャストでハマりそうな例

注目すべきは、3つ目の要素のキーが string(2) "00" になっていること!!

フォームから送られてくる数値(のように見える文字列)とか、データベースでCHARで格納してるけど運用上数値しか入れてないのものなど、「配列のキーに入れればintになってくれるでしょ」と思い込んでるとちょっとハマるので注意しましょう、ということでした。

連想配列のキーに渡したときの暗黙のキャスト - いちいの日記

具体例を考えてみました。普段は困ることはなさそうですが、同じようなデータなのに持ち方が違うと割とハマりやすそうです。どちらも ID を格納しているけれど片や配列、片や DB のような。

$cart
SESSION とかに入った配列のおかいものかご
$id
カートに入れたい商品 ID のフォーム値
$db
DB インタフェースオブジェクト
一個だけ PEAR DB 固有だったので legacy だけどそれで
item
商品マスタテーブル

同じものが違う要素に

// array(商品 ID => 注文個数) なお買い物かご
$cart = array(
	20 => 1,
	23 => 1,
);

$id = '023'; // なんかまかり間違って 0 がついたままきちゃった

$statement = $db->prepare('SELECT * FROM item WHERE id = ?');
$result = $db->execute($statement, array($id));
if (!DB::isError($result) && $result->numRows() > 0) {
	// id カラムが int なので WHERE id = '023' で 23 のレコードが選択される
	// item は存在するからカートに入れる
	if (isset($cart[$id])) { // *ここが false*
		$cart[$id]++;
	} else {
		$cart[$id] = 1; // 新しく追加されてしまう
	}
}
  • 商品も配列で持っていれば、存在チェックでこける
    • isset($item_list[$id])
  • 商品 ID が int な cart テーブルに入れれば int になる
回避策
  • 取得した商品データの ID を使う
$item = $result->fetchRow(DB_FETCHMODE_ASSOC);
$id = $item['id'];

見つかるんだけど見つからない

$cart = array(
	20    => 1,
	'023' => 1, // さっきの状況で入ってしまった string な商品 ID
);

$item_list = array();

$args = implode(', ', array_map(array($db, 'smartQuote'), array_keys($cart)));
$result = $db->simpleQuery("SELECT * FROM item WHERE id IN ($args)"),
while ($item = $result->fetchRow(DB_FETCHMODE_ASSOC)) {
	$id = $item['id'];
	$item_list[$id] = $item;
	$item_list[$id]['order'] = $cart[$id]; // Undefined index: 23
}

なんかインデックスが効かない

  • 今度は逆に int に変換されることを考慮していない状況
  • id は固定長文字列で zero padding されているとする
$cart = array(
	'00331' => array('order' => 1),
	'15910' => array('order' => 1), // キーが int になる
);

$statement = $db->prepare('SELECT * FROM item WHERE id = ?');
foreach (array_keys($cart) as $id) {
	$result = $db->execute(array($id));

execute から呼ばれる quoteSmartis_int が true だと quote しないので

SELECT * FROM item WHERE id = '00331';
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | item  | const | PRIMARY       | PRIMARY | 7       | const |    1 |       |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+

SELECT * FROM item WHERE id = 15910;
+----+-------------+-------+------+---------------+------+---------+------+-------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows  | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+-------+-------------+
|  1 | SIMPLE      | item  | ALL  | PRIMARY       | NULL | NULL    | NULL | 99999 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+-------+-------------+

まとめ

  • 連想配列として array を使う場合はキーに数値使わないほうがいい
    • 配列地獄なコードだと便利すぎてつい使っちゃう
    • 使うときは大丈夫かな、くらいに意識しとく
  • 正規表現一発でちゃんと int になる数値っぽい文字列か、という validate がしにくいのでいまいち回避策が思い浮かばない
    • $cart[(int) $id] くらい ?
  • ハマるときは要因が重なり合って再現性が低かったりするので、今ギリギリ動いていても突然妙な挙動をしだす
  • あんま面白くなかった
  • 誰もこんな風に書かねーよ、と思ったけど探してみたらはまってたお買い物かごライブラリがあった