@blog.justoneplanet.info

日々勉強

PHP Perl-compatible Regular Expressions(Perl互換正規表現)

正規表現を使うと非常に複雑な文字列の抽出、検索や置換、分割が行える。正規表現とは検索パターンを表す文字列である。PHPではPOSIXの正規表現とPerlの正規表現をサポートしている。Perl-compatible Regular Expressionsは略してPCREと呼ばれる。

Perl互換正規表現 POSIXの正規表現
速度
機能
読みやすさ

最後の読みやすさについて、個人的にはPerl互換正規表現の方が読みやすいとは思う。

■PCRE正規表現の区切り文字

慣例ではスラッシュ「/」が使用されるが、urlなどを正規表現で表す際に、エスケープするのが面倒になるので#が使われることもある。通常では使用されないが、英数字とバックスラッシュ以外の文字ならば区切り文字として使用が可能である。

<?php
if(preg_match('/[a-z]/', 'string')){
    //code
}
if(preg_match('#(http://|https://)[a-zA-Z0-9./_-]+#', 'http://justoneplanet.info/')){
    //code
}
?>

■文字クラス

. 任意の一文字にマッチする
^ 文字列の最初にマッチする、もしくは行の先頭にマッチする(mフラグが有効な場合はnの次の文字)
$ 文字列の最後にマッチする、もしくは行の末尾にマッチする(mフラグが有効な場合はnの前の文字)
s 改行コード、半角スペース(全角スペースは含まない)、タブにマッチする [rn t]
d 全ての数字にマッチする [0-9]
D 全ての数字以外にマッチする [^0-9]
w 単語に使用する文字(アルファベット等)にマッチする [0-9a-zA-Z_]
W 単語に使用する文字(アルファベット等)以外にマッチする [^0-9a-zA-Z_]

全角スペースの扱いには要注意だ。その辺りが初心者のときは全然わからなかった。。。

■量指定子

貪欲 貪欲でない
* 直前の文字の0個以上の繰り返し *?
+ 直前の文字の1個以上の繰り返し +?
? 直前の文字が0個もしくは1個だけ存在する(直前の文字が繰り返しではないことを意味する) ??
{n} 直前の文字がn個繰り返し {n}?
{n,} 直前の文字が少なくともn個繰り返し {n,}?
{n,m} 直前の文字が最低n個、m個以下の繰り返し {n,m}?

貪欲でない量指定子の使用法

以下のように通常「.*」を使用すると、一つ目のstrongタグの開始から二つ目のstrongタグの終了までマッチしてしまう。残りのパターンを満たす限りできるだけ短い範囲でマッチするようにするには貪欲でない量指定子を使用する。

<?php
preg_match('/(<.*>)/', '<strong>PHP</strong>のPerl互換正規表現', $match);
var_dump($match);
/*
array(2) {
  [0]=>
  string(20) "<strong>PHP</strong>"
  [1]=>
  string(20) "<strong>PHP</strong>"
}
*/
preg_match('/(<.*?>)/', '<strong>PHP</strong>のPerl互換正規表現', $match);
var_dump($match);
/*
array(2) {
  [0]=>
  string(8) "<strong>"
  [1]=>
  string(8) "<strong>"
}
*/
?>

但し、以下のコードの方が高速である。

<?php
preg_match('/(<[^>]*>)/', '<strong>PHP</strong>のPerl互換正規表現', $match);
var_dump($match);
/*
array(2) {
  [0]=>
  string(8) "<strong>"
  [1]=>
  string(8) "<strong>"
}
*/
?>

■選択肢

以下のように「|」の記号を使って正規表現内で選択肢を指定することができる。

<?php
preg_match_all('/dog|cat/i', 'Dogs and Cats is so cute!!', $matches);
var_dump($matches);
/*
array(1) {
  [0]=>
  array(2) {
    [0]=>
    string(3) "Dog"
    [1]=>
    string(3) "Cat"
  }
}
*/
?>

但し「dogもしくはcatで始まる文字列」とする場合、以下のようにグルーピングしなければならない。

<?php
preg_match_all('/^(cat|dog)/i', 'Dogs and Cats is so cute!!', $matches);
var_dump($matches);
/*
array(2) {
  [0]=>
  array(1) {
    [0]=>
    string(3) "Dog"
  }
  [1]=>
  array(1) {
    [0]=>
    string(3) "Dog"
  }
}
*/
?>

■サブパターン

パターンの一部を()で括ると後でキャプチャとして使用することができる。また、以下のようにグルーピングしてパターン化することができる。

<?php
preg_match('/(abc)+/', 'abcabcbbceed', $match);
var_dump($match);
/*
array(2) {
  [0]=>
  string(6) "abcabc"
  [1]=>
  string(3) "abc"
}
*/
?>

上述の例では、グルーピングしたabcの繰り返しにマッチするかどうかがチェックされる。また、$match[1]にはキャプチャした文字列が格納されている。ちなみにキャプチャする必要がない場合は以下のようにする。

<?php
preg_match('/(?:abc)+/', 'abcabcbbceed', $match);
var_dump($match);
/*
array(1) {
  [0]=>
  string(6) "abcabc"
}
*/
?>

■後置きオプション

i 大文字、小文字を区別しない
m ^を改行直後の文字、$を改行直前の文字にマッチさせる(マルチラインモード)
e 置換文字にPHPのコードを指定すると、evalした結果で文字列を置き換える(使い方によっては危険)

後置きオプションは以下のように使用する。

<?php
preg_match('/(?:abc)+/i', 'abcabcbbceed', $match);
var_dump($match);
/*
array(1) {
  [0]=>
  string(6) "abcabc"
}
*/
?>

■先読みと戻り読み

以下全てキャプチャは行わない。

(?=pattern) 肯定先読み
(?!pattern) 否定先読み
(?<=pattern) 肯定戻り読み
(?<!pattern) 否定戻り読み

先読み(次の文字が~だったら)と、戻り読み(前の文字が~だったら)は以下のように使用する。

<?php
preg_match('/(?<=<h4>).+(?=[0-9])/', '<h4>story1', $match);
var_dump($match);
/*
array(1) {
  [0]=>
  string(5) "story"
}
*/
?>

これは非常に強力である。ちなみに、JavaScriptには「戻り読み」が存在しない。

■Perl互換正規表現での文字列のマッチングと抽出

文字列のマッチングと抽出を行うには、以下のようにpreg_match関数を使用する。第一引数では検索パターンを指定し、第二引数では対象文字列を指定する。以降はオプションで、第三引数ではマッチした文字列が格納される変数を指定する。

<?php
$str = 'She sells seashells.';
if(preg_match('/she/im', $str, $matches)){
    var_dump($matches);
}
/*
array(1) {
  [0]=>
  string(3) "She"
}
*/
?>
<?php
$str = 'She sells seashells.';
if(preg_match('/(She)s(sell)s*s(seashell)s*/', $str, $matches)){
    var_dump($matches);
}
/*
array(4) {
  [0]=>
  string(19) "She sells seashells"
  [1]=>
  string(3) "She"
  [2]=>
  string(4) "sell"
  [3]=>
  string(8) "seashell"
}
*/
?>

preg_match関数は、1回マッチした時点で検索を終了するので、戻り値が1より大きくなることはない。従って、全て検索させたい場合には、以下のようにpreg_match_all関数を使用する。

<?php
$str = 'She sells seashells.';
if(preg_match_all('/she/i', $str, $matches)){
    var_dump($matches);
}
/*
array(1) {
  [0]=>
  array(2) {
    [0]=>
    string(3) "She"
    [1]=>
    string(3) "she"
  }
}
*/
?>

各関数について

int preg_match(string $pattern, string $str[, array $matches])
マッチした回数を返すが0か1となる。従って、全て調べるにはpreg_match_all関数を使用しなければならない。マッチした部分については$matchesに格納される。
int preg_match_all(string $pattern, string $str, array $matches)
パターンがマッチした総数を返す(0も含む)。但しエラーが発生した場合はfalseを返す。

■Perl互換正規表現での文字列の置換

以下のようにpreg_replace関数を使って、Perl互換正規表現での文字列置換を行う。この例では独自のタグをHTMLタグに変換する処理を行っている。いくつかのCMSにはこのようなタグが導入されている。これはユーザーのHTML入力を安全にする方法の一つである。

<?php
$str = '[b]Make Me Bold![/b]';
$str = preg_replace('#[b](.*?)[/b]#i', '<strong>$1</strong>', $str);
var_dump($str);
/*string(30) "<strong>Make Me Bold!</strong>"*/
?>

また、以下のように検索パターンと置換文字列に配列を指定することもできる。

<?php
$subjects['body'] = '[b]Make Me Bold![/b]';
$subjects['title'] = '[i]Make Me Italics![/i]';

$reg[] = '#[b](.*?)[/b]#i';
$reg[] = '#[i](.*?)[/i]#i';

$rep[] = '<strong>$1</strong>';
$rep[] = '<i>$1</i>';

$results = preg_replace($reg, $rep, $subjects);
var_dump($results);
/*
array(2) {
  ["body"]=>
  string(30) "<strong>Make Me Bold!</strong>"
  ["title"]=>
  string(23) "<i>Make Me Italics!</i>"
}
*/
?>

検索パターンの配列と置換文字列の配列の長さを比較し、検索パターンが多く置換文字列がない場合は空文字が置換文字列となる。また検索パターンのみ配列ということもあり得る。

各関数について

mixed preg_replace(mixed $pattern, mixed $replace,mixed $str)
正規表現$patternでマッチングを行い$replaceに置換する。

PHP Formatting Strings(文字列の整形)

■数字の整形

以下のように、PHPではnumber_format関数を使用して、数字を整形することができる。その時の引数の数として成立するパターンは以下の通りである。第二引数では小数以下の桁数を明示し、第三引数と第四引数で小数点の区切りと千単位の区切りを明示する。

<?php
$num = '100000000.12345';
print(number_format($num));//100,000,000
print(number_format($num, 3));//100,000,000.123
print(number_format($num, 3, '.', ','));//100,000,000.123
?>

各関数について

string number_format(float $num[, int $decimals[, string $dec_point, string $thousand_sep]])
1つ、2つ、4つの引数をとる。第一引数にはフォーマットする数字を指定し、第二引数では小数点以下の桁数を表示する。第三引数と第四引数は、それぞれ小数点の区切り文字と千ごとの区切り文字を指定する。

少し話が逸れてしまうが、フランスなどでは千単位の区切りにスペース(もしくはドット)を使用し、小数と整数の区切りにカンマを使うようだ。

■地域に依存した文字列の整形

ロケール情報のセット

以下のようにsetlocale関数を使用して、地域情報をセットする。第一引数ではカテゴリを指定し、第二引数ではロケールを文字列もしくは配列で指定する。

<?php
setlocale(LC_MONETARY, 'en_US');
?>

ロケールに基づいた文字列の整形

以上をふまえて、金額文字列に変換するには以下のようにmoney_format関数を使用する。但し、この関数はWindows環境(Windowsサーバー)では現在のところ使用できない。

<?php
$num = '100000000.98765';
setlocale(LC_MONETARY, 'en_US');
print(money_format('%.2n', $num));//$100,000,000.99

setlocale(LC_MONETARY, 'ja_JP.UTF-8');
print(money_format('%.2n', $num));//¥100,000,000.99
?>

小数が不要の場合は、以下のコードの後者のようにすれば良い。

<?php
$num = '100000000.796';
setlocale(LC_MONETARY, 'en_US');
print(money_format('%.2i', $num));//USD 100,000,000.80

setlocale(LC_MONETARY, 'ja_JP.UTF-8');
print(money_format('%i', $num));//JPY 100,000,001
?>

各関数について

string setlocale(int $category, string $locale)
第一引数のカテゴリにおいて、第二引数のロケールを設定する。
string money_format(string $format, float $num)
数値を金額文字列にフォーマットする。

フォーマット文字について

% 文字をそのまま返す。
. ピリオドに続く数字で小数の桁数を指定する。この桁数に出力が丸められる。
i ロケールの国際フォーマットでフォーマットする。
n ロケールの国内フォーマットでフォーマットする。

■書式文字列を指定した整形

以下のようにprintf関数は、書式文字列を指定された値で置き換えた文字列を出力する。但し、出力せずにデータとして扱いたい場合は、sprintf関数を使用する。また、%を出力したいときは%%と記述する。

<?php
$year = '2009';
$month = '4';
$day = '26';
printf('%04d/%02d/%02d', $year, $month, $day);//2009/04/26
//$date = sprintf('%04d/%02d/%02d', $year, $month, $day);
?>

どうもC言語の経験がないせいか、よく分からん・・・そんなときは書いて覚えるべし。

<?php
for($i = 0; $i < 101; $i = $i + 10){
    printf('%.2f%% 完了しました', $i);
}
/*
0.00% 完了しました
10.00% 完了しました
20.00% 完了しました
30.00% 完了しました
40.00% 完了しました
50.00% 完了しました
60.00% 完了しました
70.00% 完了しました
80.00% 完了しました
90.00% 完了しました
100.00% 完了しました
*/
?>

やはりコノ関数は日付の整形時にもっとも見かける。従って、もう一度。

<?php
$year = 2008;
$month = 6;
$day = '1';
printf('%04d/%02d/%02d', $year, $month, $day);
/*
2008/06/01
*/
?>

各関数について

int printf(string $format[, mixed $args[, mixed $args, …]])
文字列などをフォーマットして出力する。
string sprintf(string $format[, mixed $args[, mixed $args, …]])
文字列などをフォーマットして返す。
%の後に続く指定子
-もしくは+ 符号指定子。デフォルトでは整数が負の値のときだけ、-が付く。
パディング指定子 指定の桁数に足らなかったときに、詰める文字。スペースもしくは0のみ指定可能。
表示幅指定子 出力の桁数を指定する。
精度指定子 小数点の桁数を指定する。
型指定子
% 引数は不要
d 引数を10進数の整数として扱う
e 引数を科学記法として扱う

■フォーマットに基づいた文字列の分解

PHPでは以下のようにsscanf関数を使って、printf風のフォーマットに基づき文字列を分解できる。

<?php
$str = '123 456 789';
$format = '%d %d %d';
var_dump(sscanf($str, $format));
/*
array(3) {
  [0]=>
  int(123)
  [1]=>
  int(456)
  [2]=>
  int(789)
}
*/
$str = '123 aaa 789';
$format = '%d %d %d';
var_dump(sscanf($str, $format));
/*
array(3) {
  [0]=>
  int(123)
  [1]=>
  NULL
  [2]=>
  NULL
}
*/
?>

うーん、イマイチ有用性がわからん。。。

各関数について

mixed sscanf(string $str, string $format)
フォーマット文字列で文字列を処理する。

PHP Comparing, Searching and Replacing String(文字列の比較検索置換)

■文字列の比較

以下のようなコードを書いた場合、条件はtrueとなることに注意せねばならない。条件部分で$strがint型に自動で変換される為である。(808desu → 808)

<?php
$str = '808desu';
if($str == '808'){
    //code
}
?>

上述のような型の動的変換を予期した上でのコードは、自身の可読性を非常に下げる。従って、以下のようなコードを書くことをお勧めする。

<?php
$str = '808desu';
if($str === '808'){
    //code
}
?>

こういったコードにすれば、誰もが予期するように条件はfalseとなる。

文字列比較関数

以下のように、PHPには文字列を比較する関数も用意されている。

<?php
$str = '808desu';
if(strcmp($str, '808desu') === 0){
    //code
}
?>

strcmp関数は第一引数の文字列と第二引数の文字列を比較し、等しいときは0、第一引数が第二引数よりも大きいときは差分を正の値で返し、逆のときは差分を負の値で返す。また、以下のように大文字と小文字を区別しないstrcasecmp、最初のn文字までを比較するstrncasecmp関数もある。

<?php
$str = '808desu';
if(strcasecmp($str, '808Desu') === 0){
    //code
}
?>
<?php
$str = '808desu';
if(strncasecmp($str, '808Desukedo', 7) === 0){
    //code
}
?>
各関数について
int strcmp(string $str1, string $str2)
$str1を$str2と比べたときに、小さかったら負の数、大きかったら正の数、等しいかったら0を返す。
int strcasecmp(string $str1, string $str2)
大文字と小文字を区別しない。
int strncasecmp(strng $str1, string $str2, int $length)
はじめの$lengthだけ比較対象とする。但し、$lengthよりも一方が短い場合は、その文字数に合わせる。

正直なところ、暗記するコストの方が高くつくこの手の関数の有用性は理解に苦しむ。まぁそれがPHPの良いところでもあるのかもしれない。

■単純な文字列の検索

文字列の位置情報

PHPには以下のように、文字列の位置を返すstrpos関数がある。この関数は対象文字列に検索文字列が見つからなかったとき、falseを返す。従って条件文では、0が戻ることを考慮し以下のようにしなければならない。

<?php
$haystack = 'Mike's car';
$needle = 'Mike';
if(strpos($haystack, $needle) !== false){
    //code
}
?>

また以下のように、strpos関数は第三引数にオフセット値を指定できる。

<?php
$haystack = 'Mike's car. Mike's bike';
$needle = 'Mike';
print(strpos($haystack, $needle));//0
print(strpos($haystack, $needle, 1));//12
?>

strpos関数には大文字と小文字を区別しないstripos関数がある。

文字列の位置と部分文字列

文字列内から検索文字列以降を得たい場合は以下のように、strstr関数を使用する。但し、単純な文字列検索の場合は、より高速に動作するstrpos関数を使用するべきである。

<?php
$haystack = 'This is Mike's car';
$needle = 'Mike';
print(strstr($haystack, $needle));//Mike's car
?>

strstr関数には大文字と小文字を区別しないstristr関数がある。

関数によって「case」が付くものと「i」が付くものがあるが、これはPHPの統一性に欠ける面だと個人的には思う。

文字列のマスキング処理

以下のようにstrspn関数は、文字列の最初からマスキングし、一致文字列の長さを返す。ホワイトリスト方式である。また、ブラックリスト方式の関数としてstrcspn関数がある。

<?php
$str = '13927abccdestt';
$allowed_characters = '123456789';
print(strspn($str, $allowed_characters));//5
?>

以下のようにstrspn関数は、第三引数に対象文字列のオフセット値、第四引数に対象文字列の長さを指定できる。

<?php
echo strspn("foo", "o", 1, 2); // 2
?>

個人的には、この関数を使ったことはない。

各関数について
int strpos(string $str, string $needle)
文字列$strの中で$needleが最初に見つかる位置を返す。但し、見つからなかった場合は「false」を返す。
string strstr(string $str, string $needle)
文字列$strの中で$needleが最初に見つかる位置を含めた残りの文字列を返す。
int strspn(string $str, string $mask[, int $start[, int $length]])
文字列の先頭から何文字目までが$maskの条件を満たしているかを返す。第三引数にはオフセット値、第四引数には対象文字列の長さを指定できる。

■文字列の置換

文字列による検索と置換

以下のようにstr_replace関数を使って、検索文字列に一致したすべての文字列を置換する。

<?php
$search = 'John';
$replace = 'Nick';
$str = 'Hello, John!';
$str = str_replace($search, $replace, $str);
print($str);//Hello, Nick!
?>

また以下のように、置換対象の個数を知ることもできる。

<?php
$search = 'John';
$replace = 'Nick';
$str = 'Hello, John!';
$str = str_replace($search, $replace, $str, $counter);
print($counter);//1
?>

大文字と小文字を区別しないようにするには、str_ireplace関数を使用する。

部分指定による検索と置換

以下のようにsubstr_replace関数を使用する。第一引数は対象文字列、第二引数は置換文字列、第三引数はオフセット値、第四引数は置換される文字列の長さ。第四引数を指定しなかった場合は、オフセット値から最後までが置換対象となる。

<?php
$str = 'sample@test.com';
$replace = 'Nick';
$str = substr_replace($str, $replace, 0, 6);
print($str);//Nick@test.com
?>

以上を踏まえると以下のような使用例が考えられる。

<?php
$email = 'mike@sample.com';
$user = substr_replace($email, '', strpos($email, '@'));
print($user)//mike;
?>
各関数について
string str_replace(mixed $search, mixed $replace, mixed $subject[, int &$count])
対象文字列から検索文字列を全て置換文字列に変換します。
string str_ireplace(mixed $search, mixed $replace, mixed $subject[, int &$count])
大文字と小文字を区別しないstr_replace関数。
mixed substr_replace(mixed $str, string $replace, int $start[, int $length])
$start番目から$length分の文字列を$replaceに変換する。

■文字列の抽出

以下のようにsubstr関数を使用すると、文字列から一部分を抽出できる。

<?php
$str = 'She sells sea shells on the sea shore.';
print(substr($str, 0, 3));//She
print(substr($str, 4, 5));//sells
print(substr($str, 21));//on the sea shore.
?>

また以下のように第二引数には負の値を指定することができる。その場合、オフセット値は後ろから数え、そこから正の方向に第三引数分抽出することになる。

<?php
$str = 'She sells sea shells on the sea shore.';
print(substr($str, -1, 1));//.
print(substr($str, -6, 5));//shore
?>

各関数について

string substr(string $str, int $start[, int $length])
文字列を$startの位置から$lengthバイト切り出す。$lengthに負の値を与えると、最後から数えて$length分だけ結果から切り落とされる。

■文字列の分解と結合

以下のようにexplode関数を用いて、文字列を任意の文字で分解することができる。

<?php
$str = 'John,Jack,Nick';
$names = explode(',', $str);
var_dump($names);
/*
array(3) {
  [0]=>
  string(4) "John"
  [1]=>
  string(4) "Jack"
  [2]=>
  string(4) "Nick"
}
*/
?>

また、以下のようにimplode関数を用いて、配列を任意の文字列で結合することができる。

<?php
$names = array('Nick', 'Mike', 'Fred');
$str = implode(',', $names);
var_dump($str);
/*
string(14) "Nick,Mike,Fred"
*/
?>

各関数について

array explode(string $separator, string $str)
文字列を$separatorで分割し配列にする。
string implode(string $separator, string $str)
配列を$separatorで結合し文字列を返す。

PHP String Basics(文字列の基本)

■文字列データの使用

JavaScriptではシングルクォートもダブルクォートも同じ扱いであるが、PHPでは変数の展開において挙動が異なる。

シングルクォート

通常は以下のようにシングルクォートで括る。最もシンプルで一般的な方法だ。

<?php
$str = 'sample';
?>

シングルクォート内でシングルクォートを文字列として表現するためには、以下のようにエスケープする。エスケープを表現したい時も同様にエスケープする。

但し、その他についてはマーク「」が直接表示される。従って、改行やタブなどを表現したいときは、シングルクォートは使えない。

<?php
$name = 'John's';
$str = 'This is \';
print($name);//John's
print($str);//This is 
?>

ダブルクォート

以下のようにダブルクォートで括ると変数が展開される。

<?php
$name = 'John';
$str = "Hello, $name";
echo $str;//Hello, John
?>

もしも展開される変数名の後にアルファベットが続いている場合、PHPは
変数名を判断できない。従って、以下のように{}で括ってあげる必要がある。

<?php
$fruit = 'apple';
$str = "This is two {$fruit}s";
//$str = "This is two ${fruit}s";
print($str);
?>

個人的には好きではないが、コメントアウトしたような括り方をする人もいるようだ。また、マーク「$」を表示させたい場合は、変数と認識されないように、エスケープされる必要がある。

ヒアドキュメント

以下のようにヒアドキュメント内でも、変数は展開される。自分の環境ではエラーは出なかったが「<<<」と識別子の間にはスペースが1つ以上必要とされる。最初の識別子の後にセミコロンは置かない。最後の識別子の後にはセミコロン、もしくは改行が置かれなくてはならない。また、最後の識別子の前の改行コードのみ削除される。

<?php
$name = 'Jack';
$str = <<< EOD
Hello, $name
My name is Mike.
This is ......
EOD;
print($str);
/*
Hello, Jack
My name is Mike.
This is ......
*/
?>

但し、以下のようなクラスのプロパティにおいて、ヒアドキュメントはパースエラーとなってしまい使用できない。

<?php
class Person {
    $greeting = <<< EOD
Hello, everyone!
EOD;
}
?>

個人的な見解であるが、ヒアドキュメントは可読性が下がると考えている。従って、正直に言ってしまうと嫌いな方である。

■文字列の長さ

以下のようにstrlen関数を使うと、文字列の長さを判定できる。但し、マルチバイト文字を扱うときは、mb_strlen関数を使用しなくては正しい結果が導き出せない。

<?php
$str = 'She said that she likes him';
print(strlen($str));//27
?>

各関数について

int strlen(string $str)
文字列の長さを求める。
int mb_strlen(string $str[, string $encoding])
マルチバイト文字列の長さを求める。

■文字列の変換

以下のようにstrtr関数を使うと、文字列の中の特定の値について変換が行われる。

<?php
$str = 'we';
$str = strtr($str, 'w', 'm');
print($str);//me
?>

また、以下のように連想配列を変換テーブルに設定することもできる。

<?php
$str = 'we';
$str = strtr($str,
    array(
        'w' => 'm',
        'e' => 'y'
    )
);
print($str);//my
?>

但し、以下のような場合は上手くいかない。strtr関数においては検索文字列と変換後文字列の文字数は等しいことが前提であり、長いほうの余分な文字列は無視されるからだ。

<?php
$str = 'She loves him.';
$str = strtr($str, 'She', 'Her dog');
print($str);//Her lovrs eim.
?>

各関数について

string strtr(string $str, string $form, string $to)
string strtr(string $str, array $relace_table)
特定の文字を変換する。

大文字と小文字の変換

以下のような関数を使って大文字と小文字を変換することができる。

<?php
$str = 'johN nortoN';
print(strtolower($str));//john norton
print(strtoupper($str));//JOHN NORTON
print(ucfirst($str));//JohN nortoN
print(ucwords($str));//JohN NortoN
?>
各関数について
string strtolower(string $str)
文字列を小文字にする。
string strtoupper(string $str)
文字列を大文字にする
string ucfirst(string $str)
文字列の最初の文字を大文字にする。
string ucwords(string $str)
単語ごとの最初の文字を大文字にする。

■空白の除去

以下のようにtrim関数を用いて余分な空白を除去することができる。

<?php
$str = '   Pochi ';
print(trim($str));//Pochi
?>

各関数について

string trim(string $str[, string $charlist])
文字列の先頭と末尾からホワイトスペースなどを取り除く。

■URLのエンコードとデコード

URLのエンコード方式には、スペースの扱いが異なる2種類の関数が用意されている。

RFC1738方式

ディレクトリ名などの変換に使用する。

<?php
$str = 'john norton';
print(rawurlencode($str));//john%20norton
?>

application/x-www/form-urlencoded方式

クッキーやクエリ文字列の変換に使用する。但し、クッキーは自動で変換されるので、主にクエリ用に使う。

<?php
$str = 'john norton';
print(urlencode($str));//john+norton
?>
各関数について
string rawurlencode(string $str)
RFC1738方式に基づきURLをエンコードする。
string rawurldecode(string $str)
RFC1738方式に基づきURLをデコードする。
string urlencode(string $str)
application/x-www/form-urlencoded方式に基づきURLをエンコードする。
string urldecode(string $str)
application/x-www/form-urlencoded方式に基づきURLをデコードする。

jopGlobalBlogArchive(WordPress MU Plugin)

WordPress MUで、表示中以外のブログを含めたサイト全体の新着ブログを表示するプラグインです。デフォルトのWordPress MUではできませんが、このプラグインを使えば可能になります。

みなさまのお問い合わせやバグの指摘などから「正しい動作の実現」や「機能の追加」ができ、大変ありがたい限りです。是非とも、遠慮なくコメント欄でお問い合わせください。

■ダウンロード

version 0.1.8(2009.09.15 latest release)
WordPress MU 2.8.3でテスト(運用中)
文字数制限機能の追加。
ブログIDの指定による表示ブログ・非表示ブログ指定機能の追加。
ページのリンク書き出しのにおけるバグを修正。
version 0.1.7(2009.09.04)
WordPress MU 2.8.3でテスト(運用中)
<!–more–>タグ以降の表示をオプションで設定できるように改良。
version 0.1.6(2009.08.28)
WordPress MU 2.8.3でテスト
インストール後に全ての記事で「投稿を更新」のボタンを押すことなく、記事を表示できるように改良。
version 0.1.5(2009.08.27)
WordPress MU 2.8.3でテスト
JopGlobalBlogIndex::the_timeメソッドにおいて、Wordpressと同じ引数が指定できるように改良。
version 0.1.4(2009.08.11)
WordPress MU 2.8.3でテスト
prefixをデフォルト以外にした時、投稿者名が表示されないバグを修正
ブログのタイトルや本文に(SQL文で使用される)特殊な文字があった場合に、投稿が表示されないバグを修正
複数回ループできないバグを修正
version 0.1.3(2009.07.09)
記事の削除ができないバグを修正
version 0.1.2(2009.06.24)
投稿者名が表示されるように改良
version 0.1.1
β版:Warningの警告が出るのを修正
WordPress MU 2.7.0でテスト済み(運用中)
ディレクトリでブログを増やす場合でテスト済み、サブドメインの場合は未検証
version 0.1.0(2009.04.22)
β版
WordPress MU 2.7.0でテスト済み(運用中)
ディレクトリでブログを増やす場合でテスト済み、サブドメインの場合は未検証

■仕様

  • PHP5以上
  • サブドメインでブログを管理する設定には未対応

■インストール方法

  1. 「/wp-content/mu-plugins/」に解凍したファイル、「jopGlobalBlogArchive.php」を設置する

■使い方

テンプレート

例えば以下のような記述をテンプレート内にすると、サイト内の新規ブログを新しい順に5個(デフォルト)表示します。x個表示したい場合は、have_posts()の引数をhave_posts(x)と記述すれば、任意の回数だけ表示します。

通常のWordPressタグと同じようなコントロールができます。但し、query_posts関数は使用できません。

<div class="section">
<?php while(JopGlobalBlogIndex::have_posts()): ?>
<?php JopGlobalBlogIndex::the_post(); ?>
<div class="item">
<h3><a href="<?php print(JopGlobalBlogIndex::the_permalink()); ?>" title="<?php print(JopGlobalBlogIndex::the_title()); ?>"><?php print(JopGlobalBlogIndex::the_title()); ?></a>(<?php print(JopGlobalBlogIndex::the_author()); ?>)<?php print(JopGlobalBlogIndex::the_time('Y/m/d H:i:s')); ?></h3>
<p class="item_category">Category: <?php print(JopGlobalBlogIndex::the_category()); ?></p>
<?php print(JopGlobalBlogIndex::the_content()); ?>
<p class="readmore"><a href="<?php print(JopGlobalBlogIndex::the_permalink()); ?>" title="<?php print(JopGlobalBlogIndex::the_title()); ?>">Read More</a></p>
</div>
<?php endwhile; ?>
</div>

解説

<?php while(JopGlobalBlogIndex::have_posts([int $limit = 5[, array $option = array('allowedId' => array(), 'restrictedId' => array())]])): ?>
<?php JopGlobalBlogIndex::the_post(); ?>

から

<?php endwhile; ?>

の部分が記事の個数だけループされる部分です。$option[‘allowedId’]には表示したいブログのIDを配列で指定でき、$option[‘restrictedId’]には表示したくないブログのIDを指定できます。但し、$option[‘allowedId’]か$option[‘restrictedId’]の一方しか使用できず、両方指定してある場合は前者が有効となります。

ループの中で使えるタグ一覧
<?php print(JopGlobalBlogIndex::the_permalink()); ?>
記事のリンクを表示
<?php print(JopGlobalBlogIndex::the_title()); ?>
記事のタイトルを表示
<?php print(JopGlobalBlogIndex::the_author()); ?>
記事の投稿者名を表示
<?php print(JopGlobalBlogIndex::the_time(‘Y/m/d H:i:s’)); ?>
記事の公開日時を表示
<?php print(JopGlobalBlogIndex::the_category()); ?>
記事のカテゴリーを表示
<?php print(JopGlobalBlogIndex::the_summary([int $limit = 50])); ?>
記事の本文を表示。引数にtrueを設定すると、<!–more–>以降は表示されなくなる。デフォルトはfalse
<?php print(JopGlobalBlogIndex::the_content([bool $omission = false])); ?>
記事の本文を表示。引数にtrueを設定すると、<!–more–>以降は表示されなくなる。デフォルトはfalse

■既知のバグ

パーマリンクについて
リンクを書き換えるプラグインを使用し、親ブログと子ブログのうちどちらか一方で使用していた場合、リンクの書き換えに失敗し、正しいリンク先が表示できない。

■version 0.1.8 ソースコード

jopGlobalBlogArchive.php

「/wp-content/mu-plugins/jopGlobalBlogArchive.php」

<?php
/*
Plugin Name: jopGlobalBlogArchive
Plugin URI: http://blog.justoneplanet.info/wp-content/uploads/wordpressmu/jopGlobalBlogArchive/
Description: Prepare the list of some newer posts and categories from WPMU.
Author: Mitsuaki Ishimoto
Version: 0.1.8
Author URI: http://blog.justoneplanet.info/
*/
/*  Copyright 2009  Mitsuaki Ishimoto  (email : justoneplanet.info)

    This program is free software; fundamentally, you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
    
    But the Author(:Mitsuaki Ishimoto) can deny for someone evil or immoral to use this plugin.
    it means someone evil or immoral is the people and organizations to hold a candle to the devil,
    to harm someone, not to observe various laws, not to contribute society and so on.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/
//=======================================================
//actions
add_action('publish_post', 'jopSavePostForArchive');
add_action('publish_page', 'jopSavePostForArchive');
add_action('delete_post', 'jopDeletePostForArchive');

add_action('delete_category', 'jopDeleteCategoryForArchive');
//=======================================================
//class
class JopGlobalBlogIndex {
	/*
	 * $global_prefix
	 * (string) : prefix of database not including blog id
	 */
	private static $global_prefix;
	/*
	 * $post
	 * (object) : data of a post at current loop
	 */
	private static $post;
	/*
	 * $posts
	 * (array) : data of posts from the global post's table of database
	 */
	private static $posts;
	/*
	 * $counter
	 * (int) : counter for counting of loop
	 */
	private static $counter = 0;
	/**
	 * __construct
	 * An instantiation is prohibited.
	 */
	private function __construct(){
		throw new Exception('An instantiation is prohibited.');
	}
	/**
	 * getOptionsByBlogId
	 * get user's option defined by blog id and key
	 * @return (string) maybe_unserialize($_wp_alloptions[$blog_id][$key]) : option's value
	 * @param (string) $key : the key you need
	 * @param (string) maybe_unserialize($_wp_alloptions[$blog_id][$key]) : option's value
	 */
	private static function getOptionsByBlogId($key, $id){
		global $wpdb;
		$suppress = $wpdb->suppress_errors();
		$alloptions_db = $wpdb->get_results(
			"SELECT option_name, option_value FROM " . self::$global_prefix . $id . "_options FORCE INDEX(PRIMARY) ORDER BY option_id ASC"
		);
		$wpdb->suppress_errors($suppress);
		foreach((array)$alloptions_db as $o){
			$_wp_alloptions[$blog_id][$o->option_name] = $o->option_value;
		}
		wp_cache_set('alloptions', $_wp_alloptions[$blog_id], 'options');
		return maybe_unserialize($_wp_alloptions[$blog_id][$key]);		
	}
	/**
	 * get_page_uri
	 * @return 
	 */
	private static function get_page_uri(){
		global $wpdb;
		$uri = self::$post->post_name;
		if(self::$post->post_parent == self::$post->ID){
			return $uri;
		}
		$id = self::$post->post_parent;
		while($id != 0){
			$sql = "SELECT
						`ID`,`post_name`,`post_parent`
					FROM " . self::$global_prefix . self::$post->blog_id . "_posts
					WHERE `ID` = '{$id}'";
			$result = $wpdb->get_results($sql, OBJECT);
			$result = $result[0];
			$id = $result->post_parent;
			$uri = $result->post_name . "/" . $uri;
		}
		return $uri;
	}
	/**
	 * _get_page_link
	 * @return (string)
	 * @param boolean $id[optional]
	 * @param boolean $leavename[optional]
	 */
	private static function _get_page_link($id = false, $leavename = false){
		global $wp_rewrite;
		$pagestruct = $wp_rewrite->get_page_permastruct();	
		if($pagestruct != ''){
			$link = self::get_page_uri($id);
			$link = ($leavename)? $pagestruct : str_replace('%pagename%', $link, $pagestruct);
			$link = trailingslashit(self::getOptionsByBlogId('home', self::$post->blog_id)) . "$link";
			$link = user_trailingslashit($link, 'page');
		}
		else{
			$link = trailingslashit(self::getOptionsByBlogId('home', self::$post->blog_id)) . "?page_id={$post->post_id}";
		}
		return apply_filters('_get_page_link', $link, $id);		
	}
	/**
	 * get_page_link
	 * @return (string) page_link
	 */
	private static function get_page_link($id = false, $leavename = false){
		if('page' == self::getOptionsByBlogId('show_on_front', self::$post->blog_id) && self::$post->post_id == self::getOptionsByBlogId('page_on_front', self::$post->blog_id)){
			$link = self::getOptionsByBlogId('home', self::$post->blog_id);
		}
		else{
			$link = self::_get_page_link($id, $leavename);
		}
		return apply_filters('page_link', $link, $id);
	}
	/**
	 * get_permalink
	 * get the permalink from current post
	 * @return the permalink
	 * @param (int) $id[optional]
	 * @param (bool?) $leavename[optional]
	 */
	private static function get_permalink($id = 0, $leavename = false) {
		if(empty(self::$post->ID)){
			return false;
		}
		if(self::$post->post_type == 'page'){
			return self::get_page_link(self::$post->ID, $leavename);
		}
		elseif(self::$post->post_type == 'attachment'){
			return get_attachment_link(self::$post->ID);
		}
		$permalink = get_option('permalink_structure');
		if('' != $permalink && !in_array(self::$post->post_status, array('draft', 'pending'))){
			$unixtime = strtotime(self::$post->post_date);
			$category = '';
			if(strpos($permalink, '%category%') !== false){
				$cats = get_the_category(self::$post->ID);
				if($cats){
					usort($cats, '_usort_terms_by_ID'); // order by ID
					$category = $cats[0]->slug;
					if($parent = $cats[0]->parent){
						$category = get_category_parents($parent, false, '/', true) . $category;
					}
				}
				// show default category in permalinks, without
				// having to assign it explicitly
				if(empty($category)) {
					$default_category = get_category(get_option('default_category'));
					$category = is_wp_error($default_category) ? '' : $default_category->slug;
				}
			}
			$author = '';
			if(strpos($permalink, '%author%') !== false){
				$authordata = get_userdata(self::$post->post_author);
				$author = $authordata->user_nicename;
			}
			$date = explode(" ", date('Y m d H i s', $unixtime));
			$blog_path = explode('/', self::$post->path);
			$blog_path = $blog_path[count($blog_path) - 2];
			$rewritecode = array(
				'blog',
				'%year%',
				'%monthnum%',
				'%day%',
				'%hour%',
				'%minute%',
				'%second%',
				$leavename? '' : '%postname%',
				'%post_id%',
				'%category%',
				'%author%',
				$leavename? '' : '%pagename%',
			);
			$rewritereplace = array(
				(self::$post->blog_id == 1)? 'blog' : $blog_path,
				$date[0],
				$date[1],
				$date[2],
				$date[3],
				$date[4],
				$date[5],
				self::$post->post_name,
				self::$post->ID,
				$category,
				$author,
				self::$post->post_name,
			);
			$permalink = get_option('home') . str_replace($rewritecode, $rewritereplace, $permalink);
			$permalink = user_trailingslashit($permalink, 'single');
			return apply_filters('post_link', $permalink, self::$post, $leavename);
		}
		else{ // if they're not using the fancy permalink option
			$permalink = get_option('home') . '/?p=' . self::$post->ID;
			return apply_filters('post_link', $permalink, self::$post, $leavename);
		}
	}
	/**
	 * savePost
	 * save post into the table '(prefix_)jop_global_posts' on installing and updating
	 * @return (null)
	 * @param (object) $result
	 * @param (int) $blogid
	 */
	public static function savePost($result, $blogid){
		global $wpdb;
		$id = $result['ID'];
		$sql = "INSERT INTO " . self::getGlobalPrefix() . "jop_global_posts(
			`blog_id`,
			`post_id`,
			`identifier`,
			`post_author`,
			`post_date`,
			`post_date_gmt`,
			`post_content`,
			`post_title`,
			`post_category`,
			`post_excerpt`,
			`post_status`,
			`comment_status`,
			`ping_status`,
			`post_password`,
			`post_name`,
			`to_ping`,
			`pinged`,
			`post_modified`,
			`post_modified_gmt`,
			`post_content_filtered`,
			`post_parent`,
			`guid`,
			`menu_order`,
			`post_type`,
			`post_mime_type`,
			`comment_count`
		) VALUES('"
			. $blogid . "', '"
			. $result['ID'] . "', '"
			. $blogid . "/" . $result['ID'] . "', '"
			. $wpdb->_real_escape($result['post_author']) . "', '"
			. $result['post_date'] . "', '"
			. $result['post_date_gmt'] . "', '"
			. $wpdb->_real_escape($result['post_content']) . "', '"
			. $wpdb->_real_escape($result['post_title']) . "', '"
			. $result['post_category'] . "', '"
			. $wpdb->_real_escape($result['post_excerpt']) . "', '"
			. $wpdb->_real_escape($result['post_status']) . "', '"
			. $wpdb->_real_escape($result['comment_status']) . "', '"
			. $wpdb->_real_escape($result['ping_status']) . "', '"
			. $wpdb->_real_escape($result['post_password']) . "', '"
			. $wpdb->_real_escape($result['post_name']) . "', '"
			. $wpdb->_real_escape($result['to_ping']) . "', '"
			. $wpdb->_real_escape($result['pinged']) . "', '"
			. $result['post_modified'] . "', '"
			. $result['post_modified_gmt'] . "', '"
			. $wpdb->_real_escape($result['post_content_filtered']) . "', '"
			. $result['post_parent'] . "', '"
			. $wpdb->_real_escape($result['guid']) . "', '"
			. $result['menu_order'] . "', '"
			. $wpdb->_real_escape($result['post_type']) . "', '"
			. $wpdb->_real_escape($result['post_mime_type']) . "', '"
			. $result['comment_count'] . "'
		) ON DUPLICATE KEY UPDATE "
			. "`post_author` = '"           . $wpdb->_real_escape($result['post_author']) . "',"
			. "`post_date` = '"             . $result['post_date'] . "',"
			. "`post_date_gmt` = '"         . $result['post_date_gmt'] . "',"
			. "`post_content` = '"          . $wpdb->_real_escape($result['post_content']) . "',"
			. "`post_title` = '"            . $wpdb->_real_escape($result['post_title']) . "',"
			. "`post_category` = '"         . $result['post_category'] . "',"
			. "`post_excerpt` = '"          . $wpdb->_real_escape($result['post_excerpt']) . "',"
			. "`post_status` = '"           . $wpdb->_real_escape($result['post_status']) . "',"
			. "`comment_status` = '"        . $wpdb->_real_escape($result['comment_status']) . "',"
			. "`ping_status` = '"           . $wpdb->_real_escape($result['ping_status']) . "',"
			. "`post_password` = '"         . $wpdb->_real_escape($result['post_password']) . "',"
			. "`post_name` = '"             . $wpdb->_real_escape($result['post_name']) . "',"
			. "`to_ping` = '"               . $wpdb->_real_escape($result['to_ping']) . "',"
			. "`pinged` = '"                . $wpdb->_real_escape($result['pinged']) . "',"
			. "`post_modified` = '"         . $result['post_modified'] . "',"
			. "`post_modified_gmt` = '"     . $result['post_modified_gmt'] . "',"
			. "`post_content_filtered` = '" . $wpdb->_real_escape($result['post_content_filtered']) . "',"
			. "`post_parent` = '"           . $result['post_parent'] . "',"
			. "`guid` = '"                  . $wpdb->_real_escape($result['guid']) . "',"
			. "`menu_order` = '"            . $result['menu_order'] . "',"
			. "`post_type` = '"             . $wpdb->_real_escape($result['post_type']) . "',"
			. "`post_mime_type` = '"        . $wpdb->_real_escape($result['post_mime_type']) . "',"
			. "`comment_count` = '"         . $result['comment_count'] . "'";
		$wpdb->query($sql);
		
		/*
		 * pick up the term id for save
		 */
		$sql = "SELECT "
					. self::getGlobalPrefix() . $blogid . "_term_relationships.object_id, "
					. self::getGlobalPrefix() . $blogid . "_term_taxonomy.term_id
				FROM "
					. self::getGlobalPrefix() . $blogid . "_term_relationships
				LEFT JOIN "
					. self::getGlobalPrefix() . $blogid . "_term_taxonomy
				ON "
					. self::getGlobalPrefix() . $blogid . "_term_relationships.term_taxonomy_id = " . self::getGlobalPrefix() . $blogid . "_term_taxonomy.term_taxonomy_id
				WHERE "
					. self::getGlobalPrefix() . $blogid . "_term_relationships.`object_id` = '{$result['ID']}'";
		$result = $wpdb->get_results($sql, ARRAY_A);
		/*
		 * Once deleting for update
		 */
		$sql = "DELETE FROM "
					. self::getGlobalPrefix() . "jop_global_term_relationships
				WHERE `object_id` = '{$id}'
				AND `blog_id` = '{$blogid}'";
		$wpdb->query($sql);
	
		/*
		 * insert
		 */
		$values = array();
		foreach($result as $key => $value){
			$values[] = "'{$value['object_id']}', '{$blogid}', '{$value['term_id']}', '{$blogid}/{$value['object_id']}/{$value['term_id']}'";
		}
		$values = '(' . implode('),(', $values) . ')';
		$sql = "INSERT INTO " . self::getGlobalPrefix() . "jop_global_term_relationships(
			`object_id`,
			`blog_id`,
			`cat_ID`,
			`identifier`
		) VALUES $values";
		$wpdb->query($sql);
	}
	/**
	 * install
	 * 1st query makes the table that keep the posts written by all users.
	 * 2nd query makes the table that keep the terms written by all users.
	 * @return (null)
	 */
	private static function install(){
		global $wpdb;
		$table_name = self::$global_prefix . "jop_global_term_relationships";
		if($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name){
			$sql = "CREATE TABLE IF NOT EXISTS `$table_name` (
						`object_id` bigint(20) NOT NULL DEFAULT '0',
						`blog_id` int(11) NOT NULL DEFAULT '0',
						`cat_ID` bigint(20) NOT NULL DEFAULT '0',
						`identifier` varchar(255) NOT NULL DEFAULT '0/0',
						UNIQUE KEY `identifier` (`identifier`),
						KEY `cat_ID` (`cat_ID`),
						KEY `blog_id` (`blog_id`)
					) ENGINE=MyISAM DEFAULT CHARSET=utf8";
			$wpdb->get_results($sql);
		}
		$table_name = self::$global_prefix . "jop_global_posts";
		if($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name){
			$sql = "CREATE TABLE IF NOT EXISTS `$table_name` (
						`ID` bigint(20) NOT NULL AUTO_INCREMENT,
						`blog_id` int(11) NOT NULL DEFAULT '0',
						`post_id` int(11) NOT NULL DEFAULT '0',
						`identifier` varchar(255) NOT NULL,
						`post_author` bigint(20) NOT NULL DEFAULT '0',
						`post_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
						`post_date_gmt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
						`post_content` longtext NOT NULL,
						`post_title` text NOT NULL,
						`post_category` int(4) NOT NULL DEFAULT '0',
						`post_excerpt` text NOT NULL,
						`post_status` varchar(20) NOT NULL DEFAULT 'publish',
						`comment_status` varchar(20) NOT NULL DEFAULT 'open',
						`ping_status` varchar(20) NOT NULL DEFAULT 'open',
						`post_password` varchar(20) NOT NULL,
						`post_name` varchar(200) NOT NULL,
						`to_ping` text NOT NULL,
						`pinged` text NOT NULL,
						`post_modified` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
						`post_modified_gmt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
						`post_content_filtered` text NOT NULL,
						`post_parent` bigint(20) NOT NULL DEFAULT '0',
						`guid` varchar(255) NOT NULL,
						`menu_order` int(11) NOT NULL DEFAULT '0',
						`post_type` varchar(20) NOT NULL DEFAULT 'post',
						`post_mime_type` varchar(100) NOT NULL,
						`comment_count` bigint(20) NOT NULL DEFAULT '0',
						PRIMARY KEY (`ID`),
						UNIQUE KEY `identifier` (`identifier`),
						KEY `blog_id` (`blog_id`),
						KEY `post_id` (`post_id`)
					) ENGINE=MyISAM  DEFAULT CHARSET=utf8";
			$wpdb->get_results($sql);
			/*
			 * save the posts have been published.
			 */
			$tables = $wpdb->get_results("SHOW TABLES LIKE '" . self::$global_prefix . "_\_posts'", ARRAY_N);
			foreach($tables as $table){
				$results = $wpdb->get_results("SELECT * FROM {$table[0]} WHERE `post_status` = 'publish'", ARRAY_A);
				$blogid = str_replace(array(self::$global_prefix, '_posts'), '', $table[0]);
				foreach($results as $result){
					self::savePost($result, $blogid);
				}
			}
		}
	}
	/**
	 * init
	 * @return null
	 */
	public static function init(){
		global $wpdb;
		self::$global_prefix = preg_replace("/[0-9]+_/", '', $wpdb->prefix);
		self::install();
	}
	/**
	 * getGlobalPrefix
	 * method for access to prefix
	 * @return (string) self::$global_prefix : table prefix
	 */
	public static function getGlobalPrefix(){
		return self::$global_prefix;
	}
	/**
	 * have_posts
	 * set the list of newer posts written by all users if self::$posts is not set
	 * judge if the self::$posts of current index exist or not
	 * @return (bool)
	 * @param (int) $limit : define the number of rows.
	 * @param (array) $option : define the option.
	 */
	public static function have_posts($limit = 5, $option = array()){
		global $wpdb;
		if(!isset(self::$posts)){
			$col = array(
				'ID',
				'blog_id',
				'post_id',
				'post_date',
				'post_content',
				'post_name',
				'post_author',
				'post_title',
				'post_category',
				'post_status',
				'post_type',
				'post_parent',
				'guid'
			);
			foreach($col as $key => $value){
				$col[$key] = self::$global_prefix . "jop_global_posts." . $value;
			}
			$col = implode(',', $col);
			$sqlForAllowedBlog = '';
			if(is_array($option['allowedId'])){
				for($i = 0; $i < count($option['allowedId']); $i++){
					if(is_int($option['allowedId'][$i]) && $option['allowedId'][$i] > 0){
						$sqlForAllowedBlog .= " OR " . self::$global_prefix . "jop_global_posts.`blog_id` = '{$option['allowedId'][$i]}'";
					}
				}
				$sqlForAllowedBlog = substr($sqlForAllowedBlog, 4);
			}
			elseif(is_array($option['restrictedId'])){
				for($i = 0; $i < count($option['restrictedId']); $i++){
					if(is_int($option['restrictedId'][$i]) && $option['restrictedId'][$i] > 0){
						$sqlForAllowedBlog .= " AND " . self::$global_prefix . "jop_global_posts.`blog_id` != '{$option['restrictedId'][$i]}'";
					}
				}
				$sqlForAllowedBlog = substr($sqlForAllowedBlog, 5);
			}
			if($sqlForAllowedBlog !== ''){
				$sqlForAllowedBlog = " AND ($sqlForAllowedBlog) ";
			}
			$sql = "SELECT
						{$col}," . self::$global_prefix . "blogs.path
					FROM "
						. self::$global_prefix . "jop_global_posts
					LEFT JOIN "
						. self::$global_prefix . "blogs
					ON "
						. self::$global_prefix . "jop_global_posts.blog_id = " . self::$global_prefix . "blogs.blog_id
					WHERE "
						. self::$global_prefix . "jop_global_posts.`post_status` = 'publish'
					AND "
						. self::$global_prefix . "jop_global_posts.`post_password` = ''
					$sqlForAllowedBlog
					ORDER BY `post_date` DESC
					LIMIT 0,$limit";
			$result = $wpdb->get_results($sql, OBJECT);
			self::$posts = $result;
		}
		if(isset(self::$posts[self::$counter])){
			return true;
		}
		else{
			self::$counter = 0;
			self::$posts = null;//for multiple loop(in the case of different situation)
			return false;
		}
	}
	/**
	 * the_post
	 * get the post in a loop and set the counter for the next loop.
	 * @return (array) self::$post
	 */
	public static function the_post(){
		self::$post = self::$posts[self::$counter];
		self::$counter++;
		return self::$post;
	}
	/**
	 * the_title
	 * get the title from the current loop. 
	 * @return (string) self::$post->post_title
	 */
	public static function the_title(){
		return apply_filters('the_title', self::$post->post_title);
	}
	/**
	 * the_ID
	 * get the title from the current loop. 
	 * @return (string) self::$post->ID
	 */
	public static function the_ID(){
		return self::$post->ID;
	}
	/**
	 * the_permalink
	 * get the title from the current loop. 
	 * @return (string) self::$post->guid
	 */
	public static function the_permalink(){
		return apply_filters('the_permalink', self::get_permalink());
	}
	/**
	 * the_time
	 * get the title from the current loop. 
	 * @return (string) self::$post->post_date
	 */
	public static function the_time($d = ''){
		return apply_filters('the_time', self::get_the_time( $d ), $d);
	}
	/**
	 * get_the_time
	 * @return (String) date
	 */
	private function get_the_time($d = ''){
		if('' == $d){
			$the_time = get_post_time(get_option('time_format'), false, self::$post, true);
		}
		else{
			$the_time = get_post_time($d, false, self::$post, true);
		}
		return apply_filters('get_the_time', $the_time, $d, self::$post);
	}
	/**
	 * the_author
	 * get the title from the current loop. 
	 * @return (string) self::$post->post_author
	 */
	public static function the_author(){
		global $wpdb;
		$id = self::$post->post_author;
		$sql = "SELECT `user_nicename` FROM `" . self::$global_prefix . "users` WHERE `ID` = '$id'";
		$result = $wpdb->get_results($sql, ARRAY_N);
		return $result[0][0];
	}
	/**
	 * the_content
	 * get the body from the current loop. 
	 * @return (string) self::$post->post_content
	 */
	public static function the_content($stripteaser = false){
		$content = self::$post->post_content;
		if($stripteaser){
			list($content) = preg_split('/<!--more(.*?)?-->/', $content);
		}
		$content = apply_filters('the_content', $content);
		$content = str_replace(']]>', ']]&gt;', $content);
		return $content;
	}
	/**
	 * the_summary
	 * get the summary form the current loop.
	 * @return (string) self::$post->post_content
	 * @param (int) $limit
	 */
	public static function the_summary($limit = 50){
		if(!is_int($limit)){
			$limit = 50;
		}
		$content = self::$post->post_content;
		$funcs = get_defined_functions();
		if(in_array('mb_substr', $funcs['internal'])){
			$content = mb_substr(strip_tags($content), 0, $limit);
		}
		else{
			$content = substr(strip_tags($content), 0, $limit);
		}
		$content = str_replace(']]>', ']]&gt;', $content);
		return $content;
	}
	/**
	 * the_category
	 * get the title from the current loop. 
	 * @return (string) self::$post->post_category
	 */
	public static function the_category(){
		global $wpdb;
		$blog_id = self::$post->blog_id;
		$post_id = self::$post->post_id;
		$sql = "SELECT `"
					. self::$global_prefix . "sitecategories`.`cat_name`
				FROM `"
					. self::$global_prefix . "jop_global_term_relationships`
				LEFT JOIN `"
					. self::$global_prefix . "sitecategories`
				ON `"
					. self::$global_prefix . "jop_global_term_relationships`.`cat_ID` = `" . self::$global_prefix . "sitecategories`.`cat_ID`
				WHERE `"
					. self::$global_prefix . "jop_global_term_relationships`.`object_id` = '$post_id'
				AND `"
					. self::$global_prefix . "jop_global_term_relationships`.`blog_id` = '$blog_id'";
		$tmpAry = array();
		$result = $wpdb->get_results($sql, ARRAY_A);
		foreach($result as $key => $value){
			$tmpAry[] = $value['cat_name'];
		}
		sort($tmpAry);
		return implode(', ', $tmpAry);
	}
	/*prepare*/
	public static function the_tags(){
		return self::$post->post_title;
	}
	public static function the_title_attribute(){
		return self::$post->post_title;
	}
	/*categories*/
	/**
	 * wp_list_categories
	 * get the title from the current loop. 
	 * @return (string) self::$post->post_category
	 */
	public static function wp_list_categories($args = ''){
		$defaults = array(
			'show_option_all'    => '',
			'orderby'            => 'name',
			'order'              => 'ASC',
			'show_last_update'   => 0,
			'style'              => 'list',
			'show_count'         => 0,
			'hide_empty'         => 1,
			'use_desc_for_title' => 1,
			'child_of'           => 0,
			'feed'               => '',
			'feed_type'          => '',
			'feed_image'         => '',
			'exclude'            => '',
			'current_category'   => 0,
			'hierarchical'       => true,
			'title_li'           => __('Categories'),
			'echo'               => 1,
			'depth'              => 0
		);
		$r = wp_parse_args($args, $defaults);
		if(!isset($r['pad_counts']) && $r['show_count'] && $r['hierarchical']){
			$r['pad_counts'] = true;
		}
		if(isset($r['show_date'])){
			$r['include_last_update_time'] = $r['show_date'];
		}
		extract($r);
		$categories = get_categories($r);
		$output = '';
		if($title_li && 'list' == $style){
			$output = '<li class="categories">' . $r['title_li'] . '<ul>';
		}
		if(empty($categories)){
			if('list' == $style){
				$output .= '<li>' . __("No categories") . '</li>';
			}
			else{
				$output .= __("No categories");
			}
		}
		else{
			global $wp_query;
			if(!empty($show_option_all)){
				if('list' == $style){
					$output .= '<li><a href="' .  get_bloginfo('url')  . '">' . $show_option_all . '</a></li>';
				}
				else{
					$output .= '<a href="' .  get_bloginfo('url')  . '">' . $show_option_all . '</a>';
				}
			}
			if(empty($r['current_category']) && is_category()){
				$r['current_category'] = $wp_query->get_queried_object_id();
			}
			if($hierarchical){
				$depth = $r['depth'];
			}
			else{
				$depth = -1; // Flat.
			}
			$output .= walk_category_tree($categories, $depth, $r);
		}
		if($title_li && 'list' == $style){
			$output .= '</ul></li>';
		}
		$output = apply_filters('wp_list_categories', $output);
		if($echo){
			echo $output;
		}
		else{
			return $output;
		}
	}
}
JopGlobalBlogIndex::init();
//=======================================================
//functions
/**
 * jopDeleteCategoryForArchive
 * @return (null)
 * @param (int) $id 
 */
function jopDeleteCategoryForArchive($id){
	global $wpdb;
	$sql = "DELETE FROM "
				. JopGlobalBlogIndex::getGlobalPrefix() . "jop_global_term_relationships
			WHERE `blog_id` = '{$wpdb->blogid}' AND `cat_ID` = '{$id}'";
	$wpdb->query($sql);
}
/**
 * jopSavePostForArchive
 * @return (null)
 * @param (int) $id 
 */
function jopSavePostForArchive($id){
	global $wpdb;
	$sql = "SELECT * FROM "
				. JopGlobalBlogIndex::getGlobalPrefix() . $wpdb->blogid . "_posts
			WHERE `ID` = '$id'";
	$result = $wpdb->get_results($sql, ARRAY_A);
	$result = $result[0];
	JopGlobalBlogIndex::savePost($result, $wpdb->blogid);
}
/**
 * jopDeletePostForArchive
 * @return (null)
 * @param (int) $id 
 */
function jopDeletePostForArchive($id){
	global $wpdb;
	$sql = "DELETE FROM "
				. JopGlobalBlogIndex::getGlobalPrefix() . "jop_global_posts
			WHERE `identifier` = '" . $wpdb->blogid . '/' . $id . "'";
	$wpdb->query($sql);
	//var_dump($sql);
	//throw new Exception();
}
?>

■ライセンスについて

GNU General Public Licenseです。但し、法律に従わない組織や個人などの邪悪な(者の)使用は認めません。

PHP Arrays as Stacks, Queues and Set(スタック・待ち行列・和集合)

■スタック

スタックとは

順列において、最初に挿入されたデータが最後に取り出される。最後に挿入したデータが最初に取り出されるデータ構造。LIFO(last in first out)と呼ばれる。

実際のコード

以下のようにarray_push関数を使用すると、第一引数の配列の最後尾に第二引数以降の値を、配列の要素として追加する。また、array_pop関数を使用すると、配列の最終要素を削除し戻り値として返します。

<?php
$stack = array();
array_push($stack, 'John', 'Jack');
var_dump($stack);
/*
array(2) {
  [0]=>
  string(4) "John"
  [1]=>
  string(4) "Jack"
}
*/
$value = array_pop($stack);
var_dump($value);
var_dump($stack);
/*
string(4) "Jack"
array(1) {
  [0]=>
  string(4) "John"
}
*/
?>

但し、array_pushについては以下のようなコードでも同じようなことができる。そして、関数をコールするオーバーヘッドが発生しない分だけわずかに高速である。

<?php
$stack = array();
$stack[] = 'John';
$stack[] = 'Jack';
?>
各関数について
int array_push(array &$ary, mixed $var[, mixed $var, …])
$aryの最後に、第二引数以降の値を配列の要素として追加する。
mixed array_pop(array &$ary)
配列の最後から要素を取り除き、その値を返す。

ちなみにJavaScriptで以下のように書いてしまうと、シンタックスエラーとなる。

var tmpAry[] = 'test';
//syntax error!!

■キュー(待ち行列)

待ち行列とは

データが順列に挿入された順序で、削除(処理)されていくようなデータ構造。FIFO(first in first out)と呼ばれる。

実際のコード

以下のように、array_shift関数を使うと第一引数で指定した配列の最初の要素を削除できる。また、array_unshift関数を使うと第一引数で指定した配列に、第二引数以降の要素を代入することができる。

<?php
$queue = array('John', 'Ken', 'Mike');
$value = array_shift($queue);
var_dump($value);//John
var_dump($queue);
/*
array(2) {
  [0]=>
  string(3) "Ken"
  [1]=>
  string(4) "Mike"
}
*/
array_unshift($queue, 'Nick', 'Emily');
var_dump($queue);
/*
array(4) {
  [0]=>
  string(4) "Nick"
  [1]=>
  string(5) "Emily"
  [2]=>
  string(3) "Ken"
  [3]=>
  string(4) "Mike"
}
*/
?>

shiftとunshiftを使うとスタックと変わらない。従ってキューの実装にはarray_shift()とarray_push()を用いる。

各関数について
mixed array_shift(array &$ary)
配列の先頭から要素を一つ取り出し、その値を返す。
int array_unshift(array &$ary, mixed $var[, mixed $var])
第一引数の配列に、第二引数以降の値を配列の要素として加える。

■集合と配列の比較

集合とは、いくつかの要素の集まったデータ構造。配列を使用すると、集合論の基本となる和集合や積集合、対象差といった演算を実装できる。

和集合

集合Aと集合Bを結合し、重複を取り除いた集合。

<?php
$a = array(1, 5, 3, 9);
$b = array(2, 5, 9, 7);
$tmpAry = array_merge($a, $b);
$set = array_unique($tmpAry);
var_dump($set);
/*
array(6) {
  [0]=>
  int(1)
  [1]=>
  int(5)
  [2]=>
  int(3)
  [3]=>
  int(9)
  [4]=>
  int(2)
  [7]=>
  int(7)
}
*/
?>

配列(集合)の差分

array_diff関数を使うと配列の差を計算することができる。以下のような場合、第一引数の配列の要素で、第二引数以降の配列に含まれて居ない要素を配列として返す。

<?php
$a = array(1, 3, 9, 27);
$b = array(1, 9, 2, 7);
var_dump(array_diff($a, $b));
/*
array(2) {
  [1]=>
  int(3)
  [3]=>
  int(27)
}
*/
?>

また、配列の比較においてキーまで含める場合は以下のように、array_diff_assoc関数を使用する。

<?php
$a = array(1, 3, 9, 27);
$b = array(1, 9, 2, 7);
var_dump(array_diff_assoc($a, $b));
/*
array(3) {
  [1]=>
  int(3)
  [2]=>
  int(9)
  [3]=>
  int(27)
}
*/
?>

配列(集合)の共通項

array_intersect関数は以下のように、配列の共通項のリストを返す。

<?php
$a = array(1, 3, 9, 27);
$b = array(1, 9, 2, 7);
var_dump(array_intersect($a, $b));
/*
array(2) {
  [0]=>
  int(1)
  [2]=>
  int(9)
}
*/
?>

各関数について

array array_merge(array $ary[, array $ary2])
複数の配列をマージし結果を返す。但し、連想配列の文字列が重複している場合は値が上書きされる。
array array_unique(array $ary[, int $sort_flags=SORT_REGULAR])
配列から重複を取り除いた配列を返す。
array array_diff(array $ary1, array $ary2)
$ary1の要素のうち$ary2に含まれないものを配列として返す。
array array_intersect(array $ary1, array $ary2)
配列の共通項を配列として返す。

PHP Sorting Arrays(配列のソート)

■配列の値によるソート(0から始まる数値インデックスに直す)

以下のように、sort関数を使うと値をアルファベット順にソートし、インデックスを振りなおす。また第二引数にはSORT_REGULAR、SORT_NUMERIC、SORT_STRINGなどを取り、型を変換する事もできる。

<?php
$tmpAry = array(
    'person1' => 'Emily',
    'person2' => 'Dennis',
    'person3' => 'Andy'
);
sort($tmpAry);
var_dump($tmpAry);
/*
array(3) {
  [0]=>
  string(4) "Andy"
  [1]=>
  string(6) "Dennis"
  [2]=>
  string(5) "Emily"
}
*/
?>

逆順にソートするには

以下のようにrsort関数を使用すると、sort関数を使った時とは逆順に並ぶ。

<?php
$tmpAry = array(
    'person1' => 'Andy',
    'person2' => 'Dennis',
    'person3' => 'Emily'
);
rsort($tmpAry);
var_dump($tmpAry);
/*
array(3) {
  [0]=>
  string(5) "Emily"
  [1]=>
  string(6) "Dennis"
  [2]=>
  string(4) "Andy"
}
*/
?>

戻り値について

以下の例のようにsort関数は配列自身を変換し、戻り値は処理の成否についてのbool値が格納されるだけである。基本的に配列ソート関数は全て配列自身を変換し、戻り値は処理の成否としてのbool値となる。

<?php
var_dump(sort($tmpAry));//true or false
?>

ユーザー指定関数での並び替え

以下のようにusort関数を使用する。第二引数には文字列で関数名を指定する。(但し、以下の例はグローバル空間が汚れないように、create_function関数を使っての匿名関数を利用している。)

<?php
$ary = array(1, 5, 2);
usort(
    $ary,
    create_function(
        '$a,$b',
        'if($a < $b){return 1;}if($a === $b){return 0;}if($a > $b){return -1;}'
    )
);
var_dump($ary);
/*
array(3) {
  [0]=>
  int(5)
  [1]=>
  int(2)
  [2]=>
  int(1)
}
*/
?>

■配列の値によるソート(キーとの関係は維持)

以下のようにasort関数を使うとキーとの関係は維持されつつ、値によるソートが行われる。

<?php
$tmpAry = array(
    'person1' => 'Emily',
    'person2' => 'Dennis',
    'person3' => 'Andy'
);
asort($tmpAry);
var_dump($tmpAry);
/*
array(3) {
  ["person3"]=>
  string(4) "Andy"
  ["person2"]=>
  string(6) "Dennis"
  ["person1"]=>
  string(5) "Emily"
}
*/
?>

逆順にしたい時にはarsort関数を使用し、ユーザー定義関数でソートしたいときにはuasort関数を使用する。但し、今考える限りではキーとの関係を維持したままソートする有用なシチュエーションは思い浮かばない。

■配列のキーによるソート

以下のようにksort関数を使うと、配列のキーによってソートされる。

<?php
$tmpAry = array(
    'Naomi' => 'student',
    'Dennis' => 'driver',
    'Emily' => 'traveler'
);
ksort($tmpAry);
var_dump($tmpAry);
/*
array(3) {
  ["Dennis"]=>
  string(6) "driver"
  ["Emily"]=>
  string(8) "traveler"
  ["Naomi"]=>
  string(7) "student"
}
*/
?>

また、逆順にしたい時はkrsort関数を使い、ユーザー定義関数でソートしたい時はuksort関数を使う。

PHPは内部的に配列のキーとは別に順序を持っている。内部順序とキーの順序が違う場合、json_encode関数を使うと変換が異なるので注意しなくてはならない。

<?php
$tmpAry = array(
    2 => 'Naomi',
    1 => 'Dennis',
    0 => 'Emily'
);
var_dump($tmpAry);
ksort($tmpAry);
var_dump($tmpAry);
/*
array(3) {
  [2]=>
  string(5) "Naomi"
  [1]=>
  string(6) "Dennis"
  [0]=>
  string(5) "Emily"
}
string(38) "{"2":"Naomi","1":"Dennis","0":"Emily"}"
array(3) {
  [0]=>
  string(5) "Emily"
  [1]=>
  string(6) "Dennis"
  [2]=>
  string(5) "Naomi"
}
string(26) "["Emily","Dennis","Naomi"]"
*/
?>

■まとめ

一覧にまとめると以下のようになる。

処理 昇順 降順 ユーザー定義
値によるソート、インデックスの再ラベリング sort rsort usort
値によるソート asort arsort uasort
キーによるソート ksort krsort uksort

個人的な考えだが「昇順と降順」は引数でコントロールできるようにしてもイイと思う。

ユーザー定義関数について

<?php
$ary = array(1, 5, 2);
usort(
    $ary,
    create_function(
        '$a,$b',
        'if($a < $b){return 1;}if($a === $b){return 0;}if($a > $b){return -1;}'
    )
);
var_dump($ary);
/*
array(3) {
  [0]=>
  int(5)
  [1]=>
  int(2)
  [2]=>
  int(1)
}
*/
?>
  • 引数を2つとる
  • 「if($a < $b){return 1;}」は「$a<$b」の時、そのままの順序である事を意味する。
  • 「if($a > $b){return -1;}」は「$a>$b」の時、前後を入れ替える事を意味する。
  • 「if($a === $b){return 0;}」は「$a === $b」の時、特に何もしない事を意味する。

つまり、比較関数は最初の引数が2番目の引数より小さいか、等しいか、大きい場合に、 それぞれゼロ未満、ゼロに等しい、ゼロより大きい整数を返す。

■自然順ソート

通常のソート関数を使うと、以下のように不自然な結果となる。

<?php
$tmpAry = array('10km', '5km', '7km');
sort($tmpAry);
var_dump($tmpAry);
/*
array(3) {
  [0]=>
  string(4) "10km"
  [1]=>
  string(3) "5km"
  [2]=>
  string(3) "7km"
}
*/
?>

以下のようにnatsort関数を使用すると、数字+単位のように扱われ、自然な並び順となる。また、大文字と小文字を区別しないnatcasesort関数もある。

<?php
$tmpAry = array('10km', '5km', '7km');
natsort($tmpAry);
var_dump($tmpAry);
/*
array(3) {
  [1]=>
  string(3) "5km"
  [2]=>
  string(3) "7km"
  [0]=>
  string(4) "10km"
}
*/
?>

各関数について

bool natsort(array &$ary)
自然順で配列をソートする。
bool natcasesort(array &$ary)
大文字と小文字を区別しない自然順アルゴリズムでソートする。

■配列のシャッフル

以下のようにshuffle関数を使うと、配列をランダムに並び替えることができる。以下の結果は実行ごとに異なるが、キーは必ず削除され数値のインデックスが割り当てられる。

<?php
$tmpAry = array(
    'person1' => 'Emily',
    'person2' => 'Dennis',
    'person3' => 'Andy'
);
shuffle($tmpAry);
/*
array(3) {
  [0]=>
  string(6) "Dennis"
  [1]=>
  string(5) "Emily"
  [2]=>
  string(4) "Andy"
}
*/
?>

各巻数について

bool shuffle(array &$ary)
配列をシャッフルする。但し、既存のキーは削除される。

PHP Array Iteration(配列の繰り返し)

■配列のポインタ

各々の配列は、現在の要素を示すポインタ(イテレータ)を持っている。

current()
現在のポインタが指している要素
key()
現在の要素のキーを返す
reset()
ポインタを先頭に移動
end()
ポインタを配列の最後の要素に移動
next()
ポインタを1つ次に移動
prev()
ポインタを1つ前に移動
each()
現在のキーと値を配列として返し、ポインタを次に移動

以下のように使用する。通常、whileはポインタの移動を行わないので、ポインタを移動させる関数の使用や、ループの終了条件の設定が不可欠である。また、foreachを使わないメリットは、配列のコピーを生成しないのでメモリを節約できる点である。

<?php
$tmpAry = array('a' => 1, 'b' => 2, 'c' => 3);
function display(&$array){
    reset($array);
    while(key($array) !== null){
        echo key($array) . ': ' . current($array) . PHP_EOL;
        next($array);
    }
}
display($tmpAry);
?>

以下のように配列の後方からループさせることもできる。

<?php
$tmpAry = array('a' => 1, 'b' => 2, 'c' => 3);
end($tmpAry);
while(key($tmpAry) !== null){
    echo key($array) . ': ' . current($array) . PHP_EOL;
    prev($array);
}
?>

また、以下のようにforeachと同じような表現をすることが可能だ。この方法は大きな配列でメモリーを節約したい場合などに有効である。

<?php
$tmpAry = array('a' => 1, 'b' => 2, 'c' => 3);
reset($tmpAry);
while(list($key, $value) = each($tmpAry)){
    print("{$key}: {$value}" . PHP_EOL);
}
?>

■foreach構文

上述のように、whileを使ったループコントロールはメモリ消費を抑制できるかわりに、コードが煩雑になる。従って、個人的には以下のようなforeachでのコントロールをたいていは用いる。

<?php
$tmpAry = array('a' => 1, 'b' => 2, 'c' => 3);
foreach($tmpAry as $key => $value){
    echo $key . ': ' . $value; 
}
?>

但し、foreach文では配列のコピーが生成されるので、ループ内で配列の要素($tmpAry[$key]ではなく$value)に処理を加えても、元の配列には影響しない。もしも反映させたい場合は、以下のようにリファレンス(参照)を使う。

<?php
$tmpAry = array('a' => 1, 'b' => 2, 'c' => 3);
foreach($tmpAry as $key => &$value){
    echo $key . ': ' . $value;
    $value++; 
}
var_dump($tmpAry);
/*
array(3) {
  ["a"]=>
  ∫(2)
  ["b"]=>
  ∫(3)
  ["c"]=>
  ∫(4)
}
*/
?>

しかし、この方法には後述のような危険性が潜んでいる。

foreach構文におけるリファレンス使用の注意点

以下のように空のループを2回繰り返すと、配列の中身が予想外に変更されてしまう。

<?php
$names = array('Nick', 'John', 'Mike');
foreach($names as $key => &$value){
}
foreach($names as $key => $value){
}
var_dump($names);
/*
array(3) {
  [0]=>
  string(4) "Nick"
  [1]=>
  string(4) "John"
  [2]=>
  &string(4) "John"
}
*/
?>
原因
  1. 1回目のループ:&$valueは配列の各要素の住所(リファレンス)となる。
  2. 1回目のループ:&$valueは配列の最終要素のリファレンス(&$names[2])となる。
  3. 2回目のループ:各要素が既にassignされている$value(&$names[2])に代入されていく。

つまり、$names[2] = $valueがループごとに起こる。

  1. 2回目のループ(1):$names[2]にNickが代入される。
  2. 2回目のループ(2):$names[2]にJohnが代入される。
  3. 2回目のループ(3):$names[2]に既に2回目のループ(2)で代入されているJohnにJohnが代入される。

結果、Johnが重複する。それを防ぐ方法は以下のように、unsetを使って変数の割り当てを解除する事である。

<?php
$names = array('Nick', 'John', 'Mike');
foreach($names as $key => &$value){
}
unset($value)
foreach($names as $key => $value){
}
var_dump($names);
?>

個人的には度々この手法を使用するが、unsetを書き忘れた場合に極めてデバッグしづらい事も考えられる。従って、この手法を使わないという事も選択肢の1つだ。

■for構文

扱う配列のキーが数値のみであると分かっている場合、以下のようにする事ができる。for構文は配列のコピーを生成しない。

<?php
$names = array('Nick', 'John', 'Mike');
for($i = 0; $i < count($names); $i++){
    //code
}
?>

■配列の要素に対する処理

以下のように、array_walkやarray_walk_recursive(子配列まで再帰的に処理)を使って各要素に任意の処理を加えることができる。

<?php
$names = array(
    array('Nick', 38, 'driver'),
    array('John', 25, 'programmer'),
    array('Mike', 22, 'student')
);
function setUpper(&$value, &$key){
    $value = strtoupper($value);
}
array_walk_recursive($names, 'setUpper');
var_dump($names);
/*
array(3) {
  [0]=>
  &array(3) {
    [0]=>
    string(4) "NICK"
    [1]=>
    string(2) "38"
    [2]=>
    string(6) "DRIVER"
  }
  [1]=>
  &array(3) {
    [0]=>
    string(4) "JOHN"
    [1]=>
    string(2) "25"
    [2]=>
    string(10) "PROGRAMMER"
  }
  [2]=>
  &array(3) {
    [0]=>
    string(4) "MIKE"
    [1]=>
    string(2) "22"
    [2]=>
    string(7) "STUDENT"
  }
}
*/
?> 

但し、個人的には関数名を文字列で渡す行為が嫌いなのもあり、イマイチこの方法は使わず単純に(再帰処理する関数を使って)ループさせることが多い。しかも、戻り値としては処理を加えた配列を返さない為、リファレンスを使う必要があり、コードも見づらい。このような点はJavaScriptの方が好きなところだ。

array_combine関数(おまけ)

以下のように、一方の配列をキー、もう一方の配列を値として、新しい配列を生成する。要素数が一致しない場合、エラーが起こる。

<?php
$a = array('php', 'js');
$b = array(
    array('print()', 'array()'),
    array('document.write()', 'new Array()')
);
$c = array_combine($a, $b);
var_dump($c);
/*
array(2) {
  ["php"]=>
  array(2) {
    [0]=>
    string(7) "print()"
    [1]=>
    string(7) "array()"
  }
  ["js"]=>
  array(2) {
    [0]=>
    string(16) "document.write()"
    [1]=>
    string(11) "new Array()"
  }
}
*/
?>

個人的には、この関数の有用性が分からない。PHPは関数が豊富なのが魅力的の一つだが、無駄な関数もあるのではとか思ってしまうような1つである。

各関数について
bool array_walk(array &$array, string $callbackname[, mixed $user_data])
配列の全ての要素にユーザ関数を適用する。ユーザ関数は第一引数に配列の値をとり、第二引数に配列のキーといった形式になる。
bool array_walk_recursive(array &$array, string $callbackname[, mixed $user_data])
配列の全要素にユーザ関数を適用する(配列の)。ユーザ関数は第一引数に配列の値、第二引数に配列のキーといった形式になる。
array array_combine(array $keys, array $values)
一方の配列をキーとし他方を値として配列を生成する。

PHP Array Operations(配列における演算子)

多くの演算子は、配列かどうかで異なった振る舞いをする。

<?php
$a = array(1, 2, 3);
$b = array('a' => 1, 'b' => 2, 'c' => 3);
var_dump($a + $b);
/*
array(6) {
  [0]=>
  int(1)
  [1]=>
  int(2)
  [2]=>
  int(3)
  ["a"]=>
  int(1)
  ["b"]=>
  int(2)
  ["c"]=>
  int(3)
}
*/
?>

但し、配列のキーに重複が存在する場合は結果が異なる。

<?php
$a = array(1, 2, 3);
$b = array('a' => 1, 2, 3);
var_dump($a + $b);
/*
array(4) {
  [0]=>
  int(1)
  [1]=>
  int(2)
  [2]=>
  int(3)
  ["a"]=>
  int(1)
}
*/
?>

■配列の比較

配列は以下のように、演算子(==、===)を使って比較することができる。

<?php
$a = array(1, 2, 3);
$b = array(0 => 1, 1 => 2, 2 => 3);
$c = array('a' => 1, 'b' => 2, 'c' => 3);
$d = array(2 => 3, 0 => 1, 1 => 2);
var_dump($a == $b);//true
var_dump($a === $b);//true
var_dump($a === $d);//false
var_dump($a == $c);//false
var_dump($a === $c);//false
?>

基本的には、キーと値のペアが同じように存在していればtrueを返す。注目するべきなのは、$bと$dの違いである。キーと値のペアは同様に存在しているが、順序が異なっているとPHP内部では異なった配列として扱われている。その結果、2番目と3番目の表示が異なっている。

■配列の要素

要素数

以下の例が示すとおり(JavaScriptのlengthと同様)count関数は、スカラー値が引数に指定された時に常に1を返すため、配列かどうかの判定には使えない。

<?php
$tmAry = array(1, 4, 16);
print(count($tmpAry));//3
$tmpStr = 'sample';
print(count($tmpStr));//1
?>

配列かどうかの判定をするには、以下のようにis_array関数を使う。

<?php
if(is_array($tmpAry)){
    //code
}
?>

任意の要素が存在するかテストする

以下のようにin_array関数を使う。またデフォルトでは型のチェックは行われないが、3番目の引数にtrueをセットすることにより、型のチェックまで行うことができる。

<?php
$tmpAry = array(1, 4, 16);
if(in_array(4, $tmpAry)){//true
    //code
}
?>

要素の削除

要素を削除するには、以下のようにunset関数を使用する。

<?php
$tmpAry = array(1, 4, 16);
unset($tmpAry[1]);
if(in_array(4, $tmpAry)){//false
    //code
}
?>

ランダムに要素を取得する

array_rand関数を使用すると、第二引数で指定した数だけランダムに配列から要素を取り出す。第二引数のデフォルトは1である。また、第二引数が2以上の場合だけ、戻り値も配列となる。

<?php
$tmpAry = array('a' => 'sample', 'b' => 'test', 'c' => 'prototype');
$value = array_rand($tmpAry);//string(1) "b"
$values = array_rand($tmpAry, 2);
/*
array(2) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
}
*/
?>
各関数について
bool in_array(mixed $needle, array $haystack[, bool $strict=false])
$needleが配列$haystackに存在するかを返す。第三引数で「true」を指定すると型の整合性まで調べる。
mixed array_rand(array $input[, int $num_request=1])
ランダムに配列のキーを選び、その値を返す。第二引数に2以上の値を設定した場合は、配列のキーの配列が返る。
void unset(mixed $var[, …])
指定した変数の割り当てを解除する。但し、実際には関数ではなく言語構造である。

■配列のキーを取得

以下のようにarray_keys関数を使用すると、配列のキーが配列として返される。

<?php
$tmpAry = array('a' => 'sample', 'b' => 'test', 'c' => 'prototype');
$keys = array_keys($tmpAry);
var_dump($keys);
/*
array(3) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [2]=>
  string(1) "c"
}
*/
?>

各関数について

array array_keys(array $input)
配列のキーを全て返す。

■配列の反転

キーと値の反転

<?php
$tmpAry = array('a' => 'sample', 'b' => 'test', 'c' => 'prototype');
var_dump(array_flip($tmpAry));
/*
array(3) {
  ["sample"]=>
  string(1) "a"
  ["test"]=>
  string(1) "b"
  ["prototype"]=>
  string(1) "c"
}
*/
?>

順序の反転

以下のようにarray_reverse関数は順序を逆にした配列を返す。但し、キーが文字列のペアの関係性は保持され、キーが数値の要素のみ順序が逆になる。また以下の例を見ての通り、array()で指定した順序と逆の結果が出力される。

<?php
$tmpAry = array(0 => 'sample', 1 => 'test', 'c' => 'prototype');
var_dump(array_reverse($tmpAry));
/*
array(3) {
  ["c"]=>
  string(9) "prototype"
  [0]=>
  string(4) "test"
  [1]=>
  string(6) "sample"
}
*/
?>

個人的には、この手の関数は殆ど使用した事はない。

各関数について
array array_flip(array $ary)
配列$aryのキーが値に、値がキーになった配列を返す。但し、キーに重複が出た場合は最後の値で上書きされる。また、値にオブジェクトなどがあった場合は、警告を発し処理が飛ばされる。
array array_reverse(array $ary[, bool $preserve_keys=false])
要素を逆順にした配列を返す。第二引数が「true」の場合は、キーが保持される。

PHP Array Basics(配列の基本)

■配列の種類

インデックス配列

キーが数字の、いわゆる普通の配列。

<?php
$ary = array(10, 20, 30);
?>

連想配列

キーが文字列。特に連想配列の場合は以下のように記述して縦の列を揃えると、コードが見やすくなる。

<?php
$ary = array(
    'a' => 'John',
    'b' => 'Nick',
    'c' => 'Michel'
);
?>

■配列の生成方法

関数を使う

<?php
$ary = array(1, 3, 7);
?>

[]を使う

存在しない配列にデータを代入しようとすると、その時点で配列が生成される。その原理を利用する方法。

<?php
$ary[0] = 1;
$ary[1] = 3;
$ary[2] = 7;
?>
各関数について
array array([mixed $var, …])
配列を生成する。但し、実際には関数ではなく言語構造である。

■配列の表示

デバッグなどを目的として配列の構造と要素を表示したい時、以下の2つの関数は各要素を再帰的に表示する。

print_r()

第2引数にtrueを設定する事により戻り値を発生させ、変数に値を格納させる事ができる。また配列のポインタは末端に移動する。

<?php
$ary = array(
    1 => 'sample',
    'a' => array(1,5,9),
    2 => 5
);
print_r($ary);
/*
Array
(
    [1] => sample
    [a] => Array
        (
            [0] => 1
            [1] => 5
            [2] => 9
        )

    [2] => 5
)
*/
?>

var_dump()

引数に1つ以上の変数をとり、複数の変数を表示させる事ができる。より詳細なデータが見れるので、デバッグ目的ではコチラの関数を個人的には多用している。

<?php
$ary = array(
    1 => 'sample',
    'a' => array(1,5,9),
    2 => 5
);
var_dump($ary);
/*
array(3) {
  [1]=>
  string(6) "sample"
  ["a"]=>
  array(3) {
    [0]=>
    int(1)
    [1]=>
    int(5)
    [2]=>
    int(9)
  }
  [2]=>
  int(5)
}
*/
?>
各関数について
mixed print_r(mixed $expression[, bool $return=false])
指定した変数の詳細を型に合わせて表示する。但し、第二引数に「true」を設定すると値を返す。
void var_dump(mixed $expression[, mixed $expression])
指定した変数の詳細を型に合わせて表示する。複数の変巣を一度に表示することができる。

■配列末尾への値の追加

以下のコードは、0から始まる数値のうち、最初に見つかった空きインデックスに要素を追加する方法。連想配列に対しても使用が可能であり、0から数値が割り当てられていくが、通常ではそのような使用はしない。

<?php
$person[0] = 'Mike';
$person[] = 'Nick';//1が割り当てられる
?>

但し、0から始まる数値のうち任意の正の数字インデックスが使用されている場合は、以下のようにそのインデックスの次の数字から値が格納されていく。

<?php
$tmpAry = array();
$tmpAry[10] = '1';
$tmpAry[] = 'test';//11が割り当てられる
var_dump($tmpAry);
/*
array(2) {
  [10]=>
  string(1) "1"
  [11]=>
  string(4) "test"
}
*/
?>

■Multi-dimentional Arrays(多次元配列)

<?php
$ary = array();
$ary[] = array(
    'Nick',
    'John'
);
$ary[] = array(
    'Emily',
    'Naomi'
);
print("Hello, {$ary[0][1]} and {$ary[1][1]}");
?>

活用例

$tmpAry = array();
foreach($ary as $key => $value){
    if(!is_array($tmpAry[$key])){
        $tmpAry[$key] = array();
    }
    $tmpAry[$key]['id'] = $value['id'];
    $tmpAry[$key]['value'] = $value['value'];
}

よりも以下のコードの方が美しいと思う。

$tmpAry = array();
foreach($ary as $key => $value){
    $tmpAry[] = array(
        'id' => $value['id'],
        'value' => $value['value'],
    );
}

■配列の展開

listは実際には言語の構成要素であり戻り値を発生させない。また左辺のlistの引数が多い場合、余った変数にnullが代入される。右辺の配列の要素数が多い場合、特に問題は発生しない。

<?php
$peple = array('John', 'Mike', 'Nick');
list($person1, $person2, $person3) = $people;
?>

以下のような場合に有用かもしれない。

<?php
$sql = "SELECT `name`, `age`, `job` FROM `data`";
$result = mysql_query($sql);
while(list($name, $age, $job) = $mysql_fetch_row($result)){
    echo "{$name}({$age}) - {$job}";
}
?>

但し、個人的には以下のようにループを使って処理するタイプなのでlistを使うことは少ない。

<?php
$sql = "SELECT `name`, `age`, `job` FROM `data`";
$result = mysql_query($sql);
while($row = mysql_fetch_array($result)){
    echo "{$row['name']}({$row['age']}) - {$row['job']}";
}
?>

各関数について

void list(mixed $var[, …])
複数の変数に配列の値を格納する。実際には関数ではなく言語構造である。