2012年11月16日金曜日

[PHP] PHPで特定のURLのHTTPステータスコードを取得する

リンク集のリンクが残念ながらサイト閉鎖や、URL変更などで404になることがある。
10個ぐらいなら手でチェックしてもいいが、100個、200個となるとさすがにうんざりする。
プログラムで調べるには、そのURLのHTTPステータスコードが404になるかどうか調べればよい。
なので、PHPでURLのHTTPステータスコードを返す関数をちょちょっと書いてみた。

//link・・・http://mio-koduki.blogspot.jp/2012/11/php-phpurlhttp.html
//第一引数・・・HTTPステータスコードを調べたいURL
//返り値・・・成功したらそのURLのHTTPステータスコード、失敗したらfalse
function get_http_status_code($url)
{
    if(($fp=fopen($url,'r',false,stream_context_create(array('http'=>array('ignore_errors'=>true)))))!==false)
    {
        fclose($fp);
        return preg_match('#^HTTP/\d\.\d (\d+) .+$#',$http_response_header[0],$matches)===1?(int)$matches[1]:false;
    }
    else
    {
        return false;
    }
}

とこんな感じになる。
file_get_contentsの代わりにfopenをつかってるのは、
file_get_contentsの場合はHTTPレスポンスボディを文字列に格納して返してくるので、
多少なりともメモリの消費が多くなるため、明示的に受け取る必要がないfopenを使ってる。
ただ、fopenでもfile_get_contentsでも、そのままだと401や404を返した際に返り値にfalseを返してくる。
それを防ぐために、fopenの第四引数にstream_context_createで作ったストリームコンテキストを渡してる。
この関数に連想配列で['http']['ignore_errors']にtrueを指定することによって
401や404の時にもきちんとファイルポインタリソースを返してくれるようにする。
また、突然初期化してない変数$http_response_headerを使ってるように見えるが、
じつはこれはPHPが自動的に代入してくれる変数で、
fopenなどの関数を使って通信が起こった際に(厳密にはHTTPラッパを使用する際に)、
HTTPレスポンスヘッダを配列で格納してくれる。
後は正規表現でHTTPレスポンスヘッダからHTTPステータスコードを取得して返しているだけである。

また、上と同じであるが、ちょっと見やすく整形したのがこっち。

//link・・・http://mio-koduki.blogspot.jp/2012/11/php-phpurlhttp.html
//第一引数・・・HTTPステータスコードを調べたいURL
//返り値・・・成功したらそのURLのHTTPステータスコード、失敗したらfalse
function get_http_status_code($url)
{
    $options=array
    (
        'http'=>array
        (
            'ignore_errors'=>true
        )
    );
    $fp=fopen($url,'r',false,stream_context_create($options));
    if($fp)
    {
        fclose($fp);
        $pattern='#^HTTP/\d\.\d (\d+) .+$#';
        if(preg_match($pattern,$http_response_header[0],$matches))
        {
            return (int)$matches[1];
        }
        else
        {
            false;
        }
    }
    else
    {
        return false;
    }
}

どちらがいいかはお好みで。

ポイントとなる変数・関数

2012年8月31日金曜日

[PHP][Qdmail] Qdmailで簡単に添付ファイル付きメール送信をする

メールを送信する際に添付ファイルをつけたいときがある。
そのときにも前回紹介したQdmailの出番だ。
高機能日本語PHPメール送信ライブラリ Qdmail
http://hal456.net/qdmail/
添付ファイルをつけて送信する方法も非常に簡単で
関数を使う方法

//件名に「件名」本文に「本文」というテキストメールを「to@example.com」へと「from@example.com」から「photo.png」を添付して送る方法
qd_send_mail
(
    'text',
    'to@example.com',
    '件名',
    '本文',
    'from@example.com',
    'photo.png',
);

とするだけ添付ファイル付きで送ることができる。
また、オブジェクトを使う方法

//件名に「件名」本文に「本文」というテキストメールを「to@example.com」へと「from@example.com」から「photo.png」を添付して送る方法
$mail=new Qdmail();
$mail->to('to@example.com');
$mail->subject('件名');
$mail->text('本文');
$mail->from('from@example.com');
$mail->attach('photo.png');
$mail->send();

として添付ファイル付きで送ることが可能だ。
もちろんどちらの方法でもOK。
このように非常に簡単に添付ファイルを送ることができる。

[PHP][Qdmail] Qdmailで簡単にメール送信をする

メールを送信するというのは要件定義としてよくあることだろう。
例えばショッピングカートなら注文確認メールを送ったり、
タスクスケジューラなら、アラートメールを送ったりなど。

もちろんPHPのデフォルトの関数であるmb_send_mailを使ってメール送信をしてもいいのだが、ちょっと複雑なことをしようとしたら(例えば添付メールなど)結構面倒くさい。
また、エンコードの問題で文字化けすることもあるため、
エンコード周りでも結構神経を使う。

そこでライブラリを使う人も多い。PEARなどが代表的だが、個人的にはQdmailを推したい。
高機能日本語PHPメール送信ライブラリ Qdmail
http://hal456.net/qdmail/
というのも、様々なエンコード問題に対応してくれ、HTMLメール、デコメール、添付ファイル、テンプレート機能などなど・・・。
様々な機能を提供してくれる。さらに使い方も非常に簡単だ。

とりあえず今回は簡単なメールの送信からしてみようと思う。
まず、関数として使う方法

//件名に「件名」本文に「本文」というテキストメールを「to@example.com」へと「from@example.com」から送る方法
qd_send_mail
(
    'text',
    'to@example.com',
    '件名',
    '本文',
    'from@example.com',
);

とするだけ送ることができる。
また、オブジェクトとして使う方法

//件名に「件名」本文に「本文」というテキストメールを「to@example.com」へと「from@example.com」から送る方法
$mail=new Qdmail();
$mail->to('to@example.com');
$mail->subject('件名');
$mail->text('本文');
$mail->from('from@example.com');
$mail->send();

として送ることも可能だ。
この辺は好みがあるだろうが、
オブジェクトを使った方法の方が後々様々な複雑なことができるので、
今のうちになれておくことをお勧めする。
とはいえ、関数を使った方法でも結構複雑なことができるので、
用途や好みに応じて使い分けるのもありだろう。

Qdmailで簡単に添付ファイル付きメール送信をする

2012年8月27日月曜日

[PHP][cURL] PHPのcURLを使ってGoogleにログインする

YAHOO! 知恵袋に(どなたか本当にお願いします!phpのcurlに関して教えて頂きたいです。)というPHPのcURLを使って、Googleにログインする方法が聞かれていたのでちょっと組んでみた。

実はこういうのは意外と面倒くさくて、
POSTデータでIDとパスワードを飛ばせばいいというものではない。
もちろんそれでログインできる(できてしまう)サイトもあるのだが、
セキュリティポリシーの高いサイトではそうはいかない。
不正なログインを防ぐためにフォーム内にトークンを埋め込み、
かつCookieにもそのトークンを埋め込んでおき、
サブミットされた際にフォームから飛んできたPOSTとCookieを比較しているのだ。
ちなみにGoogleとPixivはこの方式を採用している(2012/08/27現在)。

とりあえず早速ソースを見ていこう。

//URLを指定する
$url='https://accounts.google.com/ServiceLoginAuth';
//POST用のデータを作っておく
$data=array
(
    //ID部分(適宜置き換え)
    'Email'=>'',
    //パスワード部分(適宜置き換え)
    'Passwd'=>'',
    //ログインを維持するかのチェックボックス部分
    //'PersistentCookie'=>'yes',
);
//テンポラリファイルを作成する
$cookie=tempnam(sys_get_temp_dir(),'cookie_');

//cURLを初期化して使用可能にする
$curl=curl_init();
//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//文字列で結果を返させる
curl_setopt($curl,CURLOPT_RETURNTRANSFER,true);
//クッキーを書き込むファイルを指定
curl_setopt($curl,CURLOPT_COOKIEJAR,$cookie);
//URLにアクセスし、結果を文字列として返す
$html=curl_exec($curl);
//cURLのリソースを解放する
curl_close($curl);

//Document初期化
$dom=new DOMDocument();
//html文字列を読み込む(htmlに誤りがある場合エラーが出るので@をつける)
@$dom->loadHTML($html);
//XPath初期化
$xpath=new DOMXPath($dom);
//inputのtypeがhiddenの要素をとってくる
$node=$xpath->query('//input[@type="hidden"]');
foreach($node as $v)
{
    //POST用のデータに追加する
    $data[$v->getAttribute('name')]=$v->getAttribute('value');
}

//cURLを初期化して使用可能にする
$curl=curl_init();
//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//メソッドをPOSTに設定
curl_setopt ($curl,CURLOPT_POST,true);
//POSTデータ設定
curl_setopt($curl,CURLOPT_POSTFIELDS,$data);
//クッキーを読み込むファイルを指定
curl_setopt($curl,CURLOPT_COOKIEFILE,$cookie);
//Locationをたどる
curl_setopt($curl,CURLOPT_FOLLOWLOCATION,true);
//URLにアクセスし、結果を表示させる
curl_exec($curl);
//cURLのリソースを解放する
curl_close($curl);

//テンポラリファイルを削除
unlink($cookie);

という感じになる。
さて、実際に何をしているか見ていくと

//URLを指定する
$url='https://accounts.google.com/ServiceLoginAuth';
//POST用のデータを作っておく
$data=array
(
    //ID部分(適宜置き換え)
    'Email'=>'',
    //パスワード部分(適宜置き換え)
    'Passwd'=>'',
    //ログインを維持するかのチェックボックス部分
    //'PersistentCookie'=>'yes',
);
//テンポラリファイルを作成する
$cookie=tempnam(sys_get_temp_dir(),'cookie_');

の部分で必要な変数を用意している。
ID部分とパスワード部分は適宜置き換えて欲しい。

//cURLを初期化して使用可能にする
$curl=curl_init();
//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//文字列で結果を返させる
curl_setopt($curl,CURLOPT_RETURNTRANSFER,true);
//クッキーを書き込むファイルを指定
curl_setopt($curl,CURLOPT_COOKIEJAR,$cookie);
//URLにアクセスし、結果を文字列として返す
$html=curl_exec($curl);
//cURLのリソースを解放する
curl_close($curl);

の部分でフォーム内のトークンを分析するためのHTMLを取得する。

//Document初期化
$dom=new DOMDocument();
//html文字列を読み込む(htmlに誤りがある場合エラーが出るので@をつける)
@$dom->loadHTML($html);
//XPath初期化
$xpath=new DOMXPath($dom);
//inputのtypeがhiddenの要素をとってくる
$node=$xpath->query('//input[@type="hidden"]');
foreach($node as $v)
{
    //POST用のデータに追加する
    $data[$v->getAttribute('name')]=$v->getAttribute('value');
}

の部分でHTML解析を実際に行いトークンを取得し、POST用データ配列に入れる。

//cURLを初期化して使用可能にする
$curl=curl_init();
//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//メソッドをPOSTに設定
curl_setopt ($curl,CURLOPT_POST,true);
//POSTデータ設定
curl_setopt($curl,CURLOPT_POSTFIELDS,$data);
//クッキーを読み込むファイルを指定
curl_setopt($curl,CURLOPT_COOKIEFILE,$cookie);
//Locationをたどる
curl_setopt($curl,CURLOPT_FOLLOWLOCATION,true);
//URLにアクセスし、結果を表示させる
curl_exec($curl);
//cURLのリソースを解放する
curl_close($curl);

の部分で今まで作ったPOSTデータを飛ばしログインしている。

//テンポラリファイルを削除
unlink($cookie);

の部分でcookie保持用に用意したファイルを消しておく。
ただ、実際は放っておけばそのうち勝手に消えてしまうファイルなので、
わざわざ消す必要がないと言えばないのだが・・・

ポイントとなる関数
  • curl_init ・・・ cURL セッションを初期化する
  • curl_setopt ・・・ cURL 転送用オプションを設定する
  • curl_exec ・・・ cURL セッションを実行する
  • curl_close ・・・ cURL セッションを閉じる
  • DOMDocument クラス ・・・ HTML ドキュメントあるいは XML ドキュメント全体を表し、 ドキュメントツリーのルートとなります。
  • DOMXPath クラス ・・・ XPath 1.0 をサポートします。

2012年8月23日木曜日

[PHP][cURL] cURLでリダイレクトをたどるようにする

PHPはサイト解析するためにURLから情報を取得する方法がたくさんある。
その一つにcURLというのがある。
cURLではHTTPステータスコードが300系などで既に移転してしまったサイトなどは
リクエストのヘッダにLocationがあればリダイレクトしてたどっていく機能がある。

//URLを指定する
$url='google.co.jp';

//cURLを初期化して使用可能にする
$curl=curl_init();

//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//Locationをたどる
curl_setopt($curl,CURLOPT_FOLLOWLOCATION,true);
//最大何回リダイレクトをたどるか
curl_setopt($curl,CURLOPT_MAXREDIRS,10);
//リダイレクトの際にヘッダのRefererを自動的に追加させる
curl_setopt($curl,CURLOPT_AUTOREFERER,true);

//URLにアクセスし、結果を表示させる
curl_exec($curl);

//cURLのリソースを解放する
curl_close($curl);

とすればよい。
意外と自分でリダイレクトをたどり続ける処理を書くのは一手間なので、
こういう細かな機能はありがたい。

ポイントとなる関数
  • curl_init ・・・ cURL セッションを初期化する
  • curl_setopt ・・・ cURL 転送用オプションを設定する
  • curl_exec ・・・ cURL セッションを実行する
  • curl_close ・・・ cURL セッションを閉じる

[PHP][cURL] cURLで結果を文字列として受け取る

PHPは数多くの関数やクラスがあり、URLから結果を取得することができるものもある。
その一つにcURLがある。
cURLはデフォルトでは取得した結果をそのままブラウザやコンソールなどに出力するが、
文字列として受け取りたい場合がある。
その際には

//URLを指定する
$url='www.google.co.jp';

//cURLを初期化して使用可能にする
$curl=curl_init();

//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//文字列で結果を返させる
curl_setopt($curl,CURLOPT_RETURNTRANSFER,true);

//URLにアクセスし、結果を文字列として返す
$result=curl_exec($curl);

//cURLのリソースを解放する
curl_close($curl);

となる。
この後にDOM解析したり、特定文字列を検知したりとすることになるだろう。

ポイントとなる関数
  • curl_init ・・・ cURL セッションを初期化する
  • curl_setopt ・・・ cURL 転送用オプションを設定する
  • curl_exec ・・・ cURL セッションを実行する
  • curl_close ・・・ cURL セッションを閉じる

[PHP][cURL] cURLでSSL(https)のCA証明書警告の回避や設定

PHPは柔軟な言語で特定のサイトを取得するのに様々な手法がある。
その中でcURLという選択をとる人は多い。
色々と便利なcURLだが、SSL(つまりhttps)で外部のサイトへアクセスした場合

SSL certificate problem, verify that the CA cert is OK. Details:
error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed

というエラーが出る場合がある。
これはcURLの–with-ca-bundleオプションで指定されたパスか特定パスにCA証明書がないと起こるエラーだ。
もちろん–with-ca-bundleオプションでCA証明書を指定してもいいのだが、
できない環境(レンタルサーバ)などもあるだろう。
これを回避するには

//URLを指定する
$url='https://www.google.co.jp/';

//cURLを初期化して使用可能にする
$curl=curl_init();

//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//CA証明書の検証をしない
curl_setopt($curl,CURLOPT_SSL_VERIFYPEER,false);

//URLにアクセスし、結果を表示させる
curl_exec($curl);

//cURLのリソースを解放する
curl_close($curl);

としてやればよい。
また、CA証明書を持っているなら

//URLを指定する
$url='https://www.google.co.jp/';
//CA証明書へのパス
$path='./cacert.pem';

//cURLを初期化して使用可能にする
$curl=curl_init();

//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//CA証明書ファイルを指定
curl_setopt($curl,CURLOPT_CAINFO,$path);

//URLにアクセスし、結果を表示させる
curl_exec($curl);

//cURLのリソースを解放する
curl_close($curl);

としてやればよい。

ポイントとなる関数
  • curl_init ・・・ cURL セッションを初期化する
  • curl_setopt ・・・ cURL 転送用オプションを設定する
  • curl_exec ・・・ cURL セッションを実行する
  • curl_close ・・・ cURL セッションを閉じる

[PHP][cURL] cURLでリクエストヘッダを表示させる

PHPでは特定のサイトにアクセスするのに様々な手法があるが、
その中で便利なものの一つがcURLだ。
そのcURLで相手サイトに想定したパラメータなどが渡ってなくて、
思った通りの結果がとれないときがある。
そのときにはリクエストヘッダを出力してみるといいだろう。

//URLを指定する
$url='www.google.co.jp';

//cURLを初期化して使用可能にする
$curl=curl_init();

//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//リクエストヘッダ出力設定
curl_setopt($curl,CURLINFO_HEADER_OUT,true);

//URLにアクセスし、結果を表示させる
curl_exec($curl);

//リクエストヘッダ出力
echo curl_getinfo($curl,CURLINFO_HEADER_OUT);

//cURLのリソースを解放する
curl_close($curl);

とすることによってリクエストヘッダを出力させることができる。
ちなみに上記例では

GET / HTTP/1.1
Host: www.google.co.jp
Accept: */*


となっている。
ちなみに余談だが、HTTP1.1ではHostフィールドだけがリクエストヘッダで唯一の必須になっている。

ポイントとなる関数
  • curl_init ・・・ cURL セッションを初期化する
  • curl_setopt ・・・ cURL 転送用オプションを設定する
  • curl_exec ・・・ cURL セッションを実行する
  • curl_getinfo ・・・ 指定した伝送に関する情報を得る
  • curl_close ・・・ cURL セッションを閉じる

[PHP][cURL] cURLでクッキーを送受信する

PHPは特定のURLにアクセスしたりリクエストを送るのが非常に簡単だ。
とりわけcURLを使う方法は結構メジャーである。
cURLには通信のための様々なオプション設定ができるが、
クッキーを扱うためのオプションももちろんある。

相手からクッキーを受け取りたいときは

//URLを指定する
$url='www.google.co.jp';
//テンポラリファイルを作成する
$cookie=tempnam(sys_get_temp_dir(),'cookie_');

//cURLを初期化して使用可能にする
$curl=curl_init();

//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//クッキーを書き込むファイルを指定
curl_setopt($curl,CURLOPT_COOKIEJAR,$cookie);

//URLにアクセスし、結果を表示させる
curl_exec($curl);

//cURLのリソースを解放する
curl_close($curl);

//クッキーを表示する
echo file_get_contents($cookie);

//テンポラリファイルを削除
unlink($cookie);

となる。
ちなみにクッキーを受け取ったファイルの中身は

# Netscape HTTP Cookie File
# http://www.netscape.com/newsref/std/cookie_spec.html
# This file was generated by libcurl! Edit at your own risk.

ドメイン ドメインクッキーフラグ パス セキュアフラグ 有効期限 名前 値
ドメイン ドメインクッキーフラグ パス セキュアフラグ 有効期限 名前 値
ドメイン ドメインクッキーフラグ パス セキュアフラグ 有効期限 名前 値

となっている。
逆に相手にクッキーを送信したい場合は

//URLを指定する
$url='www.google.co.jp';
//クッキー情報の入ったファイルへのパス
$cookie='./cookie.txt';

//cURLを初期化して使用可能にする
$curl=curl_init();

//オプションにURLを設定する
curl_setopt($curl,CURLOPT_URL,$url);
//クッキーを読み込むファイルを指定
curl_setopt($curl,CURLOPT_COOKIEFILE,$cookie);

//URLにアクセスし、結果を表示させる
curl_exec($curl);

//cURLのリソースを解放する
curl_close($curl);

となる。
ちなみにpixivへの自動ログインを作るためには、
このクッキーの扱いをマスターするのが必須になってる。

ポイントとなる関数
  • curl_init ・・・ cURL セッションを初期化する
  • curl_setopt ・・・ cURL 転送用オプションを設定する
  • curl_exec ・・・ cURL セッションを実行する
  • curl_close ・・・ cURL セッションを閉じる

2012年5月17日木曜日

[PHP] 配列の前のキー、次のキーを取得する

配列で特定のキーの前のキー、もしくは次のキーを取得したい時がある。
キーさえわかってしまえば、前の値や次の値も簡単に取れる。
array_keysを使ってキーだけの配列を作り、
更にキーだけの配列に第二引数つきのarray_keysを使って取得するのもありだとは思うが、
配列が大きい時に、array_keysで更に配列を作ると効率が悪い。
そういう時は素直にforeachで調べるのがベター。

//link・・・http://mio-koduki.blogspot.jp/2012/05/php.html
//第一引数・・・調べたい配列
//第二引数・・・基準となるキー
//返り値・・・調べたい配列の、基準となるキーの前のキーが返る。なければfalse
function prev_key($array,$key)
{
    $tmp=false;
    foreach($array as $k=>$v)
    {
        if($k==$key)
        {
            return $tmp;
        }
        $tmp=$k;
    }
    return false;
}

//link・・・http://mio-koduki.blogspot.jp/2012/05/php.html
//第一引数・・・調べたい配列
//第二引数・・・基準となるキー
//返り値・・・調べたい配列の、基準となるキーの次のキーが返る。なければfalse
function next_key($array,$key)
{
    $tmp=false;
    foreach($array as $k=>$v)
    {
        if($k==$key)
        {
            $tmp=$k;
        }
        elseif($tmp!==false)
        {
            return $k;
        }
    }
    return false;
}

記述的にはarray_keysなどを使ったほうがスマートに見えるが、
処理速度やメモリなどを考慮するとやはりforeachで処理するのがベターだと思う。

2012年5月9日水曜日

[PHP] http_build_urlを実装する

PECLにhttp_build_urlというURLを組み立ててくれる便利な関数があるが、
レンタルサーバや、相手のサーバの都合などでPECLを導入できないことがある。
となると、自分で実装するしかない。
http_build_urlの使い方はPHPマニュアルに乗ってるので参考にするといいかもしれない。

//link・・・http://mio-koduki.blogspot.jp/2012/05/php-httpbuildurl.html

//PECLのhttp_build_urlがある可能性があるためチェックする
if(!function_exists('http_build_url'))
{
    //フラグの定数を設定
    define('HTTP_URL_REPLACE',1);
    define('HTTP_URL_JOIN_PATH',2);
    define('HTTP_URL_JOIN_QUERY',4);
    define('HTTP_URL_STRIP_USER',8);
    define('HTTP_URL_STRIP_PASS',16);
    define('HTTP_URL_STRIP_AUTH',24);
    define('HTTP_URL_STRIP_PORT',32);
    define('HTTP_URL_STRIP_PATH',64);
    define('HTTP_URL_STRIP_QUERY',128);
    define('HTTP_URL_STRIP_FRAGMENT',256);
    define('HTTP_URL_STRIP_ALL',504);

    function http_build_url($url,$parts=array(),$flags=HTTP_URL_REPLACE,&$new_url=array())
    {
        //置き換えるキー
        $key=array('user','pass','port','path','query','fragment');
        //urlをパースする
        $new_url=parse_url($url);
        //スキーマとホストが設定されていれば置き換える
        if(isset($parts['scheme']))
        {
            $new_url['scheme']=$parts['scheme'];
        }
        if(isset($parts['host']))
        {
            $new_url['host']=$parts['host'];
        }
        //フラグにHTTP_URL_REPLACEがあれば置き換える
        if($flags&HTTP_URL_REPLACE)
        {
            foreach($key as $v)
            {
                if(isset($parts[$v]))
                {
                    $new_url[$v]=$parts[$v];
                }
            }
        }
        else
        {
            //フラグにHTTP_URL_JOIN_PATHがあり新しいパスがあれば新しいパスをつなげる
            if(isset($parts['path'])&&$flags&HTTP_URL_JOIN_PATH)
            {
                if(isset($new_url['path']))
                {
                    $new_url['path']=rtrim(preg_replace('#'.preg_quote(basename($new_url['path']),'#').'$#','',$new_url['path']),'/').'/'.ltrim($parts['path'],'/');
                }
                else
                {
                    $new_url['path']=$parts['path'];
                }
            }
            //フラグにHTTP_URL_JOIN_QUERYがあり新しいクエリがあれば新しいクエリをつなげる
            if(isset($parts['query'])&&$flags&HTTP_URL_JOIN_QUERY)
            {
                if(isset($new_url['query']))
                {
                    $new_url['query'].='&'.$parts['query'];
                }
                else
                {
                    $new_url['query']=$parts['query'];
                }
            }
        }
        //ストリップフラグの判定をし、設定されていれば消す
        foreach($key as $v)
        {
            if($flags&constant('HTTP_URL_STRIP_'.strtoupper($v)))
            {
                unset($new_url[$v]);
            }
        }
        //パーツを繋げて返す
        return (isset($new_url['scheme'])?$new_url['scheme'].'://':'').(isset($new_url['user'])?$new_url['user'].(isset($new_url['pass'])?':'.$new_url['pass']:'').'@':'').(isset($new_url['host'])?$new_url['host']:'').(isset($new_url['port'])?':'.$new_url['port']:'').(isset($new_url['path'])?$new_url['path']:'').(isset($new_url['query'])?'?'.$new_url['query']:'').(isset($new_url['fragment'])?'#'.$new_url['fragment']:'');
    }
}

という感じになる。
しょっちゅう使うものではないにしても、
たまにあると便利な時がある。

ポイントとなる関数
  • http_build_url ・・・ URL を組み立てる
  • parse_url ・・・ URL を解釈し、その構成要素を返す

2012年4月25日水曜日

[PHP][Excel][PHPExcel] 作成したExcelをダウンロードする

PHPExcelで作成したExcelをダウンロードしたい時がある。
一旦適当なフォルダにExcelを作ってそれをダウンロードさせてもいいが、
フォルダにパーミッションを設定したりダウンロードし終わったExcelを削除したりと、
余計な手間が多い。

そういう時は直接Excelをダウンロードさせるといい。

//ClassesをおいたパスからPHPExcel.phpをインクルードする
require_once('Classes/PHPExcel.php');
//Excelのクラスを作る
$excel=new PHPExcel();
//アクティブなシートを取得する
$sheet=$excel->getActiveSheet();
//シートに名前をつける
$sheet->setTitle('test');
//A1参照で値を入れる
$sheet->setCellValue('A1','test');

//ヘッダを設定する
header('Content-Type: application/octet-stream');
//ダウンロードするときのファイル名を指定する
//.xlsx形式でDLするときは拡張子をxlsxに変えておく
header('Content-Disposition: attachment;filename="test.xls"');
//.xls形式で書き込むためのクラスを作る
//.xlsx形式を書き込む場合は'Excel5'の部分を'Excel2007'に変える →$writer=PHPExcel_IOFactory::createWriter($excel,'Excel2007');
$writer=PHPExcel_IOFactory::createWriter($excel,'Excel5');
//出力バッファへの書き込む
$writer->save('php://output');
exit;

こうすることによって、
一旦ファイルを作ったりすることなくダウンロードさせることが出来る。
なお
header('Content-Type: application/octet-stream')
の代わりに
header('Content-Type: application/vnd.ms-excel')
を指定することもあるが、ブラウザやバージョン、設定によって挙動が異なってしまうため、
あまりお勧めはしない。
ただ、あえてブラウザでExcelを開けるときは開きたいという場合は指定することもある。

2012年4月23日月曜日

[PHP][Excel][PHPExcel] ボーダーをまとめて設定する

PHPExcelの関連サイトを見ると一つのセルに対して上下左右のボーダーにつけるときに

//ClassesをおいたパスからPHPExcel.phpをインクルードする
require_once('Classes/PHPExcel.php');
//書き込むExcelのパス
define('EXCEL','data/test.xls');
//Excelのクラスを作る
$excel=new PHPExcel();
//アクティブなシートを取得する
$sheet=$excel->getActiveSheet();
//シートに名前をつける
$sheet->setTitle('border test');
//A1参照で値を入れる
$sheet->setCellValue('A1','test');
//セルの上に罫線を引く
$sheet->getStyle('A1')->getBorders()->getTop()->setBorderStyle(PHPExcel_Style_Border::BORDER_THIN);
//セルの下に罫線を引く
$sheet->getStyle('A1')->getBorders()->getBottom()->setBorderStyle(PHPExcel_Style_Border::BORDER_THIN);
//セルの左に罫線を引く
$sheet->getStyle('A1')->getBorders()->getLeft()->setBorderStyle(PHPExcel_Style_Border::BORDER_THIN);
//セルの右に罫線を引く
$sheet->getStyle('A1')->getBorders()->getRight()->setBorderStyle(PHPExcel_Style_Border::BORDER_THIN);
//.xls形式で書き込むためのクラスを作る
//.xlsx形式を書き込む場合は'Excel5'の部分を'Excel2007'に変える →$writer=PHPExcel_IOFactory::createWriter($excel,'Excel2007');
$writer=PHPExcel_IOFactory::createWriter($excel,'Excel5');
//ファイルに書き込む
$writer->save(EXCEL);

としていることが多い。
もちろんこれでも誤りではないし、きちんとボーダーは設定できるのだが、
いかんせんセル1つならまだしも数が多いと大変である。

実は

//ClassesをおいたパスからPHPExcel.phpをインクルードする
require_once('Classes/PHPExcel.php');
//書き込むExcelのパス
define('EXCEL','data/test.xls');
//Excelのクラスを作る
$excel=new PHPExcel();
//アクティブなシートを取得する
$sheet=$excel->getActiveSheet();
//シートに名前をつける
$sheet->setTitle('border test');
//A1参照で値を入れる
$sheet->setCellValue('A1','test');
//セルの上下左右全てに罫線を引く
$sheet->getStyle('A1')->getBorders()->getAllBorders()->setBorderStyle(PHPExcel_Style_Border::BORDER_THIN);
//.xls形式で書き込むためのクラスを作る
//.xlsx形式を書き込む場合は'Excel5'の部分を'Excel2007'に変える →$writer=PHPExcel_IOFactory::createWriter($excel,'Excel2007');
$writer=PHPExcel_IOFactory::createWriter($excel,'Excel5');
//ファイルに書き込む
$writer->save(EXCEL);

とすることでまとめてボーダーを設定することが出来る。

覚えておいて損はないと思う。

2012年4月16日月曜日

[PHP] 現在のURLを取得する

たまに現在のURLを取得したい時がある。
しかし残念ながらPHPに一発で取得する方法は今のところない。
なので、地道にサーバ変数から作っていくことになる。

//link・・・http://mio-koduki.blogspot.jp/2012/04/php-url.html
//返り値・・・成功した場合は現在のURL、失敗した場合はfalse
function get_current_url()
{
    $host=getenv('HTTP_HOST');
    $uri=getenv('REQUEST_URI');
    if(strval($host)===''||strval($uri)==='')
    {
        return false;
    }
    $https=getenv('HTTPS');
    return 'http'.(isset($https)&&strval($https)!==''&&strtolower($https)!='off'?'s':'').'://'.$host.$uri;
}

というふうになる。
ちなみにgetenv()のところはもちろん$_SERVERでも構わないが、
$_SERVERが定義されてない際にE_NOTICEを出さないために使ってる。

また、getenv('HTTPS')HTTPSの判定はPHPの$_SERVERマニュアルを見てみると
スクリプトが HTTPS プロトコルを通じて実行されている場合に 空でない値が設定されます。
注意: ISAPI を IIS で使用している場合は、HTTPS プロトコルを通さないでリクエストが行われたときの値は off となることに注意しましょう。
と書いてあるため、上記のようなチェックにしてある。
ただし、世には「shttp」 とちょっと変わり種や「ftp」などのプロトコルもあるが、
基本必要ではないため「http」と「https」にのみ対応している。

また限定的な場合であるが、特定のSSLアクセラレータを使用しているサーバでは
https通信を行ってもHTTPSに値が設定されない。
その場合は残念ながらPHPではhttps通信をキャッチできない。

ポイントとなる関数
  • getenv ・・・ 環境変数の値を取得する

2012年4月3日火曜日

[PHP] 連想配列かどうか調べる

PHPは非常に型に柔軟なため、
添字配列(いわゆるキーが0から始まる連番の配列)でも
連想配列(キーが文字列など)でも
基本的に同じように扱うことができる。

ただ、たまに添字配列なのか連想配列なのかを判定したくなる時がある。
チェックする方法としてはキーが数字の連番になってるかを調べたら良いので

//link・・・http://mio-koduki.blogspot.jp/2012/04/php-0-linktruefalsefunction-ishasharray.html
//第一引数・・・調べたい配列
//返り値・・・連想配列であればtrue、でなければfalse
function is_hash($array)
{
    return array_keys($array)!==range(0,count($array)-1);
}

というふうになる。
ただし、array_keysとrangeで2個新たに調べたい配列と同数の要素を持つ配列を作るので、
配列サイズが大きいと無駄なメモリを食うことになる。
そういう時は

//link・・・http://mio-koduki.blogspot.jp/2012/04/php-0-linktruefalsefunction-ishasharray.html
//第一引数・・・調べたい配列
//返り値・・・連想配列であればtrue、でなければfalse
function is_hash($array)
{
    $i=0;
    foreach($array as $k=>$v)
    {
        if($k!==$i++)
        {
            return true;
        }
    }
    return false;
}

素直にforeachで回しながら、連番になってるかチェックすれば良い。

ポイントとなる関数
  • array_keys ・・・ 配列のキーすべて、あるいはその一部を返す
  • range ・・・ ある範囲の整数を有する配列を作成する

2012年3月22日木曜日

[PHP] 配列の最初と最後のキーまたは値を調べる

あんまり機会としては少ないとは思うが、稀に配列の最初の要素のキー(添字)や値を調べたくなる時がある。
添字配列であれば最初は0から始まりcount($array)-1で終わっているはずだが、
連想配列としてキーが文字列だったり、添字配列に連想配列が混ざったりしていると、
なかなかそうはいかない。

そういう時は配列のポインタを利用する。

配列は実は内部に現在どこの要素を指しているかを調べるポインタを持っているのだ。
foreachなんかはそのポインタを1個1個移動させて中の要素を取り出している。

故にポインタを最初の位置と最後の位置に移動させて、キーや値を取得すれば良い。

//link・・・http://mio-koduki.blogspot.com/2012/03/javascriptphp_12.html
//第一引数・・・最初のキーを取得したい配列
//返り値・・・最初のキー
function get_first_key($array)
{
    reset($array);
    return key($array);
}

//第一引数・・・最初の値を取得したい配列
//返り値・・・最初の値
function get_first_value($array)
{
    return reset($array);
}

//第一引数・・・最後のキーを取得したい配列
//返り値・・・最後のキー
function get_last_key($array)
{
    end($array);
    return key($array);
}

//第一引数・・・最後の値を取得したい配列
//返り値・・・最後の値
function get_last_value($array)
{
    return end($array);
}

array_shiftやarray_popでも最初、最後の値を取得することはできるが、
これらは元になった配列から値が削除されてしまうため、予期しない挙動になることがある。

しかし、これらを利用して

//最初のキー
$keys=array_keys($array);
array_shift($keys);

//最初の値
$values=array_values($array);
array_shift($values);

//最後のキー
$keys=array_keys($array);
array_pop($keys);

//最後の値
$values=array_values($array);
array_pop($values);

ということも出来なくはない。
これらを組み合わせて

//最初のキー
$keys=array_keys($array);
reset($keys);

//最初の値
$values=array_values($array);
reset($values);

//最後のキー
$keys=array_keys($array);
end($keys);

//最後の値
$values=array_values($array);
end($values);

でもOK
また下記のように一行でもかけるが、
E_NOTICEレベルエラーが出るので、
E_NOTICEレベルエラーを表示してるとちょっと邪魔臭い。

//最初のキー
reset(array_keys($array));

//最初の値
reset(array_values($array));

//最後のキー
end(array_keys($array));

//最後の値
end(array_values($array));

こんな感じ。

2012年3月16日金曜日

[PHP][JavaScript] Ajaxからの通信(アクセス)かどうか判定する

ファイルへのアクセスがAjaxから要求されたものなのか、
それともブラウザからアドレスを直接叩くなどしてAjaxを通さずにアクセスしてきたのか調べたい時がある。

jQueryやprototypeなどのライブラリを使ってAjax通信をするときは実はヘッダに
「X-Requested-With:XMLHttpRequest」
というデータをつけて送ってくる。
なので、ヘッダ情報に「X-Requested-With」があるかどうかを調べて
それが「XMLHttpRequest」という値になってるかをチェックすればいい。

//link・・・http://mio-koduki.blogspot.com/2012/03/phpjavascript-ajax.html
//返り値・・・Ajaxからの通信であればtrue、そうでなければfalse
function is_ajax()
{
    return isset($_SERVER['HTTP_X_REQUESTED_WITH'])&&strtolower($_SERVER['HTTP_X_REQUESTED_WITH'])=='xmlhttprequest';
}

という感じになる。

では、jQueryなどのライブラリを使わずに自力でXMLHttpRequest(IEであればActiveXObject」)を使ってAjaxを行なっている場合はどうすればいいか?
その時は自分でヘッダに「X-Requested-With:XMLHttpRequest」をつけてしまえばいい。

//link・・・http://mio-koduki.blogspot.com/2012/03/phpjavascript-ajax.html

//Ajax通信用のオブジェクト作成関数
function create_ajax_object()
{
    //IE以外
    if(window.XMLHttpRequest)
    {
        try
        {
            return new XMLHttpRequest();
        }
        catch(e)
        {
            return null;
        }
    }
    //IE
    else if(window.ActiveXObject)
    {
        try
        {
            //IE6以降
            return new ActiveXObject('Msxml2.XMLHTTP');
        }
        catch(e)
        {
            try
            {
                //IE5
                return new ActiveXObject('Microsoft.XMLHTTP');
            }
            catch(e)
            {
                return null;
            }
        }
    }
    else
    {
        return null;
    }
}

//Ajax通信関数
function ajax_access(method,path,data,callback,async_flag)
{
    //パスが設定されていなければnullを返す
    if(path==null)
    {
        return null;
    }

    //Ajax通信用のオブジェクト作成
    var ajax_object=create_ajax_object();

    //Ajax通信用のオブジェクト作成に失敗したらnullを返す
    if(ajax_object==null)
    {
        return null;
    }

    //メソッドに何も設定されていなければGETを設定する
    method=method||'GET';
    //同期フラグに何も設定されていなければtrueを設定する
    async_flag=async_flag||true;

    //通信回線を開く
    ajax_object.open(method,path,async_flag);

    //content-typeを指定する
    //これがないとPOSTのデータを飛ばすときなどに失敗することがある
    ajax_object.setRequestHeader('Content-Type','application/x-www-form-urlencoded; charset=UTF-8');

    //「X-Requested-With:XMLHttpRequest」をヘッダにつける
    ajax_object.setRequestHeader('X-Requested-With','XMLHttpRequest');

    //通信時のイベント
    ajax_object.onreadystatechange=function()
    {
        //readyStateが4の時通信終了
        //statusが200の時通信成功
        if(ajax_object.readyState==4&&ajax_object.status==200)
        {
            //コールバック関数が設定されていれば呼び出す
            if(callback!=null)
            {
                //コールバック関数に結果を返す
                callback(ajax_object.responseText);
            }
        }
    }

    //データ送信
    ajax_object.send(data);
}

//Ajax通信を行う
ajax_access('post','test.php',encodeURIComponent('test')+'='+encodeURIComponent('テスト'),function(data){alert(data);},true);

とこんな感じになる。
久しぶりにJavaScriptでAjax書いた気がする。
ポイントはopenメソッドを呼び出した後に
setRequestHeaderを呼び出して「X-Requested-With:XMLHttpRequest」をヘッダにつけるところ

2012年3月12日月曜日

[JavaScript][PHP] オブジェクトからクエリストリングを作る

以前クエリストリング(URLパラメータ、GETパラメータって呼ばれたりもする)から
オブジェクトを作るということをやったので今度はその逆で
オブジェクトからクエリストリングを作るのをやってみたいと思う。
ちなみに、クエリストリングのキーの部分が数字のみになる場合のプレフィックスはつけてない。

//link・・・http://mio-koduki.blogspot.com/2012/03/javascriptphp_12.html
//第一引数・・・クエリストリングにしたいオブジェクト
//返り値・・・オブジェクトからビルドされたクエリストリング
function build_query(data)
{
    if(is_array(data)||is_object(data))
    {
        var params=[];
        for(var i in data)
        {
            if(is_array(data[i])||is_object(data[i]))
            {
                params.push(build(i,data[i]));
            }
            else if(is_can_format_value(data[i]))
            {
                params.push(url_encode(i)+"="+url_encode(format_value(data[i])));
            }
        }
        return params.join('&');
    }
    else
    {
        throw new Error('Parameter 1 expected to be Array or Object.');
    }

    function build(key,data)
    {
        var params=[];
        for(var i in data)
        {
            if(is_array(data[i])||is_object(data[i]))
            {
                params.push(build(key+'['+i+']',data[i]));
            }
            else if(is_can_format_value(data[i]))
            {
                params.push(url_encode(key+'['+i+']')+'='+url_encode(format_value(data[i])));
            }
        }
        return params.join('&');
    }

    function is_can_format_value(value)
    {
        return typeof value=='boolean'||typeof value=='string'||typeof value=='number';
    }

    function format_value(value)
    {
        if(value===true)
        {
            return 1;
        }
        else if(value===false)
        {
            return 0;
        }
        else
        {
            return value;
        }
    }

    function url_encode(string)
    {
        return encodeURIComponent((string+'')).replace(/!/g,'%21').replace(/'/g,'%27').replace(/\(/g,'%28').replace(/\)/g,'%29').replace(/\*/g,'%2A').replace(/%20/g,'+');
    }

    function is_array(data)
    {
        return data instanceof Array;
    }

    function is_object(data)
    {
        return data!=null&&typeof data=='object'&&Object.prototype.toString.call(data)!='[object Array]';
    }
}

と、こんな感じになると思う。
ちなみにPHPではhttp_build_queryが同じような挙動をする関数。

[PHP] 配列からCSVを作成しダウンロード

DBなどからデータを引き出し、CSV形式でダウンロードさせることは往々にしてある。
ロジック自体はそこまで複雑じゃないので毎回書いてもいいが、
数が多いと面倒なので関数にしてしまう。

//link・・・http://mio-koduki.blogspot.com/2012/03/php-csv.html
//第一引数・・・ダウンロードするときのファイル名
//第二引数・・・CSVにする配列(array(array(data1,data2),array(data1,data2))の2次元配列形式)
//第三引数・・・ヘッダを付ける場合は配列を渡す(array(header1,header2)の配列形式)
//返り値・・・なし
function csv_download($file_name,$data,$header=null)
{
    //ヘッダを付ける場合はarray(header1,header2)の形式で渡す
    if($header!=null)
    {
        array_unshift($data,$header);
    }

    //PHPのテンポラリに読み書きの準備をする
    $fp=fopen('php://temp','r+');
    foreach($data as $v)
    {
        //テンポラリにCSV形式で書き込む
        fputcsv($fp,$v,',','"');
    }
    //ファイルポインタを一番先頭に戻す
    rewind($fp);
    //ファイルポインタの今の位置から全てを読み込み文字列へ代入
    $csv=stream_get_contents($fp);
    //SJISに変える
    $csv=mb_convert_encoding($csv,'SJIS',mb_internal_encoding());
    //ファイルポインタを閉じる
    fclose($fp);
    //渡されたファイル名の拡張子やパスを切り落とす
    $file_name=basename($file_name);

    //ダウンロードヘッダ定義
    header('Content-Disposition:attachment; filename="'.$file_name.'.csv"');
    header('Content-Type:application/octet-stream');
    header('Content-Length:'.strlen($csv));
    echo $csv;
    exit;
}

という風に実装してみた。
tempの代わりにoutputストリームに書き込む方法もあるが、
こちらは出力バッファ系もごちゃごちゃいじる必要があるので、
一手間増えるためあまり好きじゃない。

ポイントとなる関数
  • fputcsv ・・・ 行を CSV 形式にフォーマットし、ファイルポインタに書き込む
  • stream_get_contents ・・・ 残りのストリームを文字列に読み込む

2012年3月9日金曜日

[JavaScript][PHP] クエリストリングからオブジェクトを作る

クエリストリングやURLパラメータ、GETパラメータと様々な呼び方があるが、
要するにURLの?以降にあるtest_key=test_valueの形式の文字列のことである。

PHPはわざわざ自前でパースしなくても、$_GETの中に配列として展開された形式で入ってるし、
parse_str関数を使えば好きなタイミングでパースすることが出るが、
JavaScriptにはそのような仕組みはなく、自力でパースしないといけない。

クエリストリングには配列として変換する仕組みがある。
test_key[]=test_value1&test_key[]=test_value2&test_key[test]=test_value3
というクエリストリングだと

array(1) {
  ["test_key"]=>
  array(3) {
    [0]=>
    string(11) "test_value1"
    [1]=>
    string(11) "test_value2"
    ["test"]=>
    string(11) "test_value3"
  }
}

という配列が生成される。

何サイトかJavaScriptでクエリストリングをオブジェクトに変える処理を見てみたが、
ほとんどのサイトでこの配列形式をサポートしてなかった。

なのでちょっと作ってみた。

//link・・・http://mio-koduki.blogspot.com/2012/03/javascriptphp.html
//第一引数・・・パースしたいクエリストリング。省略した場合は現在のURLのクエリストリング
//返り値・・・クエリストリングからパースされたオブジェクト
function parse_query(query)
{
    if(query==null)
    {
        query=(location.search+'').replace(/^[?&]/,'');
    }
    var separator1='=';
    var separator2='&';
    var data={};
    query=(query+'').replace(/^( |%20)*/g,'').split(separator2);
    for(var i=0;i<query.length;i++)
    {
        var tmp=query[i].split(separator1);
        if(tmp.length<2)
        {
            tmp=[tmp,''];
        }
        var key=url_decode(tmp.shift());
        if(key.length<=0)
        {
            continue;
        }
        data=merge(data,make_array(key,url_decode(tmp.join(separator1))));
    }
    return data;

    function url_decode(string)
    {
        return decodeURIComponent((string+'').replace(/\+/g,'%20'));
    }

    function make_array(key,value)
    {
        var tmp_key=[];
        var bracket_key='';
        var bracket=false;
        key=key.replace(/^ */g,'');
        if(key.length>0&&key.charAt(0)=='[')
        {
            key='';
        }
        for(var i=0;i<key.length;i++)
        {
            var char=key.charAt(i);
            if(!bracket&&char=='[')
            {
                if(bracket_key!='')
                {
                    tmp_key.push(bracket_key);
                    bracket_key='';
                }
                bracket=true;
                bracket_key+=char;
            }
            else if(char==']')
            {
                if(bracket)
                {
                    if(tmp_key==1)
                    {
                        tmp_key.push(bracket_key);
                    }
                    else
                    {
                        tmp_key.push(bracket_key.substr(1));
                    }
                    bracket_key='';
                    bracket=false;
                    if(key.charAt(i+1)!='[')
                    {
                        break;
                    }
                }
                else
                {
                    bracket_key+=char;
                }
            }
            else if(char=='\0')
            {
                break;
            }
            else
            {
                bracket_key+=char;
            }
        }

        if(bracket)
        {
            if(tmp_key.length==1)
            {
                tmp_key[tmp_key.length-1]+=bracket_key;
            }
        }
        else if(bracket_key!='')
        {
            tmp_key.push(bracket_key);
        }
        var count=0;
        if(tmp_key.length>0)
        {
            for(var i=0;i<tmp_key[0].length;i++)
            {
                var char=tmp_key[0].charAt(i);
                if(char==' '||char=='.'||char=='[')
                {
                    tmp_key[0]=tmp_key[0].substr(0,i)+'_'+tmp_key[0].substr(i+1);
                }
                //下の1文を変えれば$_GET準拠、現在はparse_str準拠
                //if(char=='['&&++count>0)
                if(char=='['&&count++>0)
                {
                    break;
                }
            }
        }
        for(var i=tmp_key.length-1;i>=0;i--)
        {
            var tmp_data={};
            tmp_data[tmp_key[i]]=value;
            value=tmp_data;
        }
        return value;
    }

    function merge(base,extend)
    {
        if(!is_object(base)||!is_object(extend))
        {
            return {};
        }
        extend=numbering(base,extend);
        for(var i in extend)
        {
            if(is_object(base[i])&&is_object(extend[i]))
            {
                base[i]=merge(base[i],extend[i]);
            }
            else
            {
                base[i]=extend[i];
            }
        }
        return base;
    }

    function numbering(base,extend)
    {
        var tmp={};
        for(var i in extend)
        {
            var index=(i.match(/^\s?$/)?max_index(base):i);
            if(is_object(extend[i]))
            {
                tmp[index]=numbering(is_object(base)?base[i]:null,extend[i]);
            }
            else
            {
                tmp[index]=extend[i];
            }
        }
        return tmp;
    }

    function max_index(data)
    {
        var max=0;
        if(!is_object(data))
        {
            return max;
        }
        for(var i in data)
        {
            if((i+'').match(/^(0|[1-9][0-9]*)$/)&&i>=max)
            {
                max=i-0+1;
            }
        }
        return max;
    }

    function is_object(data)
    {
        return data!=null&&typeof data=='object'&&Object.prototype.toString.call(data)!='[object Array]';
    }
}

とこんな感じ。
配列形式をサポートすると非常に面倒くさくなってくる。
第一引数にクエリストリングを渡す形式だが、省略すると現在のURLのクエリストリングをパースする。

余談だが、PHPのマニュアルには$_GETや$_POSTと同じ仕組でparse_strを動かしてあると書いているが、「a[b[c=d」というクエリの時に結果が違ってる。
地味に気になるw
この関数もどっちに準拠すべきか迷ったので、一応どっちにも切り替えられるようにしている。

この関数の逆の挙動をする関数も作ってみた。

[PHP][PDO][SQL] IN句のbindを簡単に作成する。

PHPからPDOでIN句を含むSQLを発行するとき
IN(?,?,?,?)やIN(:in_1,:in_2,:in_3)
と検索する個数分「?」なり「:in_1」なりを書かないといけない。
個数が固定であれば直書きでもいいが、checkboxから飛んできたデータなど、
不定であることが多い。
もちろんforやforeachなどループで回せば書くことはできるが少々面倒臭い。
そういう時は

//idに対してこういうデータで検索条件の値が飛んでくるとする
$data=array(2,7,14,23);

$sql='';
$where=array();

$sql.="SELECT * FROM table WHERE flag=?";
$where[]=$flag;
$sql.=" AND id IN(".substr(str_repeat(',?',count($data)),1).")";
$where=array_merge($where,$data);

$st=$pdo->prepare($sql);
$st->execute($where);

とするとループを書くことなくスマート?に記述することができる。
ざっくり解説すると
SQLを組み立てるところは、まず、データの個数をcount関数で取得し、
str_repeat関数で「,?」をデータの個数分作成する。
その状態だと「,?,?,?,?」と先頭に1個「,」が多い状態なので
それをsubstr関数で先頭1文字を切り出し、「?,?,?,?」の状態にする。
実データの方はarray_mergeを使うことでflagの検索条件の値を保持したまま
1つの配列にまとめている。
なお、$dataに何も入ってないとSQLエラーになる(IN()というふうに空のIN句になってしまうので)。
そのため、事前に$dataが空じゃないかのチェックをするなりしておいたほうがいいだろう。

ポイントとなる関数
  • str_repeat ・・・ 文字列を反復する
  • substr ・・・ 文字列の一部分を返す
  • array_merge ・・・ ひとつまたは複数の配列をマージする

2012年3月7日水曜日

[PHP][Excel][PHPExcel] セルに0埋め(0詰め)した値をセットする

PHPExcelにあるメソッドのsetCellValueやsetCellValueByColumnAndRowで0埋め(0詰め)で検索してきていた人がいたのでせっかくなのでコードを紹介してみる。

ちなみに普通にアプリケーションのExcelでもセルの書式設定から表示形式を文字列にしないと、「01」などと入力しても「1」に勝手に書き換えられてしまう。

PHPExcelも考え方は同じでセルの表示形式を文字列にしつつ値を入れるという方法になる。

//ClassesをおいたパスからPHPExcel.phpをインクルードする
require_once('Classes/PHPExcel.php');
//書き込むExcelのパス
define('EXCEL','data/test.xls');

//Excelのクラスを作る
$excel=new PHPExcel();
//アクティブなシートを取得する
$sheet=$excel->getActiveSheet();
//シートに名前をつける
$sheet->setTitle('sheet title');
//R1C1参照で値を入れる
$sheet->setCellValueExplicitByColumnAndRow(0,1,'01',PHPExcel_Cell_DataType::TYPE_STRING);
//A1参照で値を入れる
$sheet->setCellValueExplicit('A2','01',PHPExcel_Cell_DataType::TYPE_STRING);
//.xls形式で書き込むためのクラスを作る
//.xlsx形式を書き込む場合は'Excel5'の部分を'Excel2007'に変える →$writer=PHPExcel_IOFactory::createWriter($excel,'Excel2007');
$writer=PHPExcel_IOFactory::createWriter($excel,'Excel5');
//ファイルに書き込む
$writer->save(EXCEL);

とすることによって

ABC
101
2002
3

のようなデータを書き込むことが可能になる。

2012年3月6日火曜日

[PHP][Excel][PHPExcel] PHPExcelのgetNestingLevelエラー問題

PHPExcelを使ってExcelを読み込みたい時がある。
しかし、一定の条件によって
Fatal error: Call to a member function getNestingLevel() on a non-object in ~~~パス/Classes/PHPExcel/Reader/Excel5.php on line 842 
というエラーが表示され、先に進まないことがある。

どうも、これ自体はPHPExcelのバグのようでまだ修正されてないようだ。
ただ、パッチを作ってくれている人がいるようで、そのパッチを当てれば動くようになる。

PHPExcel - Patches

上記サイトのID:9136のところにそのパッチのダウンロードリンクがある。
そのパッチをExcel5.phpに当ててやれば、getNestingLevel問題は解決する。

[PHP][Excel][PHPExcel] PHPExcelの導入2

前回はPHPExcelからデータを読み込む方法をやったので、
今回はExcelにデータを書き込む方法をやってみる。
まずは単純なデータの書き込み。

//ClassesをおいたパスからPHPExcel.phpをインクルードする
require_once('Classes/PHPExcel.php');
//書き込むExcelのパス
define('EXCEL','data/test.xls');

//Excelのクラスを作る
$excel=new PHPExcel();
//アクティブなシートを取得する
$sheet=$excel->getActiveSheet();
//シートに名前をつける
$sheet->setTitle('sheet title');
//R1C1参照で値を入れる
$sheet->setCellValueByColumnAndRow(0,1,'test');
//A1参照で値を入れる
$sheet->setCellValue('B1','test2');
//数字を入れる
$sheet->setCellValue('A2','3');
//計算式を入れる
$sheet->setCellValue('B2','=A2*3');
//.xls形式で書き込むためのクラスを作る
//.xlsx形式を書き込む場合は'Excel5'の部分を'Excel2007'に変える →$writer=PHPExcel_IOFactory::createWriter($excel,'Excel2007');
$writer=PHPExcel_IOFactory::createWriter($excel,'Excel5');
//ファイルに書き込む
$writer->save(EXCEL);

とすると

ABC
1testtest2
239
3

というExcelファイルが作られる。

またスタイルの設定なども変えることが可能。

//ClassesをおいたパスからPHPExcel.phpをインクルードする
require_once('Classes/PHPExcel.php');
//書き込むExcelのパス
define('EXCEL','data/test.xls');

//Excelのクラスを作る
$excel=new PHPExcel();
//アクティブなシートを取得する
$sheet=$excel->getActiveSheet();
//シートに名前をつける
$sheet->setTitle('sheet title');
//A1参照で値を入れる
$sheet->setCellValue('A1','test1');
//フォントサイズ
$sheet->getDefaultStyle()->getFont()->setSize(10);
//フォント
$sheet->getDefaultStyle()->getFont()->setName('メイリオ');
//セルの下に罫線を引く
$sheet->getStyle('A1')->getBorders()->getBottom()->setBorderStyle(PHPExcel_Style_Border::BORDER_THIN);
//センター寄せ
$sheet->getStyle('A1')->getAlignment()->setHorizontal(PHPExcel_Style_Alignment::HORIZONTAL_CENTER);
//文字色
$sheet->getStyle('A1')->getFont()->getColor()->setARGB('FFFF3333');
//背景色
$sheet->getStyleByColumnAndRow(0,1)->getFill()->setFillType(PHPExcel_Style_Fill::FILL_SOLID);
$sheet->getStyleByColumnAndRow(0,1)->getFill()->getStartColor()->setARGB('FFCCCCFF');

//.xls形式で書き込むためのクラスを作る
//.xlsx形式を書き込む場合は'Excel5'の部分を'Excel2007'に変える →$writer=PHPExcel_IOFactory::createWriter($excel,'Excel2007');
$writer=PHPExcel_IOFactory::createWriter($excel,'Excel5');
//ファイルに書き込む
$writer->save(EXCEL);

ABC
1test
2
3

という結果になる。

2012年3月2日金曜日

[PHP][Excel][PHPExcel] PHPExcelの導入

PHPからExcelを扱うというのは、意外と顧客からの要望に上がることが多い。

また、仕様書やデータ定義、DB定義などもExcelで書かれていると、
簡単にコピペでプログラムに持ってくることはできない。
だからといっていちいち手作業でDB定義書からCREATE文を作るなどというのも結構手間だ。
そういう時にPHPからExcelを読み取って、自動的にCREATE文を作るなりすると効率がいい。

まずは、PHPExcelの本家サイトからライブラリを落としてこよう。
落としてきたZIPを解凍すると、中にClassesフォルダがある。
この中に必要なクラスが定義されているファイルが入っているので、
これを必要な場所にフォルダごとおいておこう。

後はコードを書くだけ。
試しにExcel内の最初のシートの中身を配列に代入するプログラムを書いてみる。

ABC
1123
2456
3789

上記のような構造のExcelファイルを読み込むとする。

//ClassesをおいたパスからPHPExcel.phpをインクルードする
require_once('Classes/PHPExcel.php');
//読み込むExcelのパス
define('EXCEL','data/test.xls');

//.xls形式を読むという設定でクラスと作る
//.xlsx形式を読み込む場合は'Excel5'の部分を'Excel2007'に変える →$reader=PHPExcel_IOFactory::createReader('Excel2007');
$reader=PHPExcel_IOFactory::createReader('Excel5');
//読み込むExcelのパスを指定する
$excel=$reader->load(EXCEL);
//最初のシートを選ぶ
$sheet=$excel->setActiveSheetIndex(0);

$data=array();
//行イテレータ取得
foreach($sheet->getRowIterator() as $row)
{
    $tmp=array();
    //列イテレータ取得
    foreach($row->getCellIterator() as $cell)
    {
        //セルの値取得
        $tmp[]=$cell->getValue();
    }
    $data[]=$tmp;
}

//整形して出力
echo '<pre><code>';
var_dump($data);
echo '</code></pre>';

出力結果はこんな感じになる。
array(3) {
  [0]=>
  array(3) {
    [0]=>
    float(1)
    [1]=>
    float(2)
    [2]=>
    float(3)
  }
  [1]=>
  array(3) {
    [0]=>
    float(4)
    [1]=>
    float(5)
    [2]=>
    float(6)
  }
  [2]=>
  array(3) {
    [0]=>
    float(7)
    [1]=>
    float(8)
    [2]=>
    float(9)
  }
}

しかし、このままのソースだと例えば

ABC
113
25
378

のような虫食い状態のデータの時に下記のような結果になる。

array(3) {
  [0]=>
  array(2) {
    [0]=>
    float(1)
    [1]=>
    float(3)
  }
  [1]=>
  array(1) {
    [0]=>
    float(5)
  }
  [2]=>
  array(2) {
    [0]=>
    float(7)
    [1]=>
    float(8)
  }
}

上記プログラムだと空の場合は読み飛ばしてしまうため、
配列の添字が詰められてしまう。
データが開いてる時は配列も開けておきたい場合は

//ClassesをおいたパスからPHPExcel.phpをインクルードする
require_once('Classes/PHPExcel.php');
//読み込むExcelのパス
define('EXCEL','data/test.xls');

//.xls形式を読むという設定でクラスと作る
//.xlsx形式を読み込む場合は'Excel5'の部分を'Excel2007'に変える →$reader=PHPExcel_IOFactory::createReader('Excel2007');
$reader=PHPExcel_IOFactory::createReader('Excel5');
//読み込むExcelのパスを指定する
$excel=$reader->load(EXCEL);
//最初のシートを選ぶ
$sheet=$excel->setActiveSheetIndex(0);

$data=array();
//列の最大値を取得
for($i=1;$i<=$sheet->getHighestRow();$i++)
{
    $tmp=array();
    //行の最大値を取得
    //A、B、Cの文字列形式で帰ってくるので数字に変換する
    $max_column=PHPExcel_Cell::columnIndexFromString($sheet->getHighestColumn());
    for($j=0;$j<$max_column;$j++)
    {
        //列のインデックスと、行のインデックスからセルの値を取得
        $tmp[]=$sheet->getCellByColumnAndRow($j,$i)->getValue();
    }
    $data[]=$tmp;
}

//整形して出力
echo '<pre><code>';
var_dump($data);
echo '</code></pre>';

とすると出力結果は

array(3) {
  [0]=>
  array(3) {
    [0]=>
    float(1)
    [1]=>
    NULL
    [2]=>
    float(3)
  }
  [1]=>
  array(3) {
    [0]=>
    NULL
    [1]=>
    float(5)
    [2]=>
    NULL
  }
  [2]=>
  array(3) {
    [0]=>
    float(7)
    [1]=>
    float(8)
    [2]=>
    NULL
  }
}

となる。

次へ

2012年2月29日水曜日

[PHP] ディレクトリ内の全ファイルを消す

特定フォルダ内にある全ファイルを消したい時

//$pathに特定のフォルダのパスを代入しておく。末尾に/が必要。
$files=glob($path.'*',GLOB_MARK);
foreach($files as $v)
{
     if(is_file($v))
     {
          @unlink($v);
     }
}

とglob関数を使うと便利。

特定フォルダ内にある特定の拡張子のファイルを消したい場合は

//$pathに特定のフォルダのパスを代入しておく。末尾に/が必要。
$files=glob($path.'*.{jpg,jpeg,png,gif,bmp}',GLOB_MARK|GLOB_BRACE);
foreach($files as $v)
{
     if(is_file($v))
     {
          @unlink($v);
     }
}

とする。

ポイントとなる関数

  • glob ・・・ パターンにマッチするパス名を探す

2012年2月24日金曜日

[JavaScript][jQuery] inputのtype="file"であるときにvalueの値を空にする

jQueryで$(':file').val('')などをしても、実はtype="file"の時は消すことができない。

type="file"のvalueを操作させない。値が入力されても無視するという挙動は
HTMLの仕様ではなくて厳密にはブラウザのセキュリティポリシーに基づくものらしいが、
値をどうしても空にしたい時には困った挙動である。

inputのtype="reset"だとtype="file"の値も消すことはできるが、
フォームにあるinputやselect、textareaなどの値まで消してしまうのであまり使えない。

そういう時にはjQueryで

$(':file').replaceWith($(':file').clone())

と書くとうまいこと消すことができる。
:fileの部分はもちろん好きなセレクタで構わない。

やってることは単純で、type="file"のコピーを作り出し、
元のものと置き換えているだけである。

これは上記に述べた値を無視するという挙動を利用して、
新規に作成されたら(コピーされたら)値が消されるように仕向けている。

なお、イベントなどもコピーに引き継ぎたい場合は、
clone(true)と引数にtrueを入れるとイベントもコピーされる。

2012年2月21日火曜日

[PHP] 相対URLから絶対URLを作る

たまにURLの相対パスから絶対パスを取りたい時がある。
PHPのデフォルトの関数にありそうだけど実はなかったりする。
せっかくなので自作してみる。

ちなみにis_absolute関数は絶対パスかどうかを調べる関数で、
以前作成したものを使用。

対象はPHP5.2以上

//link・・・http://mio-koduki.blogspot.com/2012/02/php-urlurl.html
//第一引数・・・相対パス
//第二引数・・・基準のパス
//返り値・・・成功したら絶対パスの文字列を、失敗したらfalseを返す
function get_absolute_url($path,$base)
{
    //パスがURLの形式でない場合はfalseを返す
    if(($ppath=parse_url($path))===false)
    {
        return false;
    }

    //基準のパスがURLの形式でない、または絶対パスでない場合はfalseを返す
    if(($pbase=parse_url($base))===false||!is_absolute($base))
    {
        return false;
    }

    //パスが初めから絶対パスの場合は自身を返す
    if(is_absolute($path))
    {
        return $path;
    }
    //絶対パスの形式でないのにスキームがある場合はfalseを返す
    elseif(isset($ppath['scheme']))
    {
        return false;
    }
    //パスが空文字だった場合は現在のパスを返す
    elseif($path=='')
    {
        return is_absolute($base)?$base:false;
    }

 //パスがパラメータ、フラグメントのみの場合は基準のパスのパラメータを置き換えて返す
 if(strpos($path,'?')===0)
 {
  $absolute_path=preg_replace('#\?'.(isset($pbase['query'])?preg_quote($pbase['query'],'#'):'').'(\#'.(isset($pbase['fragment'])?preg_quote($pbase['fragment'],'#'):'').')?$#','',$base).$path;
  return is_absolute($absolute_path)?$absolute_path:false;
 }
    //パスがフラグメントのみの場合は基準のパスのフラグメントを置き換えて返す
    if(strpos($path,'#')===0)
    {
        $absolute_path=preg_replace('#\#'.(isset($pbase['fragment'])?preg_quote($pbase['fragment'],'#'):'').'$#','',$base).$path;
        return is_absolute($absolute_path)?$absolute_path:false;
    }

    $absolute_path=$pbase['scheme'].'://'.$pbase['host'];
    $tmp_path=array();

    //パスがルートパスの場合はスキームとホストをつけて返す
    if(strpos($path,'/')===0)
    {
        $absolute_path.=$path;
        return is_absolute($absolute_path)?$absolute_path:false;
    }

    //基準のパスのパス部分を取得
    $base_path=isset($pbase['path'])?preg_replace('#^/#','',$pbase['path']):'';
    if($base_path!=''&&strpos($base_path,'/')!==false)
    {
        $tmp_path=explode('/',substr($base_path,0,substr($base_path,-1,1)=='/'?-1:strrpos($base_path,'/')));
    }

    //基準のパスのパス部分の階層をたどっていく
    $exploded_path=explode('/',$path);
    foreach($exploded_path as $v)
    {
        if(preg_match('#^\.+$#',$v))
        {
            if($v!='.')
            {
                $count=strlen($v)-1;
                array_splice($tmp_path,count($tmp_path)-$count,$count);
            }
            continue;
        }
        $tmp_path[]=$v;
    }

    //パスなどをつなげて絶対パスを返す
    $absolute_path.='/'.implode('/',$tmp_path);
    return is_absolute($absolute_path)?$absolute_path:false;
}

//link・・・http://mio-koduki.blogspot.com/2012/02/php.html
//第一引数・・・絶対パスかどうか調べたいパス
//返り値・・・絶対パスであればtrue、そうでなければfalse
function is_absolute($url)
{
    return
    ($purl=parse_url($url))!==false
    &&isset($purl['scheme'])
    &&isset($purl['host'])
    &&filter_var
    (
        $url,
        FILTER_VALIDATE_URL,
        FILTER_FLAG_PATH_REQUIRED,
    );
}

第一引数に調べたい相対パスと第二引数に基準となるパス(基本現在のパス)を渡せば
成功した際に絶対パスの文字列を、失敗した際にfalseを返す仕様になっている。

ポイントとなる関数
  • parse_url ・・・ URLを解釈し、その構成要素を返す

2012年2月20日月曜日

[PHP] 絶対パスかどうか調べる

絶対パスかどうか調べる必要があったのでなんとなく。
正規表現で済ませてもいいのだけれども、
http以外にもshttpやhttps、ftpなどその他派生まで考慮してるとぐだぐだなるため、
PHPのデフォルトに任せることにしたのです。

対象はPHP5.2以上

//link・・・http://mio-koduki.blogspot.com/2012/02/php.html
//第一引数・・・絶対パスかどうか調べたいパス
//返り値・・・絶対パスであればtrue、そうでなければfalse
function is_absolute($url)
{
    return
    ($purl=parse_url($url))!==false
    &&isset($purl['scheme'])
    &&isset($purl['host'])
    &&filter_var
    (
        $url,
        FILTER_VALIDATE_URL,
        FILTER_FLAG_PATH_REQUIRED,
    );
}

ちなみにhttpだけなどの条件をつけるなら

//link・・・http://mio-koduki.blogspot.com/2012/02/php.html
//第一引数・・・絶対パスかどうか調べたいパス
//返り値・・・絶対パスであればtrue、そうでなければfalse
function is_absolute_only_http($url)
{
    return
    ($purl=parse_url($url))!==false
    &&isset($purl['scheme'])
    &&$purl['scheme']=='http'
    &&isset($purl['host'])
    &&filter_var
    (
        $url,
        FILTER_VALIDATE_URL,
        FILTER_FLAG_PATH_REQUIRED,
    );
}

こんな感じ。

ポイントとなる関数
  • parse_url ・・・ URL を解釈し、その構成要素を返す
  • filter_var ・・・ 指定したフィルタでデータをフィルタリングする

Hello World

メモついでに色々と気づいたことを公開していこうかなと