PHPの雑多なメモ
Built-in Serverの起動
PHP処理系には簡易なWebサーバがついていて、開発検証用途などに便利に使えます。
コマンドラインから次のように起動するホスト名とポート番号、HTML文書ルートディレクトリを指定できます。

php -S localhost:8080 -t /var/www/phpdev/docroot

PHPと日本語文字コード
「mbstring」というモジュールがあり、 日本語などのマルチバイト文字のエンコーディング変換がサポートされています。例えば、

$b = mb_convert_encoding($a, "EUC-JP", "SJIS");
とすると、シフトJISで書かれた文字列$aを日本語EUCに変換して、$bに代入します。 3番目の引数は省略できて、その場合はPHPのシステムエンコーディングから、 2番目の引数で指定したエンコーディングへの変換となります。 これを使うと、シフトJISで書かれたテキストファイルを読んで、 日本語EUCで書かれたPHPスクリプトによってWebブラウザに表示する、といったことが可能になります。

先月の末日は何日?
PHPで業務システムを作っていて結構あるのが、 先月の初日から末日までのデータをファイルやデータベースから抽出する、 という処理です。それで、末日の日付は何日だっけ? という話なんです。OracleだとLAST_DAYという関数があるので、 現在の日時(SYSDATE)が属する月の末日を簡単に分かるのですが、 そうでない場合とか、あるいは、

集計期間(YYYYMMDD): 〜

このような↑入力フォームを表示したときのデフォルト期間として、 先月1日〜先月末日をセットしたい、という機会もあるでしょう。 その場合の簡単なコードです:

<?php
/* 先月の初日〜末日を求め、表示します。*/
$now = time();
list($h, $mi, $s, $d, $mo, $y) = localtime($now);
$y += 1900; $mo++;
print "$y $mo $d $h $mi $s\n";

/* 今月1日 */
$tsuitachi = mktime(0, 0, 0, $mo, 1, $y);
/* その1日前=先月末 */
$sengetsumatsu = $tsuitachi - 24 * 60 * 60;
list($h, $mi, $s, $d, $mo, $y) = localtime($sengetsumatsu);
$y += 1900; $mo++;

printf("%4d%02d%02d - %4d%02d%02d\n", $y, $mo, 1, $y, $mo, $d);
?>

Perl互換の正規表現
PHPには幾つかの系統の正規表現の関数群がありますが、Perl互換の「PCRE」という関数群が便利です。 そのpreg_matchの使い方は

$v = "1234.56";
if(preg_match("/^[1-9\.]+$/", $v)) print "マッチします。";
else print "マッチしません。";

こんな感じです。 PHPではPerlと異なり、正規表現をスラッシュ/で囲んだうえで(※1)、さらに引用符でも囲む必要があります。 その代わり、正規表現内部の\マークはエスケープさせる必要はありません。
※1 正規表現の中に/が含まれる場合、エスケープする必要があり見にくくなります。 そのような場合、/以外の文字を使うこともできます。 例えばパイプ(縦棒)文字|を使って、「|^[1-9\.]+$|」 のようにすることもできます。#なども良く使われます。 また変り種として、中括弧 { } で囲んで表現することも可能です。

また、括弧で囲んだサブパターンを取得することもできます。

if(preg_match("/(.+)\.(.+)/", basename($path), $subs)){
  $all  = $subs[0]; $main = $subs[1]; $ext  = $subs[2];
}

このように、正規表現中に括弧を指定すると、 それら各々に適合する部分文字列の配列が3番目の引数に格納されます。 (3番目の引数で指定する変数は、初期化してある必要はありません) [0]は適合した文字列全体、[1]が1番目の括弧に適合した部分文字列、 [2]が2番目の括弧に適合した部分文字列…となります。

文字列が全角文字を含んでいるかどうかを知りたい
厳密ではありませんが、ASCII文字だけからなるか、2バイト文字コードを含んでいるかの判別なら次のように可能です。

if(preg_match("/[\\x80-\\xFF]/", $english_name)){
  die("おおっと! 英語名には半角文字のみを使用してください。");
}

ファイルパスからのディレクトリとファイル名の切り出し
ファイルパスからディレクトリだけを切り出すには dirname、ファイル名だけを切り出すには basenameという関数があります。

文字列の検索
C言語のstrstrのように、ある文字列の中に別の文字列が含まれるかどうかを調べるには、 名前も同じstrstrという関数があります。(strchrという関数もありますが、これはstrstrの別名です)

$result = strstr($target, $pattern);

この例では、$targetの中に$patternを含めばその最初に出会った位置以降の文字列を、 含まなければFALSEを返します。

文字列AがBで始まるか調べる
PHP 8.0からその名の通りのstr_start_withという関数が使えるようになりました。str_ends_withもあります。
それ以前は、strposという関数で次のようにしていました。

if(strpos($target, $prefix) === 0){
    print "targetはprefixで始まります。";
}

「strpos(A, B)」とすると、文字列Aの中にBが部分文字列として現れるとき、 その始まる位置(先頭が0)を返し、見つからなければboolean FALSEを返します。
で、問題は戻り値が0 (AはBで始まる)とFALSE(Aの中にBは見つからない) の区別が難しいことです。上記のように、PHP4から導入された「===」 (値が等しく、データ型も等しい)演算子を使って、「=== 0」「=== FALSE」 とすればどちらを意図しているのかの可読性も保たれます。

配列変数に特定のキーが存在するかを調べる

if(array_key_exists($key, $array)){ ... }

fopenなどの関数の頭に@をつけるのは何?
インターネット上の個人サイトのサンプルプログラムなどを拝見していると、 fopenなどの関数の頭に@をつけている例があります。これは、 「エラー制御演算子」 というPHPの演算子で、これをつけた式が返すエラーメッセージの表示が抑制されます。
例えば運用では@をつけておき、問題が発生したらテスト環境では@を外してデバッグする、 などの使い方が考えられます。

数値をカンマ区切りにする
number_formatという関数があり、これで「3456789」⇒「3,456,789」 のように変換してくれます。とても便利。

配列から変数に分割して代入する
PHPには1つの配列の各要素を複数の変数に一斉に代入する便利な文があります。

list($a, $b, $c) = $arr;

このように書くと、$arr[0]が$a、$arr[1]が$b、$arr[2]が$cに代入されます。

今日の日付を文字列で求める
最も簡単にはdate関数を使います。

$today = date("Y/m/d");

mとdはいずれも左0詰で必ず2桁になります。 左0詰を行わない月と日を得るには、mとdの代わりにそれぞれnとjを使います。

3ヶ月後の日付を文字列で求める
strtotime関数が便利です。

$max_date = date("Y/m/d", strtotime("+3 month"));

"+3 month"の部分は他にもdayなど柔軟な指定が可能です。詳しくはマニュアルで。

2つの日付の間の日数を求める
日付からtime_t秒数を得るには、mktime関数を使いますが、 C言語やPerlなどの慣例とは引数の並び順が違っていて、時分秒月日年の並びなので要注意。

<?php
$ds1 = $argv[1]; $ds2 = $argv[2];
if(! preg_match("/^(\d\d\d\d)(\d\d)(\d\d)/", $ds1, $da1)){
    die("date1 $date1 unrecognized, expected YYYYMMDD.\n");
}
if(! preg_match("/^(\d\d\d\d)(\d\d)(\d\d)/", $ds2, $da2)){
    die("date2 $date2 unrecognized, expected YYYYMMDD.\n");
}
$date1 = mktime(0, 0, 0, $da1[2], $da1[3], $da1[1]);
$date2 = mktime(0, 0, 0, $da2[2], $da2[3], $da2[1]);
$dates = abs($date1-$date2) / (24*60*60);
print "dates=$dates\n";
?>

チェックボックス(checkbox)タグで配列パラメータをPHPに渡す
フォームでチェックボックス(checkbox)を使って、 複数の可変数の選択肢を同時に選択したいときがあります。 例えば、DBのレコードを一覧表示して、選択した任意数のレコードを削除したいときー。 この場合は、そのnameの値を「delkey」のような普通の名前ではなく、「delkey[]」 のように角括弧をつけます。
すると、PHPの世界では「$_POST["delkey"]」で、配列データとして取得できます。

<?php foreach($rset as $e){ ?>
<tr>
  <td><input type="checkbox" name="delkey[]" value="<?= $e[1] ?>"></td>
  <td><?= $e[0] ?></td>
  <td><?= $e[1] ?></td>
  <td><?= $e[2] ?></td>
</tr>
<?php } ?>

ここで、角括弧をつけない名前で同じようにしても、最後にチェックされたレコードの valueの値しかPHP世界には渡されないので、要注意です。

配列の各要素を(キー,値)の組ごとに処理する際の慣用記法

foreach($_SESSION as $key => $value){
    print "[" . $key . "] = [" . $value . "]<br/>\n";
}

このようにforeachを使うのが定番です。 ちなみに昔は次のようにeachという関数がありましたが、廃止されました。

while(list($key, $value) = each($_SESSION)){
    print "[" . $key . "] = [" . $value . "]<br/>\n";
}

PHPとAjaxと日本語のPOSTパラメータ
JavaScriptから次のように送られてくる日本語データがあるとします。

xmlhttp.open('POST', url, false); // 同期通信
xmlhttp.setRequestHeader("content-type",
  "application/x-www-form-urlencoded;charset=UTF-8");
xmlhttp.send(encodeURI(postdata));

このUTF-8の日本語をシフトJIS環境のPHPで受け取るには?
php.iniのmbstringセクションは次のように設定

[mbstring]
mbstring.language = Japanese
mbstring.internal_encoding = SJIS
mbstring.http_input = auto
mbstring.http_output = SJIS
; 残りはコメント

PHPスクリプトはシフトJISコードで、以下のように変換すればokです:

$indata = $_POST['indata'];
$indata = mb_convert_encoding($indata, 'Shift_JIS', "UTF-8");

(Windows)コマンドプロンプトで実行できるコマンドが、system()で実行できない
正確にはよく分かりませんが、 system()関数で実行する際のPATHは、「システム環境変数」だけが有効で、 「ユーザ環境変数」の定義は参照されないようです。
したがって、ユーザ環境変数で通しているパスのプログラムは、 コマンドプロンプトでは実行できますが、PHPのsystem()では実行できない、 という現象が起きるようです。

文字列リテラルの中身を変えただけで、画面が出たり真っ白になったりする
シフトJISで書いているとき、「表」など2バイト目がエスケープ文字と衝突する漢字を文字列リテラルに含めると、 正しく処理されず画面が真っ白になってしまうことがありました。
そのときの解決策?「表」という漢字を使わずに、別の意味の言葉に置き換えましたとさ…
なお、PHP 7以降はコードをSJISで書いているとその他の問題も色々起きてくるため、 今は素直にUTF-8で書くのが良いと思います…

他にも画面が真っ白になる場合があるぎゅう
画面が真っ白になってしまうのはほとんどの場合PHPの文法エラーぎゅう。 典型的なミスとしては、

  • ヒアドキュメントの開始が<<になっている(<が1つ足りない)
  • 括弧の開閉の対応がおかしい
  • 配列を初期化するとき array() と間違えて () と書いている(Perlと混同)
があるので、冷静にコードを確認しましょう。

Warning: Cannot modify header information - headers already sent by...
メッセージ通りの意味としては、header()関数を使う前に、 HTTP応答の一部が既に送信されてしまっているので、ヘッダ情報を変えられませんということで、 例えば、

  • header()を使う前に<html>などのHTML要素を記述している
  • header()を使う前にrequireなどで取り込んでいる他のスクリプトが、エラー出力を行っている
  • PHPスクリプトの先頭に空の行がある(極端な例)
などの原因が考えられます。
しかし、上記は全部完璧な場合でもこのエラーが出てしまうことがあります。 それはphp.iniで 「output_buffering = Off」 になっている場合です。ここを 「output_buffering = On」に変更すると解決すると思います。

インクルードファイルの末尾に?>は書くなと言われるのはなぜ?
非常に不恰好なのに…と、言いたいことはわかります。 書くなと言われる最大の理由は、 書いてしまうと不要な改行文字が1個挿入され、 それが元で1つ上にあるような 「header()関数を使う前に、HTTP応答の一部が既に送信されてしまっている」 のような気づきにくい問題を起こす可能性があるためです。
なるほど納得、しかしそれにしても、不恰好なのに… (←納得してないじゃないかという矯激なご指摘は、全て無効とさせていただきます)

ファイルシステムのディレクトリを再帰的に検索するサンプル
PHPでこの手の処理を使うことは稀かもしれませんが、一応標準の仕組みが用意されています。 以下の例は指定したディレクトリ以下を再帰的に検索し、 ディレクトリ以外のファイルをサイズと共に表示する処理です。

$baseDir = new \RecursiveDirectoryIterator("C:/the/path/of/targetdir");
$filter = new \RecursiveCallbackFilterIterator($baseDir, function ($current, $key, $iterator) {
    // ここでファイルcurrentを対象外にしたければFALSEを返すようにする
    return TRUE;
});
$iter = new \RecursiveIteratorIterator($filter);
foreach($iter as $f){
    if(! is_file($f)){ continue; }
    $path = $f->getPathname();
    $size = $f->getSize();
    print("${path}<br>${size}<br>\n");
}

一点注意があります。$filter変数に渡している無名関数で、 ディレクトリを抽出対象外にしようとして、ディレクトリに対してFALSEを返すようにすると、 確かにディレクトリは抽出対象外になりますが、そのディレクトリの下のサブディレクトリを検索に行かなくなってしまいます。 正しくは上のようにfilterには該当させたうえで、 一覧を処理するforeachループの方でディレクトリを除外するようにします。

「このページは動作していません」と表示されるページのエラー原因を調べるには?
HTTP 500エラーが発生した時、php.iniの設定で display_errors がOffになっていると、 Webブラウザ画面には「このページは動作していません」とだけ表示され、エラーの原因は一切分かりません。
正式運用環境ではセキュリティのためこの状態の方がいいのですが、 デバッグのために特定のページだけ一時的にエラー表示を有効にするには、PHPスクリプト冒頭に次のように追記します。

ini_set("display_errors", 'On');
error_reporting(E_ALL);

include_pathの設定をPHPプログラム中から動的に変えたい
レンタルサーバーなどでphp.iniを直接編集できない場合、 次のようにして include_path を動的に変更できます。

$path = '/home/myuser/php/packages';
set_include_path(get_include_path() . PATH_SEPARATOR . $path);

Open Source Web Architecture Top

(first uploaded 2002/07/01 last updated 2025/09/23, URANO398)