カテゴリー別アーカイブ: 汎用テク覚え書き

PHP、JavaScriptで可変長引数を、関数内で引き渡す

可変長引数が抱える心の闇

PHP、JavaScriptそれぞれの可変長引数サポートという記事で扱ったテクニック、可変長引数。関数定義でarg1,arg2,arg3…と続ける事無く、スマートに引数を取り出す事が出来ます。

しかしながら、引数にローカル変数を充てないという事は、関数内での引数の引き渡しで困難に出会います。

//PHP
function dullfunction(){
$args = func_get_args();
otherfunction($args);
}
 
//想定した動作をしてくれません。

動作が想定通りいかないのは、当たり前の事です。この書き方ですと、関数otherfunction()に配列$argsへの参照が与えられるだけで、したがってotherfunctionの中では$argsを参照した1つの引数が与えられたものと見なされてしまいます。

それならば、引数の一つ一つを$args[0],$args[1]…のように分けて渡せば良い!と思われるかもしれませんが、それは可変長引数という前提に忠実ではありません。

そこで、そもそも関数に引数として配列を渡せないのはおかしいと上司に叛旗を翻してみて下さい。数十社クビになった後、親切な上司が(もしいればですが)教えてくれます。

PHP、JavaScriptで配列を関数の引数として与える方法

PHP、JavaScriptともに、配列を要素分だけ引数として与える方法があります。

//PHP
function dullfunction(){
$args = func_get_args();
call_user_func_array("otherfunction", $args);
}

PHPでは、call_user_func_array()という関数を用います。第一引数には、関数の名前を指定します。このとき()はつけません。また、以下のように第一引数に無名関数をとる事も出来ます。

//PHP
function dullfunction(){
$args = func_get_args();
call_user_func_array(function(){$args2 = func_get_args(); print_r($args2);}, $args);
}
 
//無名関数の中でcall_user_func_array("dullfunction",$args2);とかやってはいけない

ところで、このcall_user_func_array()について、あちこちの説明でまるでユーザが定義した関数でないと動かないかのような誤解を与える表現をしていますが、別に普通の関数(var_dumpとか…)を与えても動いてくれます。便利ですよ。

お次はJavaScriptの場合です。

//JavaScript
function dullfunction(){
otherfunction.apply(this,arguments);
}

可変長だろうがそうでなかろうが、JavaScriptの引数がargumentsで取れるという事は既に説明しました。今回新しく登場したapplyというfunctionオブジェクトのメソッドは、メソッドを呼び出した関数の中の”this”キーワードに別の値を代入するという、火星人が作ったようなよくわからない働きをするメソッドですが、第二引数には配列またはオブジェクトで呼び出し元関数への引数を与える事が出来るようになっています。

//JavaScript
function.apply(引数1,引数2);
 
//引数1:関数内の"this"に充てるもの。ただ可変長引数を与えたいだけならthisを指定し
//この部分の働きを殺す。
//引数2:関数に与える引数

きっと親切に教えてくれる上司は火星人です。即刻辞表を突き付けましょう。


PHP、JavaScriptそれぞれの可変長引数サポート

PHP、JavaSciptの関数処理記述方法

PHPとJavaScriptでユーザ関数の定義をするとき、引数をもつ関数の場合には以下のような書式を用います。

//PHP
function dullfunction($arg1,$arg2){
return $arg1 + $arg2;
}
//JavaScript
function dullfunction(arg1,arg2){
return arg1 + arg2;
}

何の変哲も無い、プログラミング言語普遍の関数定義ですね。これは関数の引数が2つであると決まっている場合の書き方になります。arg1、arg2と書いているのが引数(argument)になります。

定義した関数の数より少ない引数を与える場合

上で例に挙げた関数では、そもそも処理内容的に引数が二つとも揃っていないとエラーになってしまいます。それならば、内部処理が2つの引数を必ずしも必要としない関数であれば、定義より少ない関数を与えても大丈夫なのか、という話ですが、PHPとJavaScriptの場合はそこら辺を柔軟に解決しています。

PHPの場合には、無視される可能性のある引数にデフォルト値を設定する事でエラーを抑えます。

//PHP
function dullfunction($arg1,$arg2=null){
return "ダルい";
}
 
dullfunction(); //これはエラー
dullfunction(100); //$arg1には100が与えられる。$arg2はnull。エラーにはならない。
dullfunction(100,200); //$arg1に100、$arg2に200が与えられる。エラーにはならない。

関数定義中の引数部分で代入が行えるPHPらしい(ある意味破天荒な)解決です。ただし、デフォルト値を与える引数は必ず与えない引数の後に書くようにしないといけません。

JavaScriptの場合、与えられた引数の数が関数定義より少なくても、全く問題がありません。

//JavaScript
function dullfunction(arg1,arg2){
return "ダルい";
}
 
dullfunction(); //問
dullfunction(100); //題
dullfunction(100,200); //ナシ

この辺が入門用言語としてポピュラーな理由かもしれません。WEB制作者の中には、JavaScriptは気付いたら使えるようになっていたという習得歴の方もいらっしゃるかもしれません。私もそうでした。

関数が受け付ける引数の数を無制限にしたいとき(可変長引数)

表題の通り、引数の数を無制限にしたい場合です。たとえば与えられた引数全ての和を求めるというのは、よくある処理です。

PHPの場合は、とりあえず引数を持たない関数として定義しておいて、関数内部から実際に与えられた引数にアクセスする関数を呼び出します。この、あらゆるものを関数として用意してしまうようなところがPHP的だと言えるのですが、まあそれはどうでも良い事ですね。

//PHP
function dullfunction(){
$sum = 0;
$num = func_num_args();
for($i=0;i<$num;$i++){
$sum += func_get_arg($i);
}
return $sum;
}
 
//もしくは…
function dullfunction(){
$sum = 0;
$arglist = func_get_args();
for($i=0;i<count($arglist);$i++){
$sum += $arglist[$i];
}
return $sum;
}
 
dullfunction(100,200...); //お好きな数どうぞ

func_num_args()という引数の数を返す関数と、func_get_arg()/func_get_args()という引数そのものをインデックス指定で/配列として 返す関数があります。

JavaScriptの場合、関数定義とは関数オブジェクトを作るということであり、関数オブジェクトには与えられた引数を自動的に格納するargumentsプロパティがあります。したがって、実は定義式での引数の数の宣言もへったくれも無いわけです。引数が何個来ようが、不真面目にargumentsプロパティにアクセスすればいいのです。

//JavaScript
function dullfunction(){
var sum = 0;
for(i=0;i<dullfunction.arguments.length;i++){
sum += dullfunction.arguments[i];
}
return sum;
}
 
dullfunction(100,200...); //お好きな数どうぞ

PHPもJavaScriptも、かなりゆるい言語であることがわかりましたね。そしてゆるさの性格の違いも、垣間見えたのではないかと思います。


JavaScriptがプロトタイプベース言語だということについて

オブジェクト指向言語も決して一枚岩でありません。西の横綱もいれば、西の高校生探偵もいます。このブログで扱う事の多いPHP、JavaScriptなどについて見てみますと、PHPはクラスベース言語というタイプに含まれるのに対して、JavaScriptはプロトタイプベース言語というタイプに含まれます。JavaScriptのプロトタイプベース言語の方が、幾分マイナーですので、こちらの特徴を追ってみましょう。

プロトタイプベース言語には、クラスの概念が無い

オブジェクト指向についてかじった事のある人間ならば、あらゆるものがオブジェクトで、そのオブジェクトには設計図たるクラスが必要で…といったようなオブジェクト指向のイロハに馴染んでいることと思います。一方JavaScriptが属するマイノリティ、プロトタイプベース言語には、オブジェクトの設計図たるクラスは存在しません。それでは、オブジェクトは何を元にして作られるのか、何を実体化してインスタンスをつくるのかという疑問が当然沸き上がる筈でしょう。

実は、JavaScriptにおけるインスタンス化は、既存のオブジェクトを丸々コピーすることによって行われます。コピー元もまた実体をもったオブジェクトなのです。

はい、メモリの無駄遣い発見!JavaScriptはとんだ低級言語だぜ!と思われた方は、オブジェクト指向の意義についてしっかりと理解された方だと思います。既存のオブジェクトをコピーするという方法では、もしプロパティやメソッドの実装部分が100個あったら、インスタンスそれぞれにつき100個を丸ごとコピーしなければなりません。それでは、毎度毎度新しいオブジェクトを一から作成しているのと何ら変わりません。そこで、プロトタイプ言語ではそうした問題を解決する方法として、プロトタイプオブジェクトという概念を用意しています。

JavaScriptでのクラス(仮)宣言

JavaScriptでクラスのようなもの、つまりインスタンスにコピーされるコピー元のオブジェクトを宣言する場合には、変数(クラス(仮)名)への関数リテラルの代入だけで済みます。

var Conan = function(){};

これで終わりです。右側に書かれた無名関数がコンストラクタになりますので、何かしらプロパティやメソッドを設けたい場合には、function(){}の中に書けば大丈夫です。

var Conan = function(powder){
this.powder = powder;
this.pero = function(){
if(this.powder == "青酸カリ"){
alert("これは…青酸カリ!");
} else {
alert("この料理を作ったのは誰だっ!");
}
};
};

これでクラス(仮)Conanにプロパティpowderとメソッドpero()ができました。インスタンスを作成する際に引数を与えることで、プロパティpowderを初期化できます。

var conan = new Conan("青酸カリ");
conan.pero();
 
//結果 これは…青酸カリ!

JavaScriptのプロトタイプ

さて、先ほど挙げた問題に戻ります。このままですと、Conanクラスのインスタンスをconan1,conan2,conan3…とどんどん作っていくことで、その都度powderプロパティとpero()メソッドの宣言が行われメモリの無駄になってしまいます。そこで、JavaScriptのクラス(仮)には暗黙のプロパティであるprototypeというものが用意されており、このプロパティ(正体はオブジェクト)への値の代入を行うことで、プロパティやメソッドの追加が行えるようになっています。

var Conan = function(powder){
this.powder = powder;
};
 
Conan.prototype.pero =  function(){
if(this.powder == "青酸カリ"){
alert("これは…青酸カリ!");
} else {
alert("この料理を作ったのは誰だっ!");
}
};
 
//宣言してもいないConanクラスのプロパティ prototype に値を入れてしまう

これで、先ほどと同じくインスタンスからメソッドを実行できるようになります。

どの辺りがメモリの節約になったのかと言うと、実はインスタンス側にはpero()メソッドがコピーされておらず、インスタンスからpero()メソッドが呼ばれると、インスタンスの持つprototypeオブジェクトへの参照を便りに、prototypeオブジェクト中の同名メソッドを呼ぶ仕組みになっているのです。
つまりどれだけプロトタイプ側にメソッドやプロパティを追加しても、インスタンス側にはプロトタイプへの参照しか残らないので、経済的だというわけですね。

いかがだったでしょうか。決してJavaScriptの属するプロトタイプベース言語が、クラスベース言語に劣った低級なものでないという事が分かったのではないでしょうか。実はプロトタイプベース言語はクラスベース言語へのアンチテーゼとして作られたという経緯があり、クラスベースのオブジェクト指向の弱点である、開発の段階が進むにつれて設計図であるクラスへの設計図としての要求が高くなってくる(→リファクタリングの必要性が出てくる)といったところをうまく解決して、機能の追加をアドホックに行えるという持ち味を醸しています。

ということで、JavaScriptは案外面白い奴です。


PHPとJavaScriptでURLのクエリストリングを評価する

クエリストリングというのは、ほうぼうのWEBサービスでよく見かける、URLの後ろについた情報を持った文字列です。たとえば、youtubeで動画の検索をするとき、URLの後ろに ?search_query=検索文字列 として、URLにユーザ検索の内容をつけて遷移後のページに渡しています。
このクエリストリングは、search_queryのような普遍的な語でなくても、勝手な語を選んでつけることができます。そもそも、HTMLでフォームを作って送信方法にGETを選ぶと、input要素のユーザが設定したname属性がストリングになっていますね。またinput要素が複数の場合は、&で結ばれています。これもクエリストリングの特徴です。

さて、このクエリストリングをプログラム側でキー値と値の組で取得するにはどうすればよいでしょうか。

PHPでクエリストリング

PHPの場合、大抵の文字列操作は標準の関数として装備されています。本当に。自分で作る前にまず探してみると、大抵便利なのがあります。再発明した車輪はゴミの日にそっと出しましょう。

1.URL文字列からクエリストリングを取り出す場合
//このようなURLがあったとき
$url = "http://akisi.tabiyaku.net/index.php?query1=値1&query2=値2";
 
//クエリ部分を抽出する関数
$query = parse_url($url,PHP_URL_QUERY);
//$queryは"query1=値1&query2=値2"
 
2.現在のページのURLからクエリストリングを取得する場合
$query = urldecode($_SERVER["QUERY_STRING"]);
//urldecodeで、%E3みたいなコードになっているのをデコードする
 
3.取得したクエリストリングをキーと値の組に
parse_str($query,$arr);
 
//$arr["query1"]は値1
//$arr["query2"]は値2

3で$arrを指定しなければ、通常の変数($query1,$query2)にそれぞれの値が入ります。

JavaScriptでクエリストリング

JavaScriptの場合には、車輪の開発者になる余地が大いにあります。何故PHPのような標準でクエリストリングを扱う関数が無いのでしょう?簡単な話です。JavaScriptは未来の言語だからです。未来の車には、車輪など必要ありません!

1.URL文字列からクエリストリングを取り出す場合
 
//このようなURLがあったとき
var url = "http://akisi.tabiyaku.net/index.php?query1=値1&query2=値2";
 
//クエリ部分を抽出する(処理的には、?を探して以降の文字列をとる)
var query = url.slice(url.indexOf("?")+1);
//queryは"query1=値1&query2=値2"
 
2.現在のページのURLからクエリストリングを取得する場合
var query = decodeURI(location.search);
//decodeURIで、%E3みたいなコードになっているのをデコードする
//ただし、先頭に?を含むところがPHPと異なるので、?なしで取得する場合
var query = decodeURI(location.search).substring(1);
 
3.取得したクエリストリングをキーと値の組に
var arr = new Array();
var qarr = query.split("&");
for(i in qarr){
arr[qarr[i].substr(0,qarr[i].indexOf("="))] = qarr[i].substr(qarr[i].indexOf("=")+1);
}
//arr["query1"]は値1
//arr["query2"]は値2

こういった具合です。

実はJavaScript自体の関数ラインナップはPHPより劣るものの、PHPと同様の充実度を誇るのがjQueryです。確実に世界のどこぞの誰かが、あなたより先に車輪を開発してプラグインにしています。たとえばクエリストリングの評価ならば、jquery.toObject.jsこちらを使えばよろしいでしょう。こんな何某のWEB制作日記とか言う場末ブログのブックマークは、今すぐゴミ箱に放り込みましょう。


File API FileReaderを使った画像のドラッグ&ドロップアップロード

HTML5で新たに搭載されたAPIを利用して、WordPressのメディアアップロード画面のようなドラッグ&ドロップでの画像アップロード機能を実現します。File APIのFileReaderとData URLを使ったAjax POSTの手順は、以前OnlyCSViewを作成したエントリを参考にしていただくとして、今回はさらにドラッグ&ドロップAPIを使ったローカルファイルのセットと、PHP側での受け取ったData URLの保存方法を説明します。

STEP0:HTML5におけるページ内要素のドラッグ&ドロップ

HTML5では、ブラウザ上でのネイティブのドラッグ&ドロップを実現させるための属性として、draggableおよびdropzoneが用意されました。理想的には、draggable=”true”と指定されたページ上の要素をぐぐっと掴んで、dropzone属性の指定がされた要素の上に移動できるという動作の実現が期待されているのですが、dropzone属性に対応しているブラウザがほぼ無いため、ドロップ側の挙動はJavaScriptで実装するしかありません。とりあえずJavaScriptは置いておいて、属性指定だけした動作サンプルを見てみましょう。

draggable=”true”を指定した要素
(対応ブラウザならぐぐっと掴める)
dropzone=”move”を指定した要素
(対応ブラウザならドラッグした要素を
ドロップで受け付ける)

これがページ内要素のドラッグ&ドロップです。おそらく動作していないでしょう(笑)。ちなみにdropzone属性の値は、ドラッグ中の要素を移動させる”move”以外にも、複製する”copy”、ドラッグされた要素のリンクを取得する”link”などがあります。絵に描いた餅ですね。

STEP1:ローカルファイルのドラッグ&ドロップ

今回作成するプログラムのように、ローカルファイルをドラッグ&ドロップする場合には、ドラッグ側にdraggable属性を設定しなくてもOS標準機能でドラッグができています。ただし、通常ブラウザ上にファイルをドロップしたときの処理は、例えば画像ファイルやテキストファイルであればウィンドウ全体を使ったプレビュー表示になりますので、この標準処理をドロップ側でキャンセルし、別の処理をイベントに紐づける必要があります。もちろんJavaScriptを使うことになります。

STEP2:ドロップを受け付ける要素を用意してAjaxイベント付加

では、ドロップを受け付ける要素にdropzone属性の代わりとなるイベントをつけましょう。

//jQueryのCDNでのインポート。ヴァージョンはあまり気にせず。
<script src="http://code.jquery.com/jquery-1.7.2.min.js"></script>
.
.
<script type="text/javascript">
$(function(){
//jQueryに無いイベントdataTransferを付加
$.event.props.push("dataTransfer");
 
//ドロップを受け付ける領域は、適当にclass名をdropzoneとしておく。
var dropzone = $(".dropzone");
 
//FileReaderオブジェクトが無いとそもそも動作しない
//非対応ブラウザではdropzoneをそもそも表示しない心遣い(必須ではありません)
if(!window.FileReader){
          dropzone.hide();
          return false;
        } else {
//心遣いしないのなら、本処理はここから
		dropzone.bind("dragover",function(event){
			event.preventDefault();
			event.stopPropagation();
			return false;
		});
//領域にファイルが入ったら、灰色にハイライトさせる心遣い(必須ではない)
		dropzone.bind("dragenter",function(event){			
			$(this).css("background-color","#CCC");
			return false;
		});
		dropzone.bind("dragleave",function(event){
			$(this).css("background-color","#FFF");
			return false;
		});
//心遣い終わり
 
//本処理
		 var handleDroppedFile = function(event){
			 var thisarea = $(this);
          var file = event.originalEvent.dataTransfer.files[0];
		  var type = file.type;
          var fileReader = new FileReader();
		  fileReader.readAsDataURL(file);
          fileReader.onload = function(event){
 
//心遣い担当
            thisarea.css("background-color","#FFF");
//終わり
			$.ajax({
				type : "POST",
				url : "dropupload.php",
				data: {"type" : type,
"data" : event.target.result},
				success : function(data){
//成功処理を書くならここに。"成功しました"と表示させるとか			
				},
				dataType : "json"
			});
		  }
		  fileReader.onerror = function(stuff){
//心遣い担当
            thisarea.css("background-color","#FFF");
//終わり
		  }
		  event.preventDefault();
		  event.stopPropagation();
          return false;
        }
		   dropzone.bind("drop", handleDroppedFile);
		}
});
</script>

dropzoneには$.bindでもってイベントを結びつけます。ここでは”dragover”,”dragenter”,”dragleave”,”drop”の4種類が出てきますが、領域にファイルが重なったときに処理を行わないのなら、”dragover”と”drop”だけで良いでしょう。dragoverでは、ファイルをブラウザのウィンドウ上にドラッグしたときの標準処理をpreventDefaultでキャンセルし、stopPropagationでイベントが下位要素にも伝わってしまうのを止めます。ちなみに、IEの場合下から上にイベントが伝わっていくらしく、またメソッドではなくプロパティで指定する(event.returnValue = false;/event.cancelBubble = true;)ようです。IE10ではFileReaderがサポートされるみたいなので、対応の手間が増えますね。

fileReaderからreadAsDataURLでファイルをDataURLにする辺りは、以前OnlyCSViewを作成したエントリを参考。今回は$.postではなく$.ajaxを使っているので注意です。

STEP3:PHP側でアップロードの実装

PHP側では、Data URLにされた画像の解凍に一手間が要ります。

//dropupload.php
 
<?php
$filetype = htmlentities($_POST["type"], ENT_QUOTES, "UTF-8");
if(preg_match("/jpeg/",$filetype)){
$filesuffix = ".jpg";
$mimeprefix = "data:image/jpeg;base64,";
} else if(preg_match("/gif/",$filetype)){
$filesuffix = ".gif";
$mimeprefix = "data:image/gif;base64,";
} else if(preg_match("/png/",$filetype)){
$filesuffix = ".png";
$mimeprefix = "data:image/png;base64,";
}
 
//とりあえず一意の名前になってほしいので、日付時刻をコピー先ファイル名にする。
 
$filename = date("YmdHis").$filesuffix;
 
//まあ、仮にアップロード画像を格納するディレクトリがuploads/だったとしたら
$filepathname = "uploads/".$filename;
 
file_put_contents($filepathname,base64_decode(str_replace($mimeprefix, '', $_POST["data"])));
?>

エラーのコールバック処理を入れなければこういう感じです。
base64_decodeという関数でData URLのbase64エンコードを解凍するのですが、このときURLのファイルタイプ部分を取り除いてやらないと、PHP側でうまくコピーできないというのがキモですよ。


PHPのforeachループで配列の要素を評価、除去する

今回はPHPの小ネタです。

PHPのforeachは、配列内の要素一つ一つに対して同じ処理を適用する際に便利です。構文は、以下の二種類です。

//構文1
foreach(配列 as 値を入れる変数){
処理
}
 
//構文2
foreach(配列 as キー値 => 値を入れる変数){
処理
}

構文1では配列内要素の値をそのまま順番に取得して変数に入れます。たとえば、”りんご”,”ごいさぎ”,”ぎんやんま”という3要素をもつ配列$Septemberがあったとき、

foreach($September as $value){
$value .= "?";
echo $value;
}

結果、りんご?ごいさぎ?ぎんやんま?が出力されます。これはあくまで値を変数に格納しているだけですので、元の配列$Septemberを処理終了後ダンプしてみても、内部の要素は”りんご”,”ごいさぎ”,”ぎんやんま”のままです。
元の配列の要素を変更する場合は、構文2を使います。

foreach($September as $key => $value){
$September[$key] .= "?";
}

これで$Septemberをダンプすると”りんご?”,”ごいさぎ?”,”ぎんやんま?”となっています。
この挙動は、元からキー値つきで宣言された配列に関しても同じです。

$September = array("果物" => "りんご","鳥" => "ごいさぎ","虫" => "ぎんやんま");
foreach($September as $key => $value){
$September[$key] .= "?";
}
 
//$Septemberは("果物" => "りんご?","鳥" => "ごいさぎ?","虫" => "ぎんやんま?")

配列の要素の改変についてはこれでOKですが、配列の要素を評価して削除する場合にはどのようにすればいいのか。要素を削除することでループのインデックスが変わってしまって不都合になるのではないかと心配しましたが、思いつくままunsetを使って、特に問題ない結果を得られました。

foreach($September as $key => $value){
if($September[$key] == "ごいさぎ"){
unset($September[$key]);
}
}
 
//$Septemberは、("りんご","ぎんやんま")

ただし、この場合配列のインデックスが歯抜けになってしまいますので、連番のインデックスに戻したい場合には、array_values関数を使わないといけません。
カンマ区切りで保存してあるデータを取り出し、explodeしたあとforeach内で評価、特定の値のものを削除してimplodeで元に戻す。といった処理の流れで使えそうです。


WordPressでプラグイン無しでThickBoxを使う方法

WordPressの比較的よく知られた隠し機能として、画像をクリックしたときにフロートウィンドウを出し拡大表示をするThickBoxを、プラグインなしで実現できるというものがあります。方法はまあまあ簡単で、<head>タグに囲まれた部分で、<?php wp_head(); ?>が呼ばれるより前に<?php add_thickbox(); ?>を呼んでおく。すると投稿中の画像の<a>タグにclass=”thickbox”を指定してやる事で、画像クリック→フロート拡大表示の機能が実現します。

ThickBox導入手順1

ThickBox導入手順1

ThickBox導入手順2

ThickBox導入手順2

LightBoxという呼び名が一般的な機能かもしれませんが、何故LightBoxと呼ばれているかというと、この機能を広く知らしめたのがLightBoxというプログラムだったからで、繰出鉛筆をシャープペンシルと呼ぶのに似ています。
標準でこの機能が有効化されていない理由は、ThickBox自体がディスコンになってしまっているためとのことです。WordPressの最近のアップデートが、プログラムのスリム化とそれに伴うサポート終了という傾向であるため、この機能もいずれはなくなってしまい、プラグイン導入が推奨されるようになるかもしれません。