01. スネークケースとキャメルケースを変換する
user_id と userId を行き来させます。データベースの列名と JSON のキー、定数と変数のように、名前の書き方をそろえるときに使います。HTTPServer のように大文字が続く名前で、よく見かける書き方はほとんどの言語で崩れます。 ほかの言語・正解の例・解説 →
case_convert.php8.4.26 で 14 / 14 件の出力が正解と一致
<?php
// スネークケース(user_id)とキャメルケース(userId)を、指定した向きに変換する
const NG = "NG: 半角の英数字と _ だけにしてください";
[, $mode, $s] = $argv;
// preg の $ は末尾の改行の手前にも合う。D を付けて、文字列の本当の終わりだけに合わせる
if (!preg_match('/\A[A-Za-z0-9_]*\z/D', $s)) {
echo NG . "\n";
} elseif ($mode === "snake") {
// 切れ目は2種類。小文字か数字の次の大文字(userId)と、大文字の続きの最後で次が小文字のもの(HTTP|Server)
$s = preg_replace('/(?<=[a-z0-9])(?=[A-Z])/', '_', $s);
$s = preg_replace('/(?<=[A-Z])(?=[A-Z][a-z])/', '_', $s);
// strtolower は PHP 8.2 から ASCII だけを変える(ロケールに左右されない)。ここは ASCII だけなので mbstring は要らない
echo strtolower($s) . "\n";
} else {
// explode は「__」のところで空の部分を作る。array_filter の既定は "0" も捨ててしまうので、空文字だけを捨てる
$parts = array_values(array_filter(explode("_", $s), fn($p) => $p !== ""));
$out = "";
foreach ($parts as $i => $p) {
// ucfirst は先頭しか変えない。残りを先に小文字にする
$out .= $i === 0 ? strtolower($p) : ucfirst(strtolower($p));
}
echo $out . "\n";
}
02. HTML の特殊文字をエスケープする
利用者が入力した文字を HTML に埋め込む前に、< > & " ' を < などに置き換えます。標準の関数は言語ごとに ' の書き方が違い、この決まりの形とそのまま一致したのは Ruby だけでした。 ほかの言語・正解の例・解説 →
escape.php8.4.26 で 8 / 8 件の出力が正解と一致
<?php
// 受け取った文字列の & < > " ' を、HTML の書き方(& < > " ')に変える
$s = $argv[1];
// htmlspecialchars は ' を ' と書く(この回の ' と違う)ので、自分で置き換える。
// strtr に配列を渡すと、元の文字列を1回だけ見て置き換えるので、作った < の & がもう一度変わることがない
echo strtr($s, [
'&' => '&',
'<' => '<',
'>' => '>',
'"' => '"',
"'" => ''',
]) . "\n";
03. 順番を保って重複を消す
同じ値の2回目以降を消し、初めて出た順に残します。タグの一覧や、選んだ項目の一覧でよく使います。集合(Set)に入れて戻す書き方では、言語によって順番が変わったり、実行するたびに変わったりします。 ほかの言語・正解の例・解説 →
unique.php8.4.26 で 9 / 9 件の出力が正解と一致
<?php
// 受け取った文字列から、2回目以降に出たものを消し、初めて出た順に出す
$items = array_slice($argv, 1);
// array_unique は初めて出たものを残し、既定(SORT_STRING)では文字列として比べるので "1" と "01" は別になる。
// in_array($x, $out) を第3引数なしで使うと「==」で比べ、"1" と "01" と "1.0" が同じ数とみなされる
$out = array_unique($items);
foreach ($out as $s) {
echo ($s === '' ? '(空)' : $s) . "\n";
}
// 消した分の添字が飛ぶので、数は count で数える
echo count($out) . '件(' . (count($items) - count($out)) . "件を消した)\n";
04. 日付と時刻を ISO 8601 で読み書きする
API やログでよく使う 2026-10-30T09:00:00+09:00 の形を読み、UTC と日本時間で書き直します。標準のパーサーにそのまま渡すと、2月30日を3月2日として読んだり、タイムゾーンの無い日時を動かす場所の時間帯で読んだりします。 ほかの言語・正解の例・解説 →
iso8601.php8.4.26 で 15 / 15 件の出力が正解と一致
<?php
// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
// new DateTime() は「2026-02-30」を3月2日に、「+0900」も「t」も受け付けるので、形は先に正規表現で決める。
// $ は末尾の改行も通すので、文字列の終わりは \z で書く
$re = '/\A([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})?\z/';
if (!preg_match($re, $argv[1], $m)) {
echo "NG: 日時の形ではありません\n";
exit;
}
// 2月30日・25時は DateTime が黙って繰り上げるので、checkdate と範囲で確かめる
if (!checkdate((int)$m[2], (int)$m[3], (int)$m[1]) || $m[4] > 23 || $m[5] > 59 || $m[6] > 59) {
echo "NG: 日時の形ではありません\n";
exit;
}
// タイムゾーンが無いと、php.ini の date.timezone(サーバーの設定)で読まれてしまう
if (($m[8] ?? "") === "") {
echo "NG: タイムゾーン(Z か +09:00 など)を付けてください\n";
exit;
}
if ($m[8] !== "Z" && (substr($m[8], 1, 2) > 23 || substr($m[8], 4, 2) > 59)) {
echo "NG: 日時の形ではありません\n";
exit;
}
// "+09:00" のような時差だけのタイムゾーンは tzdata を見ないので、どこで動かしても同じになる。
// 秒の小数は切り捨てるので、読むときから渡さない
$tz = new DateTimeZone($m[8] === "Z" ? "+00:00" : $m[8]);
$t = new DateTimeImmutable("$m[1]-$m[2]-$m[3] $m[4]:$m[5]:$m[6]", $tz);
$utc = $t->setTimezone(new DateTimeZone("+00:00"));
$jst = $t->setTimezone(new DateTimeZone("+09:00"));
echo "UTC " . $utc->format("Y-m-d\TH:i:s\Z") . "\n";
echo "日本時間 " . $jst->format("Y年m月d日 H:i:s") . "\n";
05. アクセスログを集計する
Web サーバーのアクセスログを標準入力から読み、ステータスごとの件数と、応答の遅いリクエストの上位3件を出します。空白で区切って何番目かを取る書き方は手軽ですが、壊れた行を数えたり、応答秒を文字のまま並べたりします。 ほかの言語・正解の例・解説 →
access_log.php8.4.26 で 8 / 8 件の出力が正解と一致
<?php
// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
// 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる。
// $ は「最後の改行の手前」にも合うので、行の終わりは \z で書く
$log = '/\A\S+ \S+ \S+ \[[^\]]+\] "(\S+) (\S+) [^"]+" ([0-9]{3}) ([0-9]+|-) ([0-9]+\.[0-9]+)\z/';
$rows = [];
$bad = 0;
// \r\n と \n のどちらでも区切る。最後に改行が無くても、最後の行はそのまま残る
foreach (preg_split('/\r?\n/', stream_get_contents(STDIN)) as $line) {
if ($line === '') {
continue;
}
if (!preg_match($log, $line, $m)) {
$bad++;
continue;
}
$rows[] = ['sec' => $m[5], 'method' => $m[1], 'path' => $m[2], 'status' => $m[3]];
}
echo "件数 " . count($rows) . "\n";
if ($bad > 0) {
echo "読めない行 " . $bad . "\n";
}
$counts = [];
foreach ($rows as $r) {
// "200" のような数字だけの文字列をキーにすると、PHP は整数の 200 に変えてしまう。並べる・出すだけなら困らない
$counts[$r['status']] = ($counts[$r['status']] ?? 0) + 1;
}
ksort($counts);
echo "ステータス\n";
foreach ($counts as $status => $n) {
echo $status . " " . $n . "\n";
}
echo "遅い順\n";
// 応答秒は (float) にして比べる。PHP 8.0 から usort は安定なので、同じ秒なら先に出た行が先に残る
usort($rows, fn($a, $b) => (float) $b['sec'] <=> (float) $a['sec']);
foreach (array_slice($rows, 0, 3) as $r) {
echo $r['sec'] . " " . $r['method'] . " " . $r['path'] . "\n";
}
06. URL 用の文字列(slug)を作る
記事のタイトルから「hello-world」のような URL の一部を作ります。ブログや商品ページのアドレスを、見て意味が分かる形にするときに使います。よく見かける書き方は、日本語のタイトルをそのまま URL に入れたり、空の slug を黙って返したりします。 ほかの言語・正解の例・解説 →
slug.php8.4.26 で 14 / 14 件の出力が正解と一致
<?php
// 文字列を URL に使える形(slug)に変える
const MAX = 32;
const NG = "NG: URL に使える文字がありません";
$s = $argv[1];
// strtolower は PHP 8.2 から ASCII の A〜Z だけを変える(İ のような文字はそのまま。mbstring は要らない)
$s = strtolower($s);
// u を付けないので1バイトずつ見るが、[^a-z0-9]+ は多バイトの文字のバイトもまとめて食うので「-」1つになる
$s = trim(preg_replace('/[^a-z0-9]+/', '-', $s), '-');
if (strlen($s) > MAX) {
// ここまでで半角の英数字と「-」だけになっているので、バイト数=文字数
$s = rtrim(substr($s, 0, MAX), '-');
}
echo ($s === "" ? NG : $s) . "\n";
07. URL エンコードする
検索語や日本語の名前を、URL のクエリの値(`?q=...`)に入れられる形に変えます。よく見かける書き方は、空白を「+」にしたり、`!` や `~` を変えずに残したりして、受け取る側で別の文字列になります。 ほかの言語・正解の例・解説 →
url_encode.php8.4.26 で 15 / 15 件の出力が正解と一致
<?php // 文字列を URL のクエリの値に入れられる形(パーセントエンコード)に変える // rawurlencode は RFC 3986 のとおり(残すのは英数字と - _ . ~ だけ・空白は %20・16進数は大文字) // urlencode は空白を「+」にして「~」も %7E にする(名前が似ているので取り違えやすい) echo rawurlencode($argv[1]) . "\n";