PHPのsimplexml_load_file()でXMLを扱うときに注意すること
はじめに
XMLは、JSONと違って同じ名前の要素を複数持つことができるのですが、PHPのsimplexml_load_file関数で扱うときにちょっとめんどくさかったのでメモ。
もっといい解決策があるかもしれません。
実験
入力XML
list1.xml
<?xml version="1.0" encoding="utf-8"?> <data> <book> <title>Swiftではじめる iPhoneアプリ開発の教科書</title> <caption>Appleのプログラミング言語、「Swift」バージョン2対応の、iPhoneアプリ作成入門書です。</caption> <date>2015/10/30</date> </book> </data>
list2.xml
<?xml version="1.0" encoding="utf-8"?> <data> <book> <title>詳細! Swift 2 iPhoneアプリ開発 入門ノート</title> <caption>待ったなし! Swift 2</caption> <date>2015/11/14</date> </book> <book> <title>Swiftではじめる iPhoneアプリ開発の教科書</title> <caption>Appleのプログラミング言語、「Swift」バージョン2対応の、iPhoneアプリ作成入門書です。</caption> <date>2015/10/30</date> </book> </data>
list1.xmlはbookタグが1個、list2.xmlはbookタグが2個存在します。
コード1
<?php $xml1 = simplexml_load_file('./list1.xml'); $xml2 = simplexml_load_file('./list2.xml'); $xml_array1 = json_decode(json_encode($xml1), true); $xml_array2 = json_decode(json_encode($xml2), true); print_r($xml_array1); print_r($xml_array2);
SimpleXMLElementオブジェクトを配列に変換する方法は、公式ドキュメントのコメントを参考に。
json_decodeの第2引数をtrue
にすることで連想配列にしています。
結果
temp $ php xml2array1.php Array ( [book] => Array //連想配列 ( [title] => Swiftではじめる iPhoneアプリ開発の教科書 [caption] => Appleのプログラミング言語、「Swift」バージョン2対応の、iPhoneアプリ作成入門書です。 [date] => 2015/10/30 ) ) Array ( [book] => Array //連想配列の配列 ( [0] => Array ( [title] => 詳細! Swift 2 iPhoneアプリ開発 入門ノート [caption] => 待ったなし! Swift 2 [date] => 2015/11/14 ) [1] => Array ( [title] => Swiftではじめる iPhoneアプリ開発の教科書 [caption] => Appleのプログラミング言語、「Swift」バージョン2対応の、iPhoneアプリ作成入門書です。 [date] => 2015/10/30 ) ) ) temp $
book要素が1個の場合と2個の場合で結果が異なります。 これでは、viewに表示するときなどに困りますね。
コード2
<?php $xml1 = simplexml_load_file('./list1.xml'); $xml_array1 = json_decode(json_encode($xml1), true); print_r($xml_array1); foreach($xml_array1 as $key => $value) { // 連想配列だったら配列に変換 if (is_array($value) && array_values($value) !== $value) { if (array_values($value) !== $value) { $xml_array1 = array($key => array($value)); } } print_r($xml_array1);
結果
temp $ php xml2array2.php Array ( [book] => Array ( [title] => Swiftではじめる iPhoneアプリ開発の教科書 [caption] => Appleのプログラミング言語、「Swift」バージョン2対応の、iPhoneアプリ作成入門書です。 [date] => 2015/10/30 ) ) Array ( [book] => Array //要素1の連想配列の配列 ( [0] => Array ( [title] => Swiftではじめる iPhoneアプリ開発の教科書 [caption] => Appleのプログラミング言語、「Swift」バージョン2対応の、iPhoneアプリ作成入門書です。 [date] => 2015/10/30 ) ) ) temp $
これで同じ構造になりました。
おまけ
if (array_values($value) !== $value)
は何をしているのか。
公式ドキュメントからこの関数の説明を引用します。
array_values() は、配列 array から全ての値を取り出し、数値添字をつけた配列を返します。
つまり、配列の場合はarray_valuesを使っても何も変わりませんが、連想配列であれは値だけになると!==
となるので、連想配列かどうかを判定できます。
参考
ランダムにタスクを割り当ててSlackに通知するbotを作った
はじめに
先日公開された下記のスライドを社内に紹介したところ、とあるディレクターさんからランダムにタスクを割り当てるbotについて聞かれたので適当につくってみました。
www.slideshare.net
やったこと
とにかく簡単にタスクの登録がしたいということだったので、Googleスプレッドシートを使うことに。
tasksシートとmembarsシートに、それぞれタスクとメンバーを記入、そこからランダムに1件選択してSlackに通知するだけのシンプル仕様。
これをディレクターさんのGoogleスプレッドシートに登録して、任意の時間をスケジューリングしてもらいました。
かなり雑な作りですが、とりあえずは要望を満たせています。
お試し運用してもらって、もっと改善要望出てきたら対応しようと思います。
コード
function doPost() { var tasks = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('tasks'); var taskMax = tasks.getLastRow(); var task = tasks.getRange(getRandomNumber(2, taskMax), 1).getCell(1, 1).getValue(); var members = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('members'); var memberMax = members.getLastRow(); var member = members.getRange(getRandomNumber(2, memberMax), 1).getCell(1, 1).getValue(); var payload = { text : "<@" + member + "> " + task + "お願いします", username : 'タスク割り当てbot', icon_emoji:':dart:', link_names: 1 } var options = { method: 'post', payload : JSON.stringify(payload) }; var url = "https://hooks.slack.com/services/xxxxxxxxxx/xxxxxxxxxx/xxxxxxxxxx"; var response = UrlFetchApp.fetch(url, options); if (response.getResponseCode() != 200) { Logger.log(response); } } function getRandomNumber(min, max) { return Math.floor((Math.random() * ((max + 1) - min)) + min); }
2015年の振り返り
雑に2015年の振り返りした。
ここ数年、1年過ぎるのが加速度的に早くなっている気がする。
お仕事全般
去年よりは、コード書く仕事が多かった。
主にFuelPHPと戯れてた。
Qiita:Team
7月くらいからQiita:Teamが導入されたので、積極的に投稿してる。
社内ということもあって、メモ的なものも投稿したりしてハードルはかなり下げてるつもりなんだけど、まめに投稿してくれる人はまだまだ少ないので、しばらくはQiita:Teamおじさんとして活動していこうと思う。
社内勉強会
FuelPHPのコードリーディングを開催した。
業務が多忙になって参加者が減り、最終的に私個人の活動になってしまったのは残念。
社外勉強会など
勉強会はPHPカンファレンスなど気になったものに少しだけ参加した。
去年よりは多かったけど、家のこともあるから2,3ヶ月に一回くらいが限界かなー。
相変わらず自宅では時間が取りにくいので、1時間くらい早めに出社するのを続けてるけど、やりたいことに対して不足気味。
新しい言語はSwiftを11月から始めてみた。来年はなんかアプリ作ろう。
プライベート
会社以外の時間は、ほぼ息子さんと遊んでばっか。
話せる言葉も増えてきたし、アドベントカレンダーでも書いたけど読み聞かせも喜んでくれるので、一緒に遊ぶのが楽しい。
あと石垣島はサイコーだった。
反省など
Qiita:Teamでの情報共有はそこそこ頑張った気がする。
でも仕事が忙しくなってくると、余裕がなくなってダメ。
FuelPHP勉強会とか、もうちょいなんとかなったんじゃないかなー。
来年の目標
もうちょい自分の時間を捻出して、積ん読の消化とコードの読み書きの時間を増やす。
社内勉強会を復活させる。
Qiita:Teamおじさん業を続ける。
FuelPHPで別フィールドを参照するバリデーションルール
はじめに
このエントリーは、FuelPHP Advent Calendar 2015の11日目です。
FuelPHPのValidationクラスは、用意されたバリデーションルール以外にも独自のルールを簡単に追加することができます。
今までいくつも作ったことがありますが、今回ちょっとだけハマったのでコードを読んで理解した内容を残しておきます。
バージョン
FuelPHP 1.7.2
追加したいルール
2つのフィールドのうち、どちらか一方だけ必須入力としたい。*1
requred_with(ある項目が入力ありなら必須)の変形みたいな感じ。
他にも、選択肢の中からN個以上必須とか、ある項目の入力内容で次の項目のバリデーションが異なるとか、検査のために別のフィールドの内容が必要になるパターン全般に共通です。
うまくいかなかったバージョン
バリデーションメソッド
<?php public static function _validation_required_which($val, $field) { //現在アクティブなValidationオブジェクトのinput配列を取得 $input = Validation::active()->input(); if (!array_key_exists($field , $input)) { return false; //ここに引っかかった } //両方からっぽ if (Validation::_empty($input[$field]) and Validation::_empty($val)) { return false; } //両方指定あり if (!Validation::_empty($input[$field]) and !Validation::_empty($val)) { return false; } return true; }
呼び出し側(抜粋)
<?php $valid = Validation::forge(); $valid->add_callable('Util_Validations'); $valid->add('hoge', 'HOGE') ->add_rule('required_which', 'fuga'); $valid->add('fuga', 'FUGA') ->add_rule('required_which', 'hoge'); if (!$valid->run()) { $error = array(); foreach ($valid->error() as $key => $e) { $error[$key] = array( 'message' => $e->get_message(), 'type' => $e->rule ); } Session::set('error', $error); return Response::redirect('/input'); }
結果
一つめのフィールドである hoge
を検査した際に、 if (!array_key_exists($field , $input)) return false;
に引っかかってしまいました。
つまり、比較対象である fuga
が入力フォームの配列になかったということらしい。
念のため Input::post
を確認したところ問題なく取得。
原因
結論としては Validation::run()
の仕様を誤解していたためでした。
すべてのフィールドを追加したら、バリデーションを実行することが出来ます。 デフォルトでは $_POST ですが、入力を与えることで、上書きや拡張することが出来ます。 Validation - クラス - FuelPHP ドキュメント
これを読んで、引数無しで Validation::run()
を実行すると $_POST
、つまり Input::post
を渡したのと同等になると理解したのですが、実際には微妙に違っていて、検査するフィールドごとにInput::post
から取得していました。
自作メソッドでは Validation::input()
を使って input 配列を取得しているのですが、一つめのフィールド hoge
の検査時点では hoge
自身しか入っていませんでした。
コードを読んでみる
Validation::run() (抜粋)
<?php public function run($input = null, $allow_partial = false, $temp_callables = array()) { //省略 $this->validated = array(); $this->errors = array(); $this->input = $input ?: array(); // ここが array() ではなく、Input::post だと思ってた $fields = $this->field(null, true); foreach($fields as $field) // add_field()やadd() したフィールドを順に処理 { static::set_active_field($field); // convert form field array's to Fuel dotted notation $name = str_replace(array('[', ']'), array('.', ''), $field->name); $value = $this->input($name); // input配列から値を取得、配列内になかったらここで追加 if (($allow_partial === true and $value === null) or (is_array($allow_partial) and ! in_array($field->name, $allow_partial))) { continue; } try { foreach ($field->rules as $rule) { $callback = $rule[0]; $params = $rule[1]; $this->_run_rule($callback, $value, $params, $field); } if (strpos($name, '.') !== false) { \Arr::set($this->validated, $name, $value); } else { $this->validated[$name] = $value; } } catch (Validation_Error $v) { $this->errors[$field->name] = $v; if($field->fieldset()) { $field->fieldset()->Validation()->add_error($field->name, $v); } } } //省略 return empty($this->errors); }
Validation::input()
<?php public function input($key = null, $default = null) { if ($key === null) { return $this->input; // 引数なしだと現在のinput配列を返す } // key transformation from form array to dot notation if (strpos($key, '[') !== false) { $key = str_replace(array('[', ']'), array('.', ''), $key); } // if we don't have this key if ( ! array_key_exists($key, $this->input)) { // it might be in dot-notation if (strpos($key, '.') !== false) { // check the input first if (($result = \Arr::get($this->input, $key, null)) !== null) { $this->input[$key] = $result; } else { $this->input[$key] = $this->global_input_fallback ? \Arr::get(\Input::param(), $key, $default) : $default; } } else { // do a fallback to global input if needed, or use the provided default $this->input[$key] = $this->global_input_fallback ? \Input::param($key, $default) : $default; //ここでInput::paramから取得 } } return $this->input[$key]; }
global_input_fallback
というのは、Validationクラスの設定で、デフォルトtrue設定。
なので、input配列に存在しなかったらInput::param
から探します。*2
true にした場合で、入力された配列に値が見つからなかった場合、値は Input::param になります。 Validation - クラス - FuelPHP ドキュメント
呼び出し時にバリデーション対象を探して内部の配列に追加というのは、個人的にはちょっと行儀がよくない動きのように感じましたがどうなんでしょう。
使わない(ルールを適用しない)フィールドがたくさんある場合は効率いいのかな。
解決策その1
run(Input::post())
のように、引数に明示的にPOSTデータを渡してやれば実行時に全て保持しているので失敗しません。
でもルールによって呼び出し側で考慮しないといけないので、イマイチな気がします。
解決策その2
バリデーションメソッドを改良する。
<?php public static function _validation_required_which($val, $field) { $valid = Validation::active(); if ($valid->_empty($valid->input($field)) and $valid->_empty($val)) { return false; } if (!$valid->_empty($valid->input($field)) and !$valid->_empty($val)) { return false; } return true; }
Validation::active()->input();
としていたのを、Validation::active();
としてアクティブなインスタンスを取得して全てインスタンス経由で操作するように変更。
Validation::input()
を引数指定で呼び出すことにより指定フィールドが input 配列になかった場合に Input::post
を探しに行くようになり、期待どおりの動作が実現できました。
おまけ
Validationクラスで扱えるのは連想配列ということになっている*3のですが、今回の流れの途中でコードを読んでみたらちょっと違いました。
以下のサンプルの1つ目は動きますが、2つ目は add()
した時に例外が発生します。
<?php $valid = \Validation::forge(); $valid->add(1, 'hoge')->add_rule('required'); $valid->run(array('a', 'b'));
<?php $valid = \Validation::forge(); $valid->add(0, '1st element')->add_rule('required'); $valid->run(array('a', 'b'));
これは、Validation::add()
の実体である Fieldset::add()
が empty()
を使ってチェックしているから。
<?php public function add($name, $label = '', array $attributes = array(), array $rules = array()) { //省略 if (empty($name) || (is_array($name) and empty($name['name']))) { throw new \InvalidArgumentException('Cannot create field without name.'); } //省略
つまり、配列であっても0番目以外のフィールドには使えます。
バリデーションで連想配列ではなく配列を使いたいシーンがあるのかわからないですが、対策するならコアクラスを拡張して、 Validation::add()
を連想配列のみ通す、または Fieldset::add()
を Validation::_empty()
みたいに 0 と '0' を通すのどちらかですかね。
参考
我が家の読み聞かせ
はじめに
このエントリーは、お子さん、どんな本読んでる? Advent Calendar 2015 - Adventarの8日目です。
昨日の記事は、id:ohesotori
さんの 復刊ドットコムと、赤羽末吉の「おへそがえるごん」 #adventcalender2015 - ここはちょっと見せられない でした。
仕事がひと段落したので、せっかくだから何かAdventCalendarに参加しようと思って探していたところ素敵なテーマを見つけたので参加しました。
珍しく家族ネタです。
2歳の息子が好きな本の中から、悩みに悩んで1冊だけ紹介します。
お気に入りの一冊
あかたろうの1・2・3の3・4・5 (おにのこあかたろうのほん 1)
- 作者: 北山葉子
- 出版社/メーカー: 偕成社
- 発売日: 1977/03
- メディア: 単行本
- 購入: 1人 クリック: 2回
- この商品を含むブログ (21件) を見る
新しい本もたくさんあるのですが、年季の入ったこの本が、なぜかお気に入りです。
出版社の紹介文を引用します。
おにの子のあかたろうが外から帰ってくると、お母さんがいません。そこで、次から次へと電話をかけて、お母さんを追いかけます。 あかたろうの1・2・3の3・4・5 | 偕成社
お母さんを探して家中の部屋をのぞいたり、いろんなところに電話をかけるといった同じことを繰り返すところが楽しいみたいです。
あかたろうが部屋をのぞいてお母さんいないと言っているところでは「いなーい」、電話をすれば「でんわー」「もしもしー」、さかなやさんが出てくれば「さかなー」など、読み聞かせというよりは一緒になって楽しんでます。
よほどフレーズが気に入ってるようで、妻が息子を連れて友人宅にお邪魔した際もトイレのドアを開けて、お母さんいなーいとかやってたそうです。
読み聞かせについて
妻が読んであげることが多いですが、そこそこ早く帰れた平日や週末は私もやります。
ちょっと前までは読み聞かせをしようとしても絵本を自分で持ちたがり、こちらが読んでいてもお構いなしにページをめくりたがりました。
まだ内容が理解できず、目の前にある絵本というモノ自体に興味津々だったのでしょう。
いつの頃からか私や妻が話す間、手を出さずに聞いていられるようになってました。
最近では、お気に入りの本を持って私のところまで来ると、膝の上にちょこんと座って「おんでー(読んでー)」、読み終わると「もっかいおんでー」です。
これが数ターン続きます。
まあ、3回、4回となってくると、別のにしようよと思いますけど。
私が他のことをしていたり、本を手に取らなかったりすると腕をペシペシしながら怒ります。
怒ってるところも含めて、仕草と舌足らずなところががめっちゃかわいいです。
完全に親バカですね。
いつまでこんな風にせがんでくれるかはわからないけど、これからもたくさんの本を一緒に楽しみたいなと思ってます。
PDOでMySQLに接続するとエラー発生
事象
コード
事象を再現させるための実験コード。
といっても、普通にPDO接続するだけ。
<?php define('DSN','mysql:host=localhost;dbname=hoge'); define('DB_USER','hoge'); define('DB_PASSWORD','hoge'); try { $dbh = new PDO(DSN, DB_USER, DB_PASSWORD, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)); } catch (PDOException $e) { echo $e->getMessage(); exit; }
結果
SQLSTATE[HY000] [2000] mysqlnd cannot connect to MySQL 4.1+ using the old insecure authentication. Please use an administration tool to reset your password with the command SET PASSWORD = PASSWORD('your_existing_password'). This will store a new, and more secure, hash value in mysql.user. If this user is used in other scripts executed by PHP 5.2 or earlier you might need to remove the old-passwords flag from your my.cnf file
原因
PHP5.3からの下位互換のない変更点に該当しているため。
新しい mysqlnd ライブラリは、MySQL 4.1 用の41バイトの新しいパスワードフォーマットを使用します。 古い16バイトのパスワードを使うと、mysql_connect() 系の関数は次のようなエラーメッセージを生成します。"mysqlnd cannot connect to MySQL 4.1+ using old authentication"
http://php.net/manual/ja/migration53.incompatible.php
要するにMySQLのパスワード設定が古いからのようだ。
対応
管理者ユーザでパスワードを再設定してやればOK。
SET SESSION old_passwords=0; SELECT * FROM user WHERE user = 'hoge'; UPDATE user SET password = password('XXXXXXXXXXXX') WHERE user = 'hoge';
FuelPHPのUploadクラスを使用するときに注意すること
FuelPHPでファイルアップローダーを作ってた時にちょっとはまったこと。
バージョン
FuelPHP 1.7.2
現象
`upload_max_filesize`、`post_max_size`などを、1GBの設定にしても、400MBのファイルでエラーになる。
原因
FuelPHPのUploadクラスには個別の設定があり、そっちがデフォルトの300MBのままだった。
公式サイトのエラーの説明読んでてたらそれっぽいこと書いてあった。
UPLOAD_ERR_MAX_SIZE The file uploaded exceeds the maximum file size defined in the configuration.
http://fuelphp.jp/docs/1.7/classes/upload/usage.html#/error_constants