毎回再帰処理を書くくらいなら、初めからこんなの作ってみるってのはどうよ?

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
CakePHPを使って開発なんかをしておりますとね。やたらめったら配列を使用する場面が多い。CakePHPとは、配列をこねくり回すフレームワークだと言っても良いくらい。多重配列。連想配列。CakePHPを使っていて配列は避けられない。

這い寄る配列。名状しがたい配列のようなもの。いつもすぐそばにあることがゆずれない配列のA・RA・SHI。それがCakePHPなのです。

なので、CakePHPで開発をしていると、再帰処理ってものを行なう場面がちょいちょい出て来ることがある。

僕の場合だと、そうっすねぇ……サニタイズを行なったり、文字のエンコードをするような場合ですかね。CSVのデータを取り込む際とか、SHIFT-JISの文字をUTF-8に変換するじゃん? 逆にCSVを作成するときはUTF-8をSHIFT-JISにするし。

配列が一重か、多くても二重にしかならないって絶対的に決まってるんなら良いんだけど、必ずしもそうとは限らない場合があると、再帰処理ってやつを行なう必要が出てくる。

だから僕も今まで、その必要が出て来るたびに、再帰処理の関数を作成していました。mb_convert_encodingを再帰処理する関数を作ったり、CakePHPにはサニタイズ用のクラスがすでに用意されてるんですけど、それを再帰的に使えるような関数を作ったり。あとは、配列とはちょっと違うけど、ディレクトリの中身を再帰的に検索する関数を作ったり。

でも、たいした工数ではないとは言え、出て来るたびに新しく関数作るのも何かあれだなーって思って、しかもそのほとんどは処理内容が一緒なわけですから、何か同じような関数をいくつも複製している気がして、それでコードの行数が増えて行くのは、あまり美しくないのかなー……と。

だから、もっと汎用的に、mb_convert_encodingとかmb_convert_kanaとかhtmlspecialcharsとかの再帰処理を共通で使えるような関数はないものかと。

一応、PHPにはそれを実現するための関数として、array_walk_recursiveなんてものも用意されているみたいんですが、今回の僕がやりたいことをカバーするには至りませんでした。

ってことで、ちょっと自作してみることにしました。需要があるのかは知らないけど、いつものようにこれを読んだ人が、もっと良い方法があるぜっていう場合に教えてくれることを願って、公開しておきます。

例によって前置きが長くなってしまいましたが、何をやりたいのかを一言で表すと、つまりこーゆーことです。



call_user_func_arrayを再帰的に使用したいことって、あると思います(キリッ)



まずはこいつを見てくれ

たいしたボリュームではないけど、100行くらいあるんで、こっちに用意したサンプルを見てくだせえ。

わざわざクラスにする必要あったのかってところが疑問ですが……まあ、作ってたらその方が良いような感じがしたんで、あまりお気にならさず。

自分で実際に動きを検証してみたいって場合には、このサンプルページからコードをコピってください。

実際の使ってみると、こんな感じになりやす。

まずはクラスのインスタンスを生成。あとは、使用したい関数とデータ、必要な引数をセットすればオーケー。



使い方は簡単

ページを行ったり来たりするの面倒だから、こっちでもちょっと動きを確認してみましょうか。

例えば、mb_convert_kanaを再帰的に使用する。今回は半角カタカナを全角カタカナに変換してみましょう。

//処理実行
$a = array('アイウエオ','ガギグゲゴ',array('パピプペポ','フライドポテト', array('ヴァヴァヴァ')));
$obj = new FuncRecursive();
$r = $obj->execute('mb_convert_kana', $a, 'KV', 'UTF-8');

//$rの中身
Array
(
  [0] => アイウエオ
  [1] => ガギグゲゴ
  [2] => Array
    (
      [0] => パピプペポ
      [1] => フライドポテト
      [2] => Array
        (
          [0] => ヴァヴァヴァ
        )

    )
)

本来、mb_convert_kanaで文字列を変換したい場合は、こう(↓)書きますよね。

$a = 'アイウエオ';
$r = mb_convert_kana($a, 'KV', 'UTF-8');

//$r
アイウエオ

なので、書き方はほぼ一緒です。ようは関数名を第一引数に持ってくれば良いだけ。

他の関数に関しても一緒っす。

//strip_tags
strip_tags($str);
=> $obj->execute('strip_tags', $str);

//mb_convert_encoding
mb_convert_encoding($str, 'SHIFT-JIS', 'UTF-8');
=> $obj->execute('mb_convert_encoding', $str, 'SHIFT-JIS', 'UTF-8');

//htmlentities
htmlentities($str, ENT_QUOTES, 'UTF-8');
=> $obj->execute('htmlentities', $str, 'ENT_QUOTES', 'UTF-8');

どうかしら? 普段とほぼ変わらない書き方になるようにしたから、そんなにイメージが掴みづらいってことはないと思うんですけど……。



ああ、もちろん、自分で作った関数をここで利用することもできますよ。

//自前の関数
function sampleFunc($data) {
    echo $data.'<br />';
}

//処理実行
$a = array('あいうえお','かきくけこ',array('食べられません', '冷やし中華始めませんか?'));
$obj->execute('sampleFunc', $a);

//実行結果
あいうえお
かきくけこ
食べられません
冷やし中華始めませんか?

今回、自作ではややこしい関数を作らなかったけど、引数の指定とかさえ間違えなければ、たぶんちゃんと動くはず。



ちなみに、再帰処理なんで基本的に配列を処理することを前提に作ってますが、配列じゃない場合でも動くようにはしてあります。

開発をしていたら、一つの処理の中で配列と文字列が同時に来る場合だって、あるかもしれないからね。



選べる3タイプ

コードの中を見てもらえれば分かるんですけど、$typeっていうメンバ変数があって、それによって処理を3つに分けています。それぞれ「string」「array」「map」で指定することができます。

何でそんなことしてるのかっつーと、「string」以外の2つはほぼ使うことはないと思うんですけど、それだと対応できない関数がいくつかあるんで、一応作っといた方が良いかなと。



例えば「implode」っていう関数がありますよね。配列の中身を一つの文字列に連結してしまう関数です。

あれって、文字列じゃなくて配列を処理するものなんで、再帰処理で文字列単位まで再帰してしまうと、正しく動かないんですね。

なので、この場合はタイプを「array」に変更します。

//チェーンジ、タイプarray、スイッチオン!
$obj->type = 'array';

//処理実行
$a = array('あいうえお','かきくけこ',array('食べられません', '冷やし中華始めませんか?'));
$r = $obj->execute('implode', $a, ',');

//$rの中身
あいうえお,かきくけこ,食べられません,冷やし中華始めませんか?

implodeを再帰的に使用する場合なんて、あんまりないと思いますけどね。僕は今回、たまたま使用するケースが発生したんですが……それはまた別の話。

本来、implodeって、一つ目の引数が連結文字で、二つ目の引数が配列なんですけど、今回は処理の関係上、逆になっちゃってます。

implode(',', $a);
=> $obj->execute($a, ',');

まあ、implode自体はどっちの書き方でもちゃんと動くんですけどね。「implode(‘,’, $a)」でも「implode($a, ‘,’)」でも、結果は一緒。PHPのマニュアルのページにも、歴史的な理由により引数をどちらの順番でも受けつけるって書いてあるし。

歴史的な理由って何さ?

どっちを第一引数にするかで、裁判沙汰にでもなったんですかね? 野菜か果物かで最高裁判所まで争ったトマトのように。

A氏「どう考えても第一引数に配列を持って来るべきだろう。他の関数だって、およそ変数は最初に持って来ているではないか!」

B氏「他もそうだからというそんな考えが、社会をダメにするのだ。なぜそれが分からぬ!」

A氏「こういう場合はなるべく統一しておいた方が使いやすい!」

B氏「いろんなパターンに慣れておいた方が、今後予想外のパターンが来た時にも柔軟に対応できる!」

裁判長「どっちでも大丈夫なようにすれば、万事解決じゃね? どちらかに決める必要なんてないわけだし」

A・B「……あ」

裁判長「これにて、いっけ〜んらくちゃく!」



あともう一つ。「map」ですね。

これはあれです。イメージ的には、「array_map」を再帰的に使うようなものです。array_mapって使ったことないから、本当にそうなのかちょっと自信ないけど。

array_mapは、それ自体が関数を再帰的に使用するものなんですけど、配列が三重、四重ってなると使えないので、今回はそういう場合でもいけるような処理を作ってみました。

んーと、例えばあれですね。動きの検証ページでも実際にやってますが、配列で来たファイル名の、拡張子を一挙に取得するようなことができます。

やりたいこととしては、「substr」で文字列を切り取るんですけど、ファイル名はそれぞれ文字の長さが違いますから、何バイト目から切り取るのかはみんな違う。そこで、まずは切り取るバイト数を指定するための配列を生成して、その配列を使って、各ファイル名の拡張子を取るって感じです。

//まずはタイプstringで切り取るバイト数を取得
$a = array('test.html', array('a' => 'sample.mp4', 'b' => 'norm-nois.com', array('abced.jp', 'test.png')), 'abc.com');
$b = $obj->execute('strrpos', $a, '.');

//チェーンジ、タイプmap、スイッ(ry)
$obj->type = 'map';

//処理実行
$r = $obj->execute('substr', $a, $b);

//実行結果
Array
(
  [0] => .html
  [1] => Array
    (
      [a] => .mp4
      [b] => .com
      [0] => Array
        (
          [0] => .jp
          [1] => .png
        )
    )

  [2] => .com
)

array_mapというのは、二つの配列を一対一対応で処理するような関数らしいんで、今回はそれに近い処理になったかなと。

//array_mapを使用
$a = array('a' => 'sample.mp4', 'b' => 'norm-nois.com');
$b = array(6, 9);
$r = array_map('substr', $a, $b);

//上記処理のときの動き(俺的イメージ)
$r['a'] = substr('sample.mp4', 6);
$r['b'] = substr('norm-nois.com', 9);



まあ、今ふと思ったけど、別にタイプmapを使わなくても、自前でstrrposとsubstrを同時に処理する関数を作ってタイプstringで処理すれば、結果は同じっぽいけどね。

function getExt($data) {
  $n = strrpos($data, '.');
  $r = substr($data, $n);
  return $r;
}

$obj->type = 'string';
$r = $obj->execute('getExt', $a);

こんなん。やってみたら同じになったわ^^

ま、まあ、もっと複雑な処理とかでね、タイプmapが役に立つことがあるかもしれないし、ないかもしれない^^;



ああ、そうそう。

このタイプmapのときだけど、例えば、同じ値を使用したい場合。えーっと、言葉で言うと分かりづらいか。

substrのとき、先頭から文字列を切り取る場合って、こんな風に書くじゃないですか。

substr($str, 0, 5);

この第二引数の0。配列の全ての要素に対して、この第二引数に同じ値を指定するなら、こんな風に書いても動きます。

$a = array('test.html', array('a' => 'sample.mp4', 'b' => 'norm-nois.com', array('abced.jp', 'eigjowk.png')), 'abc.com');
$b = $obj->execute('strrpos', $a, '.');

$obj->type = 'map';
$r = $obj->execute('substr', $a, 0, $b);

わざわざ一対一対応させるために、「array(0,array(0,0,array(0,0)),0)」なんて書き方はしなくても良し。






こんなんあると便利かもしんないなーっていう程度に作ったもんなので、最初の方にも書いたけど需要はそんなにないかもしれないし、もしかしたら俺が知らないだけで、こんなような処理を実行できるものが存在するのかもしれない。あればぜひ教えてもらいたいです。

僕が現況で使用する分には問題ないというものなので、まだ汎用性は低いかもしれないですが、もしこれを使ってみたいって思った人がいたら、動作を検証してみて、上手く動かなかった場合にはご一報ほしいっす。

よろしくお願いしゃっす。



ちなみに、今回実際に動作を検証してみた関数は、以下の通りっす。他の関数でも正常に機能するかどうかは未確認っす。特にarrayとmapは怪しいかも。

■タイプstring
strlen
mb_strlen
strip_tags
htmlentities
htmlspecialchars
mb_convert_kana
mb_convert_encoding
strpos
strrpos

■タイプarray
implode

■タイプmap
substr
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください