Skip to content

PHP チートシート

Web 開発のための人気のある汎用スクリプト言語。

01

基礎

変数、型と定数

PHP の変数は $ で始まり、動的型付けされます。PHP 7.4+ は型付きプロパティをサポートします。実行時定数には define() を、コンパイル時定数には const を使用します(より高速)。PHP 8.1 は enum を導入しました — クラス定数より優れた型付き列挙システムです。可能な限り型を宣言してください(PHP 7+)— 早期エラー検出とより良い IDE サポートのため。

php
<?php
$name = "Alice";          // string
$age = 30;                // integer
$price = 19.99;           // float (double)
$active = true;           // boolean
$items = [1, 2, 3];       // array
$null = null;             // null

// constants
define("MAX_USERS", 100); // runtime constant
const PI = 3.14159;       // compile-time constant
echo MAX_USERS;            // no $ prefix

// PHP 8.1+ enums
enum Status: string {
    case Active = 'active';
    case Inactive = 'inactive';
}
$s = Status::Active;

Echo、Print とデバッグ

echo は言語構造体です(関数ではない)— 出力に最速です。print は 1 を返すため式で使用できます。printf/sprintf は C スタイルのフォーマット指定子を使用します(%s 文字列、%d 整数、%f 浮動小数点、%x 16進数)。var_dump() は主要なデバッグツールです — 型と値を表示します。error_log() は PHP エラーログまたは syslog に書き込みます。本番環境では、デバッグ出力をユーザーに絶対に見せないでください。

php
<?php
// echo: no return, multiple args (fastest)
echo "Hello", " ", "World";

// print: returns 1, single arg
print "Hello World";

// printf: formatted output
printf("Name: %s, Age: %d, Price: %.2f", "Alice", 30, 19.99);

// debugging output
print_r($array);           // human-readable
var_dump($variable);       // type + value (detailed)
var_export($array, true);  // valid PHP code (for caching)

// sprintf: return formatted string (don't print)
$log = sprintf("[%s] %s", date('H:i:s'), "Started");
error_log($log);           // log to error log

演算子と比較

型強制のバグを避けるため、常に ===(厳密な比較)を使用してください。== は比較前に型を変換し、予期しない結果になります(PHP 7 では 0 == 'abc' が true でした)。宇宙船演算子(<=>)は -1/0/1 を返します — usort に便利です。null 合体演算子(??)はデフォルトを提供する慣用的な方法です。null 安全演算子(?->)(PHP 8+)は null でメソッドチェーンをショートサーキットし、冗長な isset() チェックを置き換えます。

php
<?php
// arithmetic
$sum = 10 + 3;       // 13
$mod = 10 % 3;       // 1
$pow = 2 ** 3;       // 8 (PHP 5.6+)

// comparison: == vs ===
echo (0 == "abc");   // true (loose, PHP 7-); false (PHP 8+)
echo (0 === "abc");  // false (strict — type + value)
echo ("1" == 1);     // true (loose)
echo ("1" === 1);    // false (strict)

// spaceship operator (PHP 7+)
echo 1 <=> 2;        // -1 (less than)
echo 2 <=> 2;        // 0 (equal)
echo 3 <=> 2;        // 1 (greater than)

// null coalescing
$name = $input ?? "default";    // if $input is null
$deep = $data['user']['name'] ?? "Anonymous";

// null safe operator (PHP 8+)
$country = $user?->getAddress()?->country; // null if any step is null

スーパーグローバルと Web

スーパーグローバルはすべてのスコープで利用可能な組み込みの連想配列です。$_GET と $_POST はユーザー入力を含みます — 使用前に必ずサニタイズ/検証してください。filter_input() は直接アクセスより安全です。クライアントが偽装できる $_SERVER の値(HTTP_USER_AGENT など)は絶対に信頼しないでください。header('Location:') の後は常に exit を呼び出してください — そうしないと PHP は実行を続けます。セッションは出力前に session_start() で開始します。

php
<?php
// superglobals available everywhere
$_GET['name'];           // query string params
$_POST['email'];         // form POST data
$_REQUEST['x'];          // GET + POST + COOKIE
$_SERVER['HTTP_HOST'];   // server/env info
$_SERVER['REQUEST_METHOD']; // GET, POST, etc.
$_COOKIE['session'];     // cookies
$_FILES['upload'];       // file uploads
$_SESSION['user_id'];    // session data (after session_start())

// get client IP
$ip = $_SERVER['REMOTE_ADDR'];

// check request method
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
}

// redirect
header("Location: /dashboard");
exit; // always exit after redirect

Include と Require

include/require は指定されたファイルを実行します。ファイルが見つからない場合、require は致命的エラーを引き起こします(重要な依存関係に使用);include は警告のみです(オプションのテンプレートに使用)。_once バリアントは二重インクルードを防ぐためにファイルを追跡します — 関数/クラス定義に不可欠です。Composer のオートローダー(require_once 'vendor/autoload.php')が手動インクルード管理を不要にします。ファイルは値を返できるため、設定に便利です。

php
<?php
// include: warning on failure, continues
include 'header.php';
include_once 'config.php'; // only once

// require: fatal error on failure, stops
require 'database.php';
require_once 'vendor/autoload.php'; // Composer autoload

// _once variants prevent re-declaration errors
// use require for critical files (DB config, autoloaders)
// use include for optional files (templates)

// return values from included files
$config = include 'config.php';
// config.php: return ['db' => 'mysql://...', 'debug' => true];
02

文字列

文字列関数

PHP には 100 以上の文字列関数があります。strpos() は見つからない場合 false を返します — === false で確認してください(0 は有効な位置です)。str_replace() は検索/置換に配列を受け取れます。substr() は負のオフセットをサポートします(末尾から)。マルチバイト文字列(UTF-8)には mb_* 等価関数を使用してください(mb_strlen、mb_substr)— strlen は文字数ではなくバイト数をカウントします。常に default_charset='UTF-8' を設定し、非 ASCII テキストには mb_* 関数を使用してください。

php
<?php
$s = "Hello, World";

// length and case
echo strlen($s);              // 12
echo str_word_count($s);      // 2
echo strtoupper($s);          // HELLO, WORLD
echo strtolower($s);          // hello, world
echo ucfirst("hello");        // Hello
echo ucwords("hello world");  // Hello World

// search and replace
echo strpos($s, "World");     // 7 (false if not found)
echo str_replace("o", "0", $s); // Hell0, W0rld
echo substr($s, 0, 5);        // Hello
echo substr($s, -5);          // World
echo strrev($s);              // dlroW ,olleH

// trim
echo trim("  hi  ");          // "hi"
echo ltrim("  hi");           // "hi"
echo rtrim("hi  ");           // "hi"

文字列補間と Heredoc

二重引用符文字列は変数を補間します;単一引用符文字列はしません(リテラルテキストには単一引用符を使用 — やや高速)。複雑な式(オブジェクトプロパティ、配列アクセス、メソッド呼び出し)には {$var} を使用します。Heredoc(<<<ID)は SQL や HTML のような複数行文字列に最適です — 変数を補間します。Nowdoc(<<<'ID')は非解釈バージョンで、バックスラッシュを含む正規表現パターンに便利です。

php
<?php
$name = "Alice";
$age = 30;

// double quotes: variable interpolation
echo "Hello, $name!";          // Hello, Alice!
echo "Hello, {$name}!";        // Hello, Alice! (braces for clarity)
echo "Age: {$age}";            // Age: 30

// single quotes: no interpolation (faster)
echo 'Hello, $name';           // Hello, $name (literal)

// complex expressions need braces
echo "Result: {$obj->method()}";
echo "Item: {$array['key']}";

// heredoc: multi-line, interpolates
$sql = <<<SQL
    SELECT * FROM users
    WHERE name = '$name'
    AND age > $age
SQL;

// nowdoc: multi-line, NO interpolation (like single quotes)
$regex = <<<'REGEX'
    \d{3}-\d{4}
REGEX;

sprintf とフォーマット

sprintf() はフォーマットされた文字列を安全に構築するために不可欠です — 文字列補間とは異なり、型変換とパディングを処理します。数値フォーマットを保証するため %s ではなく %d を使用してください。number_format() は千位区切りで数値をフォーマットします — 通貨表示に重要です。SQL フラグメントには常に sprintf を使用します(ただしユーザー入力にはプリペアドステートメントが依然として必要です)。

php
<?php
// sprintf: format string (returns, doesn't print)
$formatted = sprintf(
    "%-10s | %5d | %8.2f",
    "Alice", 42, 19.99
);
echo $formatted;  // "Alice      |    42 |    19.99"

// format specifiers:
// %s string, %d int, %f float, %x hex, %b binary, %c char
// %5d  right-padded to width 5
// %-5d left-padded
// %05d zero-padded
// %.2f 2 decimal places
// %8.2f width 8, 2 decimals

// numbered placeholders
echo sprintf("Hi %1$s, bye %1$s", "Alice"); // Hi Alice, bye Alice

// number_format
echo number_format(1234567.891, 2);  // 1,234,567.89
echo number_format(1234567.89, 2, ',', '.'); // European format

正規表現(PCRE)

PHP は /pattern/ 区切り文字付きの PCRE(Perl 互換正規表現)を使用します。preg_match はマッチした場合 1、しない場合 0 を返します(0 は falsy なので == ではなく === を使用)。ユーザー入力は正規表現で検証しますが、それだけに依存しないでください — メール、URL には filter_var() を使用します。preg_replace は強力ですが大きな文字列では遅くなる可能性があります。ブラックリストではなく [^...] で文字をホワイトリストしてください。i フラグはマッチングを大文字小文字無視にします。

php
<?php
// preg_match: test pattern (returns 0 or 1)
if (preg_match('/^[a-z]+$/i', "Hello")) {
    echo "alphabetic";
}

// capture groups
preg_match('/(d{4})-(d{2})-(d{2})/', "2024-01-15", $matches);
// $matches[0] = "2024-01-15" (full match)
// $matches[1] = "2024" (group 1)
// $matches[2] = "01"
// $matches[3] = "15"

// preg_match_all: all matches
preg_match_all('/d+/', "a1 b22 c333", $matches);
// $matches[0] = ["1", "22", "333"]

// preg_replace: substitute
$clean = preg_replace('/[^a-z0-9]/i', '', "Hello, World!"); // HelloWorld

// preg_split: split by pattern
$parts = preg_split('/[s,]+/', "one, two,three  four");
// ["one", "two", "three", "four"]

マルチバイトとエンコーディング

PHP のデフォルトの文字列関数は文字指向ではなくバイト指向です — マルチバイト文字(UTF-8、中国語、絵文字)で壊れます。非 ASCII テキストには常に mb_* 関数(mb_strlen、mb_substr、mb_strpos、mb_strtoupper)を使用してください。アプリケーションの開始時に mb_internal_encoding('UTF-8') を設定します。JSON 出力で中国語/絵文字を読みやすく保つために JSON_UNESCAPED_UNICODE を使用します。これは国際化アプリケーションでよくあるバグの原因です。

php
<?php
// UTF-8 strings: use mb_* functions
$text = "Café";  // 4 chars, 5 bytes (é = 2 bytes)

echo strlen($text);      // 5 (bytes!)
echo mb_strlen($text);   // 4 (characters!)

echo substr($text, 0, 3);    // "Caf" (might break UTF-8!)
echo mb_substr($text, 0, 3); // "Caf" (safe)

// case conversion
echo strtoupper("straße");    // "STRAßE" (wrong!)
echo mb_strtoupper("straße"); // "STRAßE" (correct, locale-aware)

// encoding detection
$encoding = mb_detect_encoding($text);
$utf8 = mb_convert_encoding($text, 'UTF-8', 'auto');

// set internal encoding
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');

// JSON with UTF-8
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
03

配列

インデックス配列と連想配列

PHP の配列は実際には順序付きハッシュマップです — リストと辞書の両方として機能します。インデックス配列は数値キーを自動割り当て;連想配列は文字列キーを使用します。isset() は null 値に対して false を返します;array_key_exists() は null でも true を返します。unset() は要素を削除しますが再インデックスしません。真のリスト(ギャップなし)にするには、削除後に array_values() で再インデックスします。PHP 8.1+ には readonly 配列型があります。

php
<?php
// indexed array (numeric keys)
$nums = [10, 20, 30];
$nums[] = 40;              // append
echo $nums[0];             // 10
echo count($nums);         // 4

// associative array (string keys)
$user = [
    "name" => "Alice",
    "age" => 30,
    "email" => "[email protected]",
];
echo $user["name"];        // Alice
$user["phone"] = "555-1234"; // add key

// mixed keys
$mixed = [0 => "a", "name" => "b", 5 => "c"];

// check key existence
if (isset($user["email"])) { /* ... */ }
if (array_key_exists("name", $user)) { /* ... */ }

// remove element
unset($user["phone"]);

多次元配列と反復

多次元配列は配列の配列です。foreach は反復の慣用的な方法です — for ループより速く読みやすいです。要素をその場で変更するには &$value を使用します(バグを避けるためループ後に必ず参照を unset してください)。array_column() は 2 次元配列から単一列を抽出します — データベース結果セットの変換に非常に便利です。PHP の配列は挿入順序を維持します。

php
<?php
$users = [
    ["name" => "Alice", "age" => 30],
    ["name" => "Bob", "age" => 25],
    ["name" => "Carol", "age" => 35],
];

// iterate with key + value
foreach ($users as $index => $user) {
    echo "$index: {$user['name']} ({$user['age']})\n";
}

// modify by reference
foreach ($users as &$user) {
    $user['age'] += 1; // increment each age
}
unset($user); // break reference!

// nested iteration
$matrix = [[1, 2], [3, 4], [5, 6]];
foreach ($matrix as $row) {
    foreach ($row as $cell) {
        echo $cell . " ";
    }
    echo "\n";
}

// extract column
$names = array_column($users, 'name'); // ["Alice", "Bob", "Carol"]

配列関数:map、filter、reduce

array_map、array_filter、array_reduce は配列の関数型プログラミングの三要素です。アロー関数(fn() =>)でこれらを簡潔にします。array_filter はキーを保持します — 必要に応じて array_values() で再インデックスします。array_merge は数値キーを再インデックスしますが文字列キーは保持します(後の値が上書き)。array_column、array_chunk、array_slice はデータ操作に不可欠です。これらの関数は PHP でのデータ処理の骨組みです。

php
<?php
$nums = [1, 2, 3, 4, 5];

// map: transform each element
$doubled = array_map(fn($n) => $n * 2, $nums);  // [2, 4, 6, 8, 10]

// filter: keep elements matching condition
$evens = array_filter($nums, fn($n) => $n % 2 === 0);  // [2, 4]

// reduce: accumulate to single value
$sum = array_reduce($nums, fn($carry, $n) => $carry + $n, 0);  // 15

// walk: like map but modifies in place (by reference)
array_walk($nums, fn(&$n) => $n *= 2);

// combining arrays
$merged = array_merge([1, 2], [3, 4]);        // [1, 2, 3, 4]
$combined = array_combine(['a', 'b'], [1, 2]); // ['a' => 1, 'b' => 2]
$sliced = array_slice($nums, 1, 2);            // [2, 3]
$chunked = array_chunk($nums, 2);              // [[1,2], [3,4], [5]]

配列のソート

PHP のソート関数は配列をその場で変更します(参照渡し)。sort/rsort は再インデックス;asort/arsort はキーを保持します。usort と比較関数(<=> を使用)でカスタムロジックでソートします。natsort() は自然ソートを行います(img10 の前に img2)— ファイル名に不可欠です。多次元配列には、目的のフィールドを比較するクロージャ付きの usort を使用します。宇宙船演算子(<=>)が比較関数を簡素化します。

php
<?php
$nums = [3, 1, 4, 1, 5, 9, 2, 6];

// sort by value (reindexes)
sort($nums);                          // [1, 1, 2, 3, 4, 5, 6, 9]
rsort($nums);                         // descending

// sort preserving keys
asort($nums);   // ascending, preserve keys
arsort($nums);  // descending, preserve keys

// sort by key
ksort($nums);   // by key ascending
krsort($nums);  // by key descending

// custom sort with callback
$users = [["name" => "Bob", "age" => 25], ["name" => "Alice", "age" => 30]];
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
// sorted by age ascending

// natural sort (for strings with numbers)
$files = ["img10.jpg", "img2.jpg", "img1.jpg"];
natsort($files); // ["img1.jpg", "img2.jpg", "img10.jpg"]
sort($files);    // ["img1.jpg", "img10.jpg", "img2.jpg"] (wrong!)

配列の検査と操作

in_array で strict=true(第3引数)を使用すると型強制のバグを防ぎます。array_search はキーを返します(=== false で確認)。array_push/pop は LIFO(スタック)を実装;array_shift/unshift は FIFO(キュー)を実装 — ただし shift は O(n) です。大きなキューには SplQueue や SplDoublyLinkedList を使用します。array_unique はキーを保持します。array_diff/intersect は値を比較;キーベースの比較には array_diff_key/intersect_key を使用します。

php
<?php
$arr = [1, 2, 3, 4, 5];

// inspection
echo count($arr);                  // 5
echo in_array(3, $arr);            // true
echo in_array("3", $arr, true);    // false (strict)
echo array_search(3, $arr);        // 2 (key, false if not found)
print_r(array_keys($arr));         // [0, 1, 2, 3, 4]
print_r(array_values($arr));       // [1, 2, 3, 4, 5]

// stack/queue operations
array_push($arr, 6);    // push to end
$last = array_pop($arr); // pop from end
$first = array_shift($arr); // remove from front
array_unshift($arr, 0); // add to front

// set operations
$unique = array_unique([1, 2, 2, 3]); // [1, 2, 3]
$diff = array_diff([1, 2, 3], [2, 3, 4]); // [1] (in first, not second)
$intersect = array_intersect([1, 2, 3], [2, 3, 4]); // [2, 3]

// flip and reverse
$flipped = array_flip(['a' => 1, 'b' => 2]); // [1 => 'a', 2 => 'b']
$reversed = array_reverse([1, 2, 3]); // [3, 2, 1]
04

制御フロー

If / Else / Elseif

PHP は elseif(1語)を使用します — スペース付きの 'else if' ではありません(ただしそれも動作します)。代替構文(if: ... endif;)は HTML テンプレートでブレースマッチングの混乱を避けるために便利です。三項演算子は右結合です — ネストは避けてください。null 合体代入演算子(??=)は現在 null の場合にのみ値を設定します — 設定デフォルトの遅延初期化に最適です。

php
<?php
$score = 85;

if ($score >= 90) {
    $grade = "A";
} elseif ($score >= 80) {
    $grade = "B";
} elseif ($score >= 70) {
    $grade = "C";
} else {
    $grade = "F";
}

// alternative syntax (for templates)
if ($score >= 90):
    echo "Excellent";
elseif ($score >= 80):
    echo "Good";
else:
    echo "Try harder";
endif;

// ternary
$status = $age >= 18 ? "adult" : "minor";

// null coalescing assignment (PHP 7.4+)
$config['timeout'] ??= 30; // set if not set

Switch と Match

switch は緩い比較(==)を使用し、フォールスルーを防ぐために break が必要です — 一般的なバグの原因です。match(PHP 8+)は厳密な比較(===)を使用し、値を直接返し、アームがマッチしない場合例外をスローします(サイレントな失敗なし)。値が必要な場合、match は switch のモダンな置き換えです。複雑な複数文ケースには switch を;シンプルな値選択には match を使用します。常に default ケースを含めてください。

php
<?php
// switch: loose comparison (==)
$day = "Mon";
switch ($day) {
    case "Mon":
    case "Tue":
    case "Wed":
        echo "Weekday";
        break;
    case "Sat":
    case "Sun":
        echo "Weekend";
        break;
    default:
        echo "Unknown";
}

// match (PHP 8+): strict comparison (===), returns value
$status = 404;
$message = match($status) {
    200, 201 => "Success",
    301, 302 => "Redirect",
    404 => "Not Found",
    500 => "Server Error",
    default => "Unknown",
};
// match throws UnhandledMatchError if no match and no default

ループ:for、while、foreach、do-while

foreach は配列の慣用的なループです — count() 付きの for より速く安全です。continue で反復をスキップし、break で終了します。PHP にはラベル付き break/continue がありません(Java/Rust とは異なります)。連想配列には foreach ($arr as $key => $value) が標準パターンです。do-while は少なくとも1回実行します — 入力検証に便利です。foreach 中の配列変更は避けてください(結果には別の配列を使用)。

php
<?php
// for loop
for ($i = 0; $i < 5; $i++) {
    echo $i;  // 01234
}

// foreach (most common in PHP)
$fruits = ["apple", "banana", "cherry"];
foreach ($fruits as $fruit) {
    echo $fruit;
}

// foreach with key
foreach ($fruits as $index => $fruit) {
    echo "$index: $fruit";
}

// while
$count = 0;
while ($count < 3) {
    echo $count++;
}

// do-while (runs at least once)
do {
    $line = readline("> ");
} while ($line !== "quit");

// break and continue
for ($i = 0; $i < 10; $i++) {
    if ($i === 3) continue; // skip 3
    if ($i === 7) break;    // stop at 7
    echo $i;
}

テンプレートでの制御フロー

PHP の代替制御構文(if:/elseif:/else:/endif;、foreach:/endforeach;)は HTML テンプレート用に設計されています。<?= $var ?> は <?php echo $var; ?> の短縮形です — テンプレートでは常に可読性のために使用してください。XSS を防ぐため、常に htmlspecialchars() で出力をエスケープしてください。PHP ロジックと HTML プレゼンテーションの分離は、Twig や Blade のようなテンプレートシステムの基盤であり、よりクリーンな構文と自動エスケープを提供します。

php
<?php // template file ?>
<?php if ($user->isAdmin()): ?>
    <div class="admin-panel">Admin Tools</div>
<?php elseif ($user->isEditor()): ?>
    <div class="editor-tools">Edit Tools</div>
<?php else: ?>
    <div class="user-view">Read Only</div>
<?php endif; ?>

<?php foreach ($products as $p): ?>
    <div class="product">
        <?= htmlspecialchars($p['name']) ?>
        - $<?= number_format($p['price'], 2) ?>
    </div>
<?php endforeach; ?>

<?php // shorthand echo ?>
<h1><?= $title ?></h1>

<?php // ternary in templates ?>
<span class="<?= $active ? 'on' : 'off' ?>"><?= $active ? 'Active' : 'Inactive' ?></span>

例外とエラー処理

PHP 7+ はほとんどのエラーに例外を使用します。異なる失敗を適切に処理するため、(単なる Exception ではなく)常に具体的な例外型をキャッチしてください。finally は常に実行されます — クリーンアップ(ファイル、接続のクローズ)に使用します。カスタム例外は Exception を拡張し、ドメインコンテキストを追加します。PHP 8+ は | で複数の例外型をキャッチできます。一貫したエラー処理のために PDO を例外モードに設定します。例外をログなしでキャッチしないでください — サイレントな失敗はバグを隠します。

php
<?php
// try / catch / finally
try {
    $pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    error_log("DB connection failed: " . $e->getMessage());
    die("Service unavailable");
} finally {
    // always runs, even after return/throw
    echo "Cleanup";
}

// custom exception
class ValidationException extends Exception {
    public function __construct(string $field, string $message = "") {
        parent::__construct("$field: $message");
    }
}

// throw
if (empty($email)) {
    throw new ValidationException("email", "is required");
}

// catch multiple types (PHP 8+)
try {
    riskyOperation();
} catch (PDOException | RuntimeException $e) {
    // catches either type
    log_error($e);
}
05

関数

関数の定義

PHP 7+ は型付きパラメータと戻り値の型をサポートします(int、string、array、nullable には ?Type)。PHP 8+ は名前付き引数(デフォルトをスキップ、パラメータを並べ替え)、ユニオン型(int|string)、mixed 型を追加します。可変長パラメータ(...$nums)は余分な引数を配列に集めます。スプレッド演算子(...$arr)は配列を引数として展開します。参照渡し(&)は元を変更します — コードの推論が難しくなるため控えめに使用してください。

php
<?php
// basic function with return type
function add(int $a, int $b): int {
    return $a + $b;
}

// default parameters
function greet(string $name, string $greeting = "Hello"): string {
    return "$greeting, $name!";
}

// named arguments (PHP 8+)
echo greet(name: "Alice", greeting: "Hi");

// variadic functions
function sum(int ...$nums): int {
    return array_sum($nums);
}
echo sum(1, 2, 3, 4);  // 10

// spread operator
$nums = [1, 2, 3];
echo sum(...$nums);  // 6

// pass by reference
function increment(int &$n): void {
    $n++;
}
$x = 5;
increment($x);
echo $x;  // 6

アロー関数とクロージャ

アロー関数(fn() =>)は簡潔な単一式クロージャで、外側の変数を値で自動的にキャプチャします。従来のクロージャ(function() use ($var))は複数行の本体や参照キャプチャ(&$var)に必要です。クロージャは array_map、array_filter、usort、イベントハンドラに不可欠です。アロー関数は文を持てません(if、for なし)— 複雑なロジックには従来のクロージャを使用してください。クロージャは第一級オブジェクトです(Closure クラス)。

php
<?php
// arrow function (PHP 7.4+): single expression, auto-capture
$square = fn($x) => $x * $x;
echo $square(5);  // 25

// auto-captures outer variables by value
$multiplier = 3;
$multiply = fn($x) => $x * $multiplier;
echo $multiply(5);  // 15

// traditional closure (multi-line, explicit capture)
$factor = 10;
$scale = function ($x) use ($factor) {
    return $x * $factor + 1;
};

// capture by reference
$count = 0;
$increment = function () use (&$count) {
    $count++;
};
$increment();
echo $count;  // 1

// closures as callbacks
$nums = [1, 2, 3, 4];
$evens = array_filter($nums, fn($n) => $n % 2 === 0);
$doubled = array_map(fn($n) => $n * 2, $nums);

変数スコープとグローバル

PHP は関数レベルのスコープを持ちます — 関数外で定義された変数は 'global' や $GLOBALS なしでは関数内からアクセスできません。'global' は避けてください — 隠れた依存関係を作り、テストを困難にします。代わりに依存性の注入を使用してください。static 変数は関数呼び出し間で持続しますが、関数にスコープされます — キャッシュ/メモ化に便利ですが、長時間実行プロセスで問題を引き起こす可能性があります。クロージャは 'use' で変数を明示的にキャプチャする必要があります。

php
<?php
$global = "I'm global";

function testScope(): void {
    // echo $global; // ERROR: not in scope
    global $global;  // import global
    echo $global;    // OK

    // $GLOBALS superglobal (alternative)
    echo $GLOBALS['global'];
}

// static variables: persist across calls
function counter(): int {
    static $count = 0;
    return ++$count;
}
echo counter();  // 1
echo counter();  // 2
echo counter();  // 3

// closures don't see outer scope by default
$outer = "hello";
$closure = function () {
    // echo $outer; // ERROR
};
$closure2 = function () use ($outer) {
    echo $outer;  // OK, captured
};

型宣言と厳密な型

declare(strict_types=1) は最初の文でなければなりません — ファイル全体に厳密な型チェック(強制なし)を強制します。これがないと、PHP は型を強制します(文字列パラメータに渡された int 5 は '5' になります)。新しいコードでは常に厳密な型を使用してください。PHP 8+ はユニオン型、mixed、never(関数が戻らない)、static(クラスを返す)を追加します。第一級呼び出し可能構文(func(...))は任意の callable からクロージャを作成します — 関数参照よりクリーンです。

php
<?php
// declare strict types (FIRST line of file)
declare(strict_types=1);

function divide(float $a, float $b): float {
    if ($b === 0.0) {
        throw new DivisionByZeroError();
    }
    return $a / $b;
}

// nullable types (?Type or Type|null)
function findUser(?int $id): ?string {
    if ($id === null) return null;
    return "User $id";
}

// union types (PHP 8+)
function process(int|string $input): int|string {
    return is_int($input) ? $input * 2 : strtoupper($input);
}

// return types: void, never, mixed, static
function log(string $msg): void { /* no return */ }
function redirect(): never { header("Location: /"); exit; }

// first-class callable syntax (PHP 8.1+)
$func = strlen(...);  // creates Closure from function
echo $func("hello");  // 5

ジェネレータと Yield

ジェネレータ(yield を持つ関数)は値を遅延生成します — すべての値を前もって計算せず、メモリを節約します。これは大きなファイルやデータセットの処理に不可欠です。yield は関数を一時停止し、値を返します;次の値が要求されると関数が再開します。ジェネレータは Iterator を実装するため、foreach で動作します。ファイル処理、データベース行反復、無限シーケンス、パイプラインにジェネレータを使用してください。key=>value ペアを yield したり、send() で値を受け取ることもできます。

php
<?php
// generator: memory-efficient iteration
function readLines(string $file): Generator {
    $handle = fopen($file, 'r');
    while (($line = fgets($handle)) !== false) {
        yield trim($line);
    }
    fclose($handle);
}

foreach (readLines("large.txt") as $line) {
    echo $line;  // one line at a time, low memory
}

// infinite generator
function fibonacci(): Generator {
    [$a, $b] = [0, 1];
    while (true) {
        yield $a;
        [$a, $b] = [$b, $a + $b];
    }
}

// take first 10
$fib = fibonacci();
for ($i = 0; $i < 10; $i++) {
    echo $fib->current() . " ";
    $fib->next();
}

// yield with key
function pairs(): Generator {
    yield 'a' => 1;
    yield 'b' => 2;
    yield 'c' => 3;
}
06

OOP とクラス

クラス、プロパティとコンストラクタ

PHP 8 のコンストラクタプロモーションはボイラープレートを排除します — プロパティをコンストラクタパラメータとして宣言します。プロパティの可視性:public(どこでも)、protected(クラス + サブクラス)、private(クラスのみ)。readonly(PHP 8.1)は初期化後の変更を防ぎます。self は現在のクラスを参照;static は呼び出しクラスを参照(遅延静的バインディング用)。継承階層で適切なポリモーフィズムのために self:: ではなく static:: を使用してください。

php
<?php
class Person {
    // typed properties (PHP 7.4+)
    public string $name;
    protected int $age;
    private string $email;
    public readonly string $id;  // PHP 8.1+

    // constructor promotion (PHP 8+)
    public function __construct(
        string $name,
        int $age,
        string $email = "",
        string $id = ""
    ) {
        $this->name = $name;
        $this->age = $age;
        $this->email = $email;
        $this->id = $id;
    }

    // methods
    public function greet(): string {
        return "Hi, I'm {$this->name}";
    }

    // static method
    public static function create(string $name): self {
        return new self($name, 0);
    }
}

$p = new Person("Alice", 30);
echo $p->greet();
echo Person::create("Bob")->name;

継承と抽象クラス

abstract クラスはインスタンス化できません — サブクラスのテンプレートを定義します。抽象メソッドは具象サブクラスが実装しなければなりません。PHP は単一継承のみをサポートします(1つの extends)。実装が変更されるべきでない場合、継承/オーバーライドを防ぐために final を使用します。protected メンバーはサブクラスでアクセス可能 — 内部 API に使用します。親にコンストラクタがある場合、常に parent::__construct() を呼び出してください。instanceof で型をチェックします:if ($dog instanceof Animal)。

php
<?php
abstract class Animal {
    protected string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    // abstract method: must be implemented by subclasses
    abstract public function speak(): string;

    // concrete method: inherited as-is
    public function describe(): string {
        return "{$this->name} says {$this->speak()}";
    }
}

class Dog extends Animal {
    public function speak(): string {
        return "Woof";
    }
}

class Cat extends Animal {
    public function speak(): string {
        return "Meow";
    }
}

$dog = new Dog("Rex");
echo $dog->describe();  // Rex says Woof

// final class/method: cannot be extended/overridden
final class Singleton { /* ... */ }

インターフェースとトレイト

インターフェースは契約を定義します — クラスは複数のインターフェースを実装できます(単一継承とは異なります)。すべてのインターフェースメソッドは public でなければなりません。トレイトは継承なしのコード再利用を提供します — 言語レベルの「コピー&ペースト」です。トレイトはプロパティ、メソッド、さらには抽象メソッドを持てます。横断的関心事(タイムスタンプ、ロギング、ソフトデリート)にトレイトを使用します。競合解決:トレイトが同じメソッド名を持つ場合、TraitA::method insteadof TraitB を使用します。

php
<?php
// interface: contract (no implementation)
interface Comparable {
    public function compareTo(object $other): int;
}

interface JsonSerializable {
    public function jsonSerialize(): mixed;
}

// a class can implement multiple interfaces
class Product implements Comparable, JsonSerializable {
    public function __construct(public float $price) {}

    public function compareTo(object $other): int {
        return $this->price <=> $other->price;
    }

    public function jsonSerialize(): mixed {
        return ['price' => $this->price];
    }
}

// trait: reusable code (horizontal reuse)
trait Timestampable {
    public DateTime $createdAt;

    public function setCreatedAt(): void {
        $this->createdAt = new DateTime();
    }

    public function age(): DateInterval {
        return $this->createdAt->diff(new DateTime());
    }
}

class Article {
    use Timestampable; // use trait
}

マジックメソッド

マジックメソッドはオブジェクト操作をインターセプトする特別なメソッドです。__get/__set はプロパティのオーバーロードを実装します(動的プロパティ)。__toString は文字列キャストを可能にします。__invoke はオブジェクトを呼び出し可能にします。__clone はクローン時に実行します(clone $obj)。控えめに使用してください — 追跡が難しい「マジック」を追加します。__get/__set はデータ転送オブジェクトや遅延読み込みに便利です。マジック動作は常に明確に文書化してください。PHP 8.2 は動的プロパティを非推奨にします — __get/__set または #[AllowDynamicProperties] を使用してください。

php
<?php
class Magic {
    private array $data = [];

    // called when accessing undefined property
    public function __get(string $name): mixed {
        return $this->data[$name] ?? null;
    }

    // called when setting undefined property
    public function __set(string $name, mixed $value): void {
        $this->data[$name] = $value;
    }

    // called when isset() or empty() on undefined property
    public function __isset(string $name): bool {
        return isset($this->data[$name]);
    }

    // called when object is used as string
    public function __toString(): string {
        return json_encode($this->data);
    }

    // called when object is called as function
    public function __invoke(string $arg): string {
        return "Called with: $arg";
    }

    // called on clone
    public function __clone(): void {
        $this->data = []; // reset on clone
    }
}

$m = new Magic();
$m->foo = "bar";       // __set
echo $m->foo;          // __get -> "bar"
echo $m;               // __toString -> {"foo":"bar"}
echo $m("test");       // __invoke

名前空間とオートロード

名前空間はクラス名の衝突を防ぎます — Java のパッケージのようなものです。namespace は最初の文でなければなりません。use はクラスをインポートします(オプションのエイリアス付き:use Foo\Bar as B)。PSR-4 オートロードは名前空間をファイルパスにマッピングします:App\Models\User → src/Models/User.php。Composer のオートローダー(require 'vendor/autoload.php')がこれを自動的に処理します。モダン PHP では常に名前空間を使用してください。文字列内の \\ はエスケープされたバックスラッシュです(名前空間セパレータ)。

php
<?php
// file: src/Models/User.php
namespace App\Models;

use App\Database\Connection;
use App\Exceptions\UserNotFoundException;

class User {
    private Connection $db;

    public function __construct(Connection $db) {
        $this->db = $db;
    }

    public function find(int $id): ?self {
        // ...
        throw new UserNotFoundException("User $id not found");
    }
}

// composer.json (PSR-4 autoloading)
// {
//   "autoload": {
//     "psr-4": { "App\\": "src/" }
//   }
// }

// usage
use App\Models\User;
$user = new User($db);
07

Web、フォームとファイル I/O

フォーム処理と検証

常にサーバー側で検証してください — クライアント側検証は UX 用であり、セキュリティ用ではありません。filter_input/filter_var と FILTER_VALIDATE_* は無効な入力で false を返します。検証前に文字列をトリムしてください。データベース挿入にはプリペアドステートメントを使用します。CSRF トークンはクロスサイトリクエストフォージェリを防ぎます — セッションごとに生成し POST で検証します。bin2hex(random_bytes(32)) は暗号学的に安全なトークンを生成します。ユーザー入力は絶対に信頼しないでください — 検証、サニタイズ、エスケープしてください。

php
<?php
// process POST form
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = trim($_POST['name'] ?? '');
    $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
    $age = filter_var($_POST['age'] ?? 0, FILTER_VALIDATE_INT);

    $errors = [];

    if (empty($name)) {
        $errors[] = "Name is required";
    } elseif (strlen($name) > 100) {
        $errors[] = "Name too long";
    }

    if ($email === false) {
        $errors[] = "Valid email required";
    }

    if ($age === false || $age < 18) {
        $errors[] = "Must be 18+";
    }

    if (empty($errors)) {
        // process valid data
        // save to database, redirect, etc.
        header("Location: /success");
        exit;
    }
}

// CSRF token
session_start();
$token = bin2hex(random_bytes(32));
$_SESSION['csrf'] = $token;
// in form: <input type="hidden" name="csrf" value="<?= $token ?>">

セッションと Cookie

セッションはサーバー側にデータを保存します(セッション ID Cookie で識別)。session_start() は出力前に呼び出す必要があります(または ob_start() を使用)。セッションには最小限のデータを保存してください — サーバーメモリを消費します。Cookieには常に secure(HTTPS のみ)、httponly(XSS アクセスを防止)、samesite(CSRF 保護)を設定してください。セッションを正しく破棄します:変数を unset、セッションを破棄、Cookie をクリア。スケーラブルなアプリでは、ファイルの代わりに Redis/データベースに基づくセッションハンドラーを使用します。

php
<?php
// start session (must be before any output)
session_start();

// set session data
$_SESSION['user_id'] = 42;
$_SESSION['username'] = "Alice";
$_SESSION['login_time'] = time();

// read session data
$userId = $_SESSION['user_id'] ?? null;

// check if logged in
function isLoggedIn(): bool {
    return isset($_SESSION['user_id']);
}

// destroy session
session_unset();     // clear variables
session_destroy();   // destroy session
setcookie(session_name(), '', time() - 3600, '/'); // clear cookie

// cookies
setcookie("theme", "dark", [
    'expires' => time() + 86400 * 30, // 30 days
    'path' => '/',
    'secure' => true,     // HTTPS only
    'httponly' => true,   // not accessible via JS
    'samesite' => 'Strict', // CSRF protection
]);

// read cookie
$theme = $_COOKIE['theme'] ?? 'light';

ファイル I/O

file_get_contents/file_put_contents は小さなファイルに便利です。大きなファイルには fopen/fread/fwrite とストリームを使用します。fgetcsv/fputcsv は CSV フォーマットを処理します(引用符/エスケープ含む)。json_decode で true を指定すると連想配列を返します(デフォルトはオブジェクト)。常に file_exists を確認し、エラーを処理してください(権限、ディスクフル)。ファイルアップロードにはセキュリティのため move_uploaded_file() を使用します。並行書き込み時は flock() でファイルをロックします。

php
<?php
// read entire file
$content = file_get_contents("data.txt");
$lines = file("data.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// write file
file_put_contents("output.txt", "Hello World");
file_put_contents("log.txt", "entry\n", FILE_APPEND); // append

// CSV
$csv = fopen("data.csv", "r");
while (($row = fgetcsv($csv)) !== false) {
    print_r($row); // array of column values
}
fclose($csv);

// write CSV
$out = fopen("export.csv", "w");
fputcsv($out, ["Name", "Email", "Age"]);
fputcsv($out, ["Alice", "[email protected]", 30]);
fclose($out);

// JSON
$data = json_decode(file_get_contents("config.json"), true); // assoc array
file_put_contents("config.json", json_encode($data, JSON_PRETTY_PRINT));

// file info
file_exists("data.txt");  // bool
filesize("data.txt");     // bytes
filemtime("data.txt");    // modification timestamp
is_dir("folder");         // bool

ファイルアップロード

ファイルアップロードは $_POST ではなく $_FILES から来ます。常に検証してください:エラーコードをチェック、finfo で MIME タイプを検証(クライアント提供で偽装可能な $_FILES['type'] ではなく)、サイズ制限を強制、安全なファイル名を生成(元の名前は絶対に信頼しない)。move_uploaded_file() はセキュリティ関数です — ファイルが HTTP POST でアップロードされたことを検証します。直接アクセスを防ぐため、アップロードを Web ルート外に保存するか PHP 経由で提供します。アップロードのマルウェアスキャンを検討してください。

php
<?php
// HTML: <form method="POST" enctype="multipart/form-data">
//   <input type="file" name="document">
//   <input type="submit">
// </form>

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $file = $_FILES['document'];

    // $file contains:
    // ['name'] => original filename
    // ['type'] => MIME type (unreliable!)
    // ['tmp_name'] => temporary path
    // ['error'] => UPLOAD_ERR_OK (0) on success
    // ['size'] => file size in bytes

    if ($file['error'] !== UPLOAD_ERR_OK) {
        die("Upload error: " . $file['error']);
    }

    // validate
    $allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mimeType = $finfo->file($file['tmp_name']);

    if (!in_array($mimeType, $allowedTypes)) {
        die("Invalid file type");
    }

    if ($file['size'] > 5 * 1024 * 1024) { // 5MB
        die("File too large");
    }

    // move to permanent location
    $dest = "uploads/" . uniqid() . "_" . $file['name'];
    move_uploaded_file($file['tmp_name'], $dest);
    echo "Uploaded to: $dest";
}

cURL と HTTP リクエスト

cURL は PHP の標準 HTTP クライアントです — HTTPS、リダイレクト、Cookie、認証を処理します。常に CURLOPT_RETURNTRANSFER を設定してレスポンスを文字列として取得してください(そうしないと出力されます)。ハングを防ぐためタイムアウトを設定します。シンプルなリクエストには stream_context 付きの file_get_contents が動作しますが機能が不足します。本番環境では Guzzle(composer require guzzlehttp/guzzle)または Symfony HTTP Client を使用してください — より良い API、再試行ロジック、PSR-18 準拠を提供します。

php
<?php
// GET request
$ch = curl_init("https://api.example.com/users");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode === 200) {
    $data = json_decode($response, true);
    print_r($data);
}

// POST request with JSON
$ch = curl_init("https://api.example.com/users");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode(['name' => 'Alice']),
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $token,
    ],
]);
$response = curl_exec($ch);
curl_close($ch);

// simpler: file_get_contents with context
$context = stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json\r\n",
        'content' => json_encode(['name' => 'Alice']),
    ],
]);
$result = file_get_contents("https://api.example.com/users", false, $context);
08

データベース(PDO)

PDO 接続と基礎

PDO(PHP Data Objects)は標準のデータベース抽象化レイヤーです — MySQL、PostgreSQL、SQLite などをサポートします。適切なエラー処理のために常に ERRMODE_EXCEPTION を設定し、本当のプリペアドステートメントのために ATTR_EMULATE_PREPARES=false を設定してください(より良いセキュリティ)。FETCH_ASSOC は連想配列を返します(オブジェクトには FETCH_OBJ、クラスへのマッピングには FETCH_CLASS を使用)。完全な Unicode サポート(絵文字含む)のために常に utf8mb4 文字セットを使用してください。接続はシングルトンまたは DI コンテナに保存します。

php
<?php
// connect (always use exception mode)
$dsn = "mysql:host=localhost;dbname=test;charset=utf8mb4";
$pdo = new PDO($dsn, "username", "password", [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false, // use real prepared statements
]);

// simple query
$stmt = $pdo->query("SELECT * FROM users LIMIT 5");
$users = $stmt->fetchAll(); // array of associative arrays

// fetch one row
$user = $pdo->query("SELECT * FROM users WHERE id = 1")->fetch();

// fetch column
$count = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();

// execute (no params)
$pdo->exec("DELETE FROM logs WHERE created_at < '2023-01-01'");
$deleted = $pdo->rowCount(); // affected rows

プリペアドステートメント(SQL インジェクション防止)

プリペアドステートメントはユーザー入力を含むクエリに必須です — SQL 構造とデータを分離し、インジェクションを不可能にします。位置パラメータには ? を、名前付きパラメータには :name を使用します。IN 句には、プレースホルダ文字列を動的に構築する必要があります(ただし値は依然としてパラメータ化されます)。lastInsertId() は最後の自動インクリメント値を返します。エスケープ関数を使用しても、ユーザー入力を SQL に連結しないでください。これは PHP のセキュリティルール第1位です。

php
<?php
// prepared statements: THE way to prevent SQL injection
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? AND status = ?");
$stmt->execute([$email, 'active']);
$user = $stmt->fetch();

// named parameters (more readable)
$stmt = $pdo->prepare(
    "INSERT INTO users (name, email, age) VALUES (:name, :email, :age)"
);
$stmt->execute([
    ':name' => 'Alice',
    ':email' => '[email protected]',
    ':age' => 30,
]);
$id = $pdo->lastInsertId(); // get auto-increment ID

// IN clause with prepared statements
$ids = [1, 2, 3, 4];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);
$users = $stmt->fetchAll();

// NEVER do this (SQL injection!):
// $pdo->query("SELECT * FROM users WHERE name = '$_GET[name]'");

トランザクションとエラー処理

トランザクションは原子性を保証します — すべての操作が成功するかすべて失敗します。beginTransaction/commit/rollBack が作業単位をラップします。常にトランザクションを try/catch でラップし、例外でロールバックしてください。PDO はエラー時に PDOException をスローします(ERRMODE_EXCEPTION 使用)。ロック競合を減らすためトランザクションを短く保ちます。ネストされたトランザクションには、セーブポイントまたはトランザクションマネージャーを使用します。トランザクションを開いたままにしないでください — 常にコミットまたはロールバックします。

php
<?php
try {
    $pdo->beginTransaction();

    $pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?")
        ->execute([$amount, $fromId]);

    $pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?")
        ->execute([$amount, $toId]);

    // log transaction
    $pdo->prepare("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)")
        ->execute([$fromId, $toId, $amount]);

    $pdo->commit();
    echo "Transfer complete";
} catch (PDOException $e) {
    $pdo->rollBack(); // undo all changes
    error_log("Transfer failed: " . $e->getMessage());
    throw new RuntimeException("Transfer failed", 0, $e);
}

// check if in transaction
if ($pdo->inTransaction()) {
    $pdo->commit();
}

データフェッチパターン

ユースケースに適切なフェッチモードを選択してください。FETCH_ASSOC が最も一般的です(列名付き配列)。FETCH_CLASS は行をオブジェクトにマッピング — ドメインモデルに最適。FETCH_KEY_PAIR は id=>value マップを作成(ドロップダウン用)。FETCH_GROUP は最初の列で行をグループ化 — 1対多関係に便利です。大きな結果セットには、メモリを節約するため fetchAll() ではなく fetch() をループで使用します。完了時に常に $stmt->closeCursor() でカーソルを閉じてください。

php
<?php
$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE active = 1");
$stmt->execute();

// fetch modes
$row = $stmt->fetch(PDO::FETCH_ASSOC);  // ['id' => 1, 'name' => 'Alice']
$row = $stmt->fetch(PDO::FETCH_NUM);    // [1, 'Alice', '[email protected]']
$row = $stmt->fetch(PDO::FETCH_BOTH);   // both (default)
$obj = $stmt->fetch(PDO::FETCH_OBJ);    // stdClass with properties

// fetch all
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);

// fetch into class
class User {
    public int $id;
    public string $name;
}
$stmt->setFetchMode(PDO::FETCH_CLASS, User::class);
$users = $stmt->fetchAll(); // array of User objects

// fetch key-value pairs
$pairs = $pdo->query("SELECT id, name FROM users")
    ->fetchAll(PDO::FETCH_KEY_PAIR); // [1 => 'Alice', 2 => 'Bob']

// fetch grouped
$grouped = $pdo->query("SELECT dept, name FROM employees")
    ->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
// ['IT' => [['name' => 'Alice']], 'HR' => [['name' => 'Bob']]]

データベースのベストプラクティス

リポジトリパターンはデータアクセスをビジネスロジックから分離します — コードをテスト可能(PDO をモック)で保守しやすくします。依存性の注入を使用して PDO 接続を渡します。クエリごとに新しい PDO 接続を作成しないでください — 単一の接続(またはプール)を再利用します。高トラフィックアプリには、接続プーラーを検討してください(MySQL には ProxySQL、PostgreSQL には PgBouncer)。常に EXPLAIN で遅いクエリをプロファイリングし、適切なインデックスを追加します。複雑なドメインには ORM(Doctrine、Eloquent)を検討してください。

php
<?php
// 1. Connection singleton (or use DI container)
class Database {
    private static ?PDO $instance = null;

    public static function conn(): PDO {
        if (self::$instance === null) {
            self::$instance = new PDO(
                "mysql:host=localhost;dbname=app;charset=utf8mb4",
                "user", "pass",
                [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
            );
        }
        return self::$instance;
    }
}

// 2. Repository pattern
class UserRepository {
    public function __construct(private PDO $db) {}

    public function findById(int $id): ?array {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$id]);
        $user = $stmt->fetch();
        return $user ?: null;
    }
}

// 3. Always close statements (or let them go out of scope)
// 4. Use LIMIT for queries that might return huge results
// 5. Index columns used in WHERE, JOIN, ORDER BY
// 6. Use EXPLAIN to analyze slow queries
09

日付/時刻とセキュリティ

日付と時刻

PHP の日付関数はデフォルトでサーバーのタイムゾーンを使用します — 常に date_default_timezone_set('Asia/Shanghai') を設定するか、DateTimeZone を明示的に使用してください。DateTime クラスはオブジェクト指向で、手続き型関数よりタイムゾーン、間隔、フォーマットをうまく処理します。strtotime() は英語の日付記述を解析します('next Monday'、'+1 month')— 便利ですが月境界で予期しない結果になる可能性があります。日付計算には DateTime::diff() と DateInterval を使用します。データベースでは常に UTC で日付を保存してください。

php
<?php
// current time
$now = date('Y-m-d H:i:s');           // 2024-06-15 14:30:00
$timestamp = time();                   // Unix timestamp
$dt = new DateTime();                  // DateTime object

// formatting
echo date('Y-m-d');                    // 2024-06-15
echo date('d/m/Y H:i:s');              // 15/06/2024 14:30:00
echo date('l, F j, Y');                // Saturday, June 15, 2024

// create from string
$dt = new DateTime('2024-01-15');
$dt = DateTime::createFromFormat('d/m/Y', '15/01/2024');

// modify dates
$dt->modify('+1 month');
$dt->modify('-2 days');
$tomorrow = date('Y-m-d', strtotime('tomorrow'));
$nextWeek = date('Y-m-d', strtotime('+1 week'));

// difference
$start = new DateTime('2024-01-01');
$end = new DateTime('2024-12-31');
$diff = $start->diff($end);
echo $diff->days;  // 365

// timezone
$dt = new DateTime('now', new DateTimeZone('Asia/Shanghai'));
$dt->setTimezone(new DateTimeZone('UTC'));

パスワードハッシュとセキュリティ

password_hash() は bcrypt(または利用可能な場合は Argon2)を自動ソルト生成付きで使用します — 独自のハッシュは絶対に作成しないでください。password_verify() はパスワードをハッシュに対して安全にチェックします(タイミング攻撃を防ぐ定数時間比較)。password_needs_rehash() はコスト係数を増やした時にハッシュをアップグレードできます。ランダムトークン(CSRF、API キー、パスワードリセット)には、予測可能な rand() や mt_rand() ではなく、常に random_bytes() を使用してください。メッセージ認証には hash_hmac を使用します。

php
<?php
// hash password (bcrypt by default)
$hash = password_hash("mypassword", PASSWORD_DEFAULT);
// $2y$10$... (includes algorithm, cost, salt)

// verify password
if (password_verify($input, $hash)) {
    echo "Valid password";
}

// check if hash needs rehash (algorithm upgrade)
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($input, PASSWORD_DEFAULT);
    // update stored hash
}

// NEVER use md5() or sha1() for passwords!
// NEVER store plaintext passwords!

// generate secure random
$token = bin2hex(random_bytes(32));    // 64-char hex string
$bytes = random_bytes(16);             // raw bytes
$int = random_int(1, 1000);            // cryptographically secure

// hash for integrity (not passwords)
$checksum = hash('sha256', $data);
$hmac = hash_hmac('sha256', $data, $secretKey);

出力エスケープと XSS 防止

XSS は Web の脆弱性第1位です — 常にコンテキストに基づいて出力をエスケープしてください。HTML には htmlspecialchars()(ENT_QUOTES は単一引用符と二重引用符の両方をエスケープ)。URL には urlencode()。JavaScript コンテキストには hex フラグ付きの json_encode()。ユーザー入力を絶対に信頼しないでください — 入力時ではなく出力時にエスケープします(他の場所で生データが必要な場合があるため)。防御の深化として Content-Security-Policy ヘッダーを設定します。デフォルトで自動エスケープするテンプレートエンジン(Twig、Blade)を検討してください。

php
<?php
// XSS: Cross-Site Scripting — always escape output!
$name = $_GET['name']; // could be: <script>alert('xss')</script>

// HTML context
echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
// converts < > " ' & to HTML entities

// in HTML template
?>
<p>Hello, <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></p>
<input value="<?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?>">

<?php
// URL context
$url = "https://example.com/search?q=" . urlencode($query);

// JavaScript context
$json = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<script>
    var data = <?= $json ?>;
</script>

<?php
// Content Security Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

JSON と API レスポンス

json_encode/decode は標準の JSON 関数です。API レスポンスには常に Content-Type: application/json を設定します。中国語/絵文字を読みやすく保つために JSON_UNESCAPED_UNICODE を使用します(そうしないと \uXXXX になります)。json_decode で true を指定すると連想配列を返します(PHP でより一般的)。信頼できない JSON をデコードした後は常に json_last_error() を確認してください。REST API では、適切な HTTP ステータスコード(200、201、400、404、500)を設定し、一貫したレスポンス構造を使用します。

php
<?php
// encode PHP array/object to JSON
$data = ['name' => 'Alice', 'age' => 30, 'hobbies' => ['reading', 'coding']];
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// {"name": "Alice", "age": 30, "hobbies": ["reading", "coding"]}

// decode JSON to PHP
$obj = json_decode($json);       // stdClass object
$arr = json_decode($json, true); // associative array

// handle errors
$data = json_decode($badJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    throw new RuntimeException("JSON error: " . json_last_error_msg());
}

// API response
header('Content-Type: application/json; charset=utf-8');
http_response_code(200);
echo json_encode([
    'status' => 'success',
    'data' => $users,
    'meta' => ['page' => 1, 'total' => 100],
], JSON_UNESCAPED_UNICODE);

// API request handling
$input = json_decode(file_get_contents('php://input'), true);

Composer と依存関係管理

Composer は PHP のパッケージマネージャーです — モダン PHP に不可欠です。require は本番依存関係を指定;require-dev は開発用(テストなど)。PSR-4 オートロードは名前空間をディレクトリにマッピングします。常に composer.json と composer.lock をコミットしてください(正確なバージョンを固定)。本番では composer install(lock から)、最新を取得するには composer update を使用します。人気パッケージ:Monolog(ロギング)、Guzzle(HTTP)、PHPUnit(テスト)、Symfony コンポーネント、Laravel フレームワーク。vendor/ ディレクトリは絶対にコミットしないでください。

php
<?php
// composer.json
// {
//   "require": {
//     "monolog/monolog": "^3.0",
//     "guzzlehttp/guzzle": "^7.0"
//   },
//   "autoload": {
//     "psr-4": {"App\\": "src/"}
//   }
// }

// install: composer install
// update:  composer update
// add:    composer require monolog/monolog

// autoload (at the top of your app)
require 'vendor/autoload.php';

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger('app');
$log->pushHandler(new StreamHandler('app.log', Logger::WARNING));
$log->warning('User not found', ['user_id' => 42]);

// environment variables (vlucas/phpdotenv)
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$dbHost = $_ENV['DB_HOST'];
10

セッションと Cookie

Cookie の設定と読み取り

Cookie はユーザーのブラウザに少量のデータを保存します。setcookie() は HTML 出力の前に呼び出す必要があります(HTTP ヘッダーを設定するため)。httponly フラグは JavaScript が Cookie を読み取るのを防ぎ(XSS を緩和)、secure は HTTPS 経由でのみ送信することを保証します。Cookie はマッチするドメイン/パスへのすべてのリクエストで送信されるため、大きなデータを保存しないでください。機密データにはセッションを使用します(データはサーバーに留まる)。Cookie 値は常に検証してサニタイズしてください — クライアントから来るため改ざん可能です。

php
// Set a cookie (must be before any output!)
setcookie("user", "Alice", time() + 3600, "/", "", true, true);
// params: name, value, expiry, path, domain, secure, httponly

// Read cookies (from $_COOKIE superglobal)
if (isset($_COOKIE['user'])) {
    echo "Welcome back, " . htmlspecialchars($_COOKIE['user']);
}

// Delete a cookie: set expiry in the past
setcookie("user", "", time() - 3600, "/");

// Cookie limitations:
// - Sent with every HTTP request (adds bandwidth)
// - Max 4KB per cookie, ~50 per domain
// - Stored client-side (don't store sensitive data!)
// - httponly=true prevents JavaScript access (XSS protection)
// - secure=true sends only over HTTPS

セッション管理

セッションはサーバーにデータを保存し、Cookie に保存されたセッション ID で識別されます。Cookie とは異なり、セッションデータはユーザーに見えません(機密データにより安全)。session_start() はセッションを使用するすべてのページで、出力前に呼び出す必要があります。session_regenerate_id(true) は新しい ID を作成し古いものを削除してセッション固定攻撃を防ぎます — ログイン後に呼び出してください。ログアウト時は常にセッションを破棄します。大規模アプリでは、複数サーバー間で共有するためファイル(デフォルト)の代わりに Redis/データベースにセッションを保存します。

php
// Start or resume a session (must be before output)
session_start();

// Store data in the session (lives on server, identified by session ID cookie)
$_SESSION['user_id'] = 42;
$_SESSION['username'] = 'alice';
$_SESSION['cart'] = ['item1', 'item2'];

// Read session data
if (isset($_SESSION['user_id'])) {
    echo "User: " . $_SESSION['username'];
}

// Remove a single session variable
unset($_SESSION['cart']);

// Destroy the entire session
session_unset();   // clear all variables
session_destroy(); // destroy session data on server
setcookie(session_name(), '', time() - 3600, '/'); // clear session cookie

// Regenerate ID to prevent session fixation attacks
session_regenerate_id(true);

セッション設定(php.ini)

セッションセキュリティ設定は重要です。cookie_httponly は XSS によるセッション ID 盗難を防ぎます。cookie_secure はセッションが HTTPS でのみ動作することを保証します。SameSite=Strict は CSRF を防ぎます(Cookie はクロスサイトリクエストで送信されません)。use_strict_mode は未初期化セッション ID を拒否します。gc_maxlifetime は無操作タイムアウトを設定します。マルチサーバーデプロイでは、データベースまたは Redis にセッションを保存するためのカスタム SessionHandlerInterface を実装してください — デフォルトのファイルベースストレージはサーバー間で動作しません。本番では常にこれらの設定を構成してください。

php
// php.ini session settings (or ini_set at runtime)
ini_set('session.cookie_lifetime', 0);     // 0 = until browser closes
ini_set('session.cookie_httponly', 1);     // prevent JS access
ini_set('session.cookie_secure', 1);       // HTTPS only
ini_set('session.cookie_samesite', 'Strict'); // CSRF protection
ini_set('session.use_strict_mode', 1);     // reject uninitialized IDs
ini_set('session.gc_maxlifetime', 1800);   // 30 min inactivity timeout

// Custom session handler (store in database/Redis)
class MySessionHandler implements SessionHandlerInterface {
    public function open($savePath, $sessionName) { /* connect DB */ }
    public function close() { /* close DB */ }
    public function read($id) { /* fetch from DB */ }
    public function write($id, $data) { /* save to DB */ }
    public function destroy($id) { /* delete from DB */ }
    public function gc($maxlifetime) { /* cleanup old sessions */ }
}
session_set_save_handler(new MySessionHandler(), true);
session_start();

フラッシュメッセージ(一回限り通知)

フラッシュメッセージは一度表示されると自動的にクリアされるセッションベースの通知です(例:「保存成功!」)。パターン:POST リクエストで $_SESSION にメッセージを保存し、次の GET リクエストで読み取って unset します。これは Post/Redirect/Get(PRG)パターンを実装します — フォーム送信後、リフレッシュ時の再送信を防ぐためにリダイレクトし、リダイレクト先のページでフラッシュメッセージを表示します。Laravel($request->session()->flash())や Symfony のようなフレームワークが組み込みのフラッシュメッセージサポートを提供します。

php
// Flash messages: shown once, then deleted
session_start();

// Set a flash message (e.g., after form submission)
$_SESSION['flash'] = [
    'type' => 'success',
    'message' => 'Item added to cart!'
];

// On the next page, display and clear:
if (isset($_SESSION['flash'])) {
    $flash = $_SESSION['flash'];
    unset($_SESSION['flash']);  // delete so it shows only once
    echo "<div class='alert alert-{$flash['type']}'>{$flash['message']}</div>";
}

// Helper function pattern
function flash(string $key, string $value = null): ?string {
    if ($value !== null) {
        $_SESSION['flash_' . $key] = $value;
        return null;
    }
    $val = $_SESSION['flash_' . $key] ?? null;
    unset($_SESSION['flash_' . $key]);
    return $val;
}

セッションセキュリティのベストプラクティス

セッションセキュリティには複数のレイヤーが必要です。固定攻撃を防ぐためログイン後にセッション ID を再生成します(攻撃者が既知のセッション ID を設定する)。アイドルタイムアウトを実装するため last_activity を追跡します。オプションで、ハイジャック検出のためにセッションを IP/ユーザーエージェントにバインドします(ただし、IP が変わるモバイルネットワークで誤検知の可能性があることに注意)。パスワードやクレジットカード番号をセッションに保存しないでください — ユーザー ID のみを保存し、機密データは必要時にデータベースから取得します。セッション ID の傍受を防ぐため、本番では常に HTTPS を使用してください。

php
// 1. Always start session before output
session_start();

// 2. Regenerate ID after login (prevent fixation)
if ($loginSuccessful) {
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user->id;
}

// 3. Validate session on each request
if (isset($_SESSION['user_id'])) {
    // Check session hasn't expired
    if (isset($_SESSION['last_activity']) &&
        time() - $_SESSION['last_activity'] > 1800) {
        session_unset();
        session_destroy();
        header('Location: /login');
        exit;
    }
    $_SESSION['last_activity'] = time();

    // Optional: verify IP/user-agent hasn't changed (anti-hijacking)
    if ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) {
        session_destroy();
        exit('Session hijacking detected');
    }
}

// 4. Use prepared statements for session storage in DB
// 5. Set secure cookie flags (see previous example)
// 6. Never store sensitive data (passwords, CC numbers) in sessions
11

REST API 開発

JSON リクエストとレスポンスの処理

REST API は JSON を交換します。フォーム送信($_POST にデータを入力)とは異なり、JSON リクエストは php://input から読み取り、json_decode でデコードする必要があります。レスポンスには常に Content-Type: application/json を設定し、適切な HTTP ステータスコードに http_response_code() を使用します。すべての入力を検証してください — json_decode は期待される構造を保証しません。安全なアクセスには null 合体演算子(??)を使用します。JSON_PRETTY_PRINT はデバッグに便利ですが、本番ではペイロードを小さくするため省略します。

php
// Get JSON from request body (not $_POST for JSON!)
$json = file_get_contents('php://input');
$data = json_decode($json, true);  // true = associative array

if (json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid JSON']);
    exit;
}

// Process the data
$name = $data['name'] ?? '';
$email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);

// Return JSON response
header('Content-Type: application/json');
http_response_code(200);  // 200 OK, 201 Created, 400 Bad Request, etc.
echo json_encode([
    'success' => true,
    'data' => ['id' => 1, 'name' => $name],
    'message' => 'User created'
], JSON_PRETTY_PRINT);

ルーティングと HTTP メソッド

REST API は HTTP メソッドを CRUD 操作にマッピングします:GET(読み取り)、POST(作成)、PUT/PATCH(更新)、DELETE(削除)。ルーティングはメソッド + URL パスをハンドラにマッチさせます。パラメータ化ルート(例:/api/users/42)には preg_match を使用します。本番では、よりクリーンなルーティング、ミドルウェア、依存性の注入のためにルーターライブラリ(FastRoute、Symfony Routing)やフレームワーク(Laravel、Slim)を使用します。常に適切な HTTP ステータスコードを返します:200(OK)、201(作成済み)、204(コンテンツなし)、400(不正リクエスト)、404(見つからない)、500(サーバーエラー)。

php
// Simple REST router based on method + path
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = trim($path, '/');

// Route table: method => [pattern => handler]
switch (true) {
    case ($method === 'GET' && $path === 'api/users'):
        echo json_encode(getAllUsers());
        break;
    case ($method === 'GET' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        echo json_encode(getUser((int)$m[1]));
        break;
    case ($method === 'POST' && $path === 'api/users'):
        $data = json_decode(file_get_contents('php://input'), true);
        echo json_encode(createUser($data));
        http_response_code(201);
        break;
    case ($method === 'PUT' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        $data = json_decode(file_get_contents('php://input'), true);
        echo json_encode(updateUser((int)$m[1], $data));
        break;
    case ($method === 'DELETE' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        deleteUser((int)$m[1]);
        http_response_code(204);  // No Content
        break;
    default:
        http_response_code(404);
        echo json_encode(['error' => 'Not found']);
}

API 認証(JWT)

JWT はステートレス認証を可能にします — サーバーはセッションを保存する必要がありません。トークンは秘密鍵で署名されたペイロード(ユーザー ID、有効期限)を含みます。クライアントは Authorization ヘッダーでトークンを送信します(Bearer トークン)。サーバーは署名を検証してトークンが改ざんされていないことを確認します。JWT は API とマイクロサービスに最適です(共有セッションストアが不要)。ただし、JWT は有効期限前に取り消せません — 短い有効期限とリフレッシュトークン戦略を使用してください。本番では、自分で暗号を実装するのではなく firebase/php-jwt ライブラリを使用します。JWT ペイロードに機密データを保存しないでください — 暗号化ではなく base64 エンコードのみです。

php
// JWT (JSON Web Token) authentication flow
// 1. Login: verify credentials, issue JWT
function login($email, $password) {
    $user = findUserByEmail($email);
    if ($user && password_verify($password, $user['password_hash'])) {
        // Create JWT: header.payload.signature
        $header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
        $payload = base64url_encode(json_encode([
            'user_id' => $user['id'],
            'exp' => time() + 3600  // expires in 1 hour
        ]));
        $signature = hash_hmac('sha256', "$header.$payload", SECRET_KEY, true);
        $jwt = "$header.$payload." . base64url_encode($signature);
        return $jwt;
    }
    return null;
}

// 2. Verify JWT on protected routes
function verifyJWT($token) {
    $parts = explode('.', $token);
    if (count($parts) !== 3) return false;
    [$header, $payload, $signature] = $parts;
    $expected = base64url_encode(
        hash_hmac('sha256', "$header.$payload", SECRET_KEY, true)
    );
    if (!hash_equals($expected, $signature)) return false;
    $data = json_decode(base64url_decode($payload), true);
    return ($data['exp'] ?? 0) > time() ? $data : false;
}

// Use firebase/php-jwt library in production!

入力検証とサニタイズ

入力検証は API セキュリティに不可欠です。PHP の filter_var は組み込みバリデータ(FILTER_VALIDATE_EMAIL、FILTER_VALIDATE_INT、FILTER_VALIDATE_URL)を提供し、min/max 範囲のようなオプションもあります。常にサーバー側で検証してください — クライアント側検証は UX 用であり、セキュリティ用ではありません。HTML を出力する際 XSS を防ぐため htmlspecialchars で文字列をサニタイズします。JSON API では、検証エラーに対して説明的なメッセージと共に 422(Unprocessable Entity)を返します。複雑なルールには検証ライブラリ(Respect/Validation、Symfony Validator)の使用を検討してください。ユーザー入力を絶対に信頼しないでください — 型、長さ、フォーマット、ビジネスルールを検証します。

php
// Validate and sanitize API input
function validateUserInput(array $data): array {
    $errors = [];

    // Required fields
    if (empty($data['name'])) {
        $errors[] = 'Name is required';
    }

    // Email validation
    $email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
    if (!$email) {
        $errors[] = 'Valid email is required';
    }

    // Age: integer between 18 and 120
    $age = filter_var($data['age'] ?? null, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 18, 'max_range' => 120]
    ]);
    if ($age === false) {
        $errors[] = 'Age must be between 18 and 120';
    }

    // String sanitization (remove tags, trim)
    $name = htmlspecialchars(trim($data['name'] ?? ''), ENT_QUOTES, 'UTF-8');

    // URL validation
    $website = filter_var($data['website'] ?? '', FILTER_VALIDATE_URL);

    return ['errors' => $errors, 'data' => compact('email', 'age', 'name')];
}

$result = validateUserInput($input);
if (!empty($result['errors'])) {
    http_response_code(422);  // Unprocessable Entity
    echo json_encode(['errors' => $result['errors']]);
    exit;
}

CORS(クロスオリジンリソースシェアリング)

CORS はブラウザからどのドメインが API にアクセスできるかを制御します。ブラウザは非シンプルリクエスト(PUT/DELETE、カスタムヘッダー)に対してプリフライト OPTIONS リクエストを送信します。サーバーは適切な Access-Control-Allow-* ヘッダーで応答しなければなりません。セキュリティのため、'*' ではなく正確なオリジンを指定してください(特に認証情報を使用する場合)。API が Cookie や Authorization ヘッダーを使用する場合、Access-Control-Allow-Credentials: true が必要です。Vary: Origin はレスポンスがオリジンによって変化することをキャッシュに伝えます。設定ミスの CORS は API を任意のウェブサイトに晒す可能性があります — 常に信頼できるオリジンをホワイトリストに登録してください。

php
// Enable CORS for API requests from browsers
header('Access-Control-Allow-Origin: https://example.com');  // specific origin
// OR: header('Access-Control-Allow-Origin: *');  // any origin (less secure)
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');  // for cookies/auth
header('Access-Control-Max-Age: 86400');  // cache preflight for 24h

// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Dynamic origin (check against whitelist)
$allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
    header('Access-Control-Allow-Origin: ' . $origin);
    header('Vary: Origin');  // important for caching
}
12

セキュリティ(XSS、CSRF、SQL インジェクション)

XSS(クロスサイトスクリプティング)の防止

XSS は信頼できないデータがエスケープなしで HTML に挿入された時に発生し、攻撃者が被害者のブラウザで JavaScript を実行できるようにします。修正:常に htmlspecialchars で出力をエスケープします(<、>、&、"、' を HTML エンティティに変換)。異なるコンテキストには異なるエスケープが必要:HTML 本文(htmlspecialchars)、HTML 属性(ENT_QUOTES 付きの htmlspecialchars)、JavaScript(json_encode)、URL(urlencode)。Content Security Policy(CSP)ヘッダーはスクリプトの読み込み元を制限して防御を深化します。ユーザー入力で eval()、innerHTML、document.write() を絶対に使用しないでください。Twig や Blade のようなフレームワークはデフォルトで自動エスケープします。

php
// XSS: attacker injects malicious JavaScript into your page
// BAD: outputting user input without escaping
echo "<p>" . $_GET['name'] . "</p>";
// If name = <script>alert('xss')</script>, it executes!

// GOOD: escape output with htmlspecialchars
echo "<p>" . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8') . "</p>";
// & < > " ' are converted to HTML entities

// For HTML attributes:
echo '<input value="' . htmlspecialchars($value, ENT_QUOTES) . '">';

// For JavaScript context (JSON encode):
echo '<script>var name = ' . json_encode($name) . ';</script>';

// For URL parameters:
echo '<a href="?q=' . urlencode($query) . '">Search</a>';

// Content Security Policy (CSP) header — defense in depth
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

// Use prepared statements (prevents SQL injection too)

CSRF(クロスサイトリクエストフォージェリ)の防止

CSRF は認証されたユーザーのブラウザを騙して、ユーザーの知識なしにサイトにリクエストを送信させます(例:送金)。防御:攻撃者が推測できない予測不可能なトークンをフォームに含めます。トークンはセッションに保存され、送信時に検証されます。タイミングセーフな比較には hash_equals() を使用します(タイミング攻撃を防止)。AJAX/API 呼び出しでは、SameSite=Strict Cookie とカスタムヘッダー(X-Requested-With など)の要求が保護を提供します。GET リクエストはデータを変更すべきではありません(画像タグやリンクでトリガー可能)。Laravel や Symfony のようなフレームワークには組み込みの CSRF ミドルウェアがあります。

php
// CSRF: attacker tricks a logged-in user into submitting a form
// to your site (using their session cookie)

// Defense: anti-CSRF tokens
session_start();

// Generate a token (once per session)
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// Include token in forms as a hidden field
echo '<form method="POST" action="/transfer">';
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';
echo '<input type="text" name="amount">';
echo '<button type="submit">Transfer</button>';
echo '</form>';

// Verify token on POST/PUT/DELETE requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';
    if (!hash_equals($_SESSION['csrf_token'], $token)) {
        http_response_code(403);
        die('CSRF token validation failed');
    }
    // Process the form...
}

// For APIs: use SameSite cookies + custom headers
// (browsers block cross-origin requests without explicit CORS permission)

SQL インジェクションの防止

SQL インジェクションは Web の脆弱性第1位です — 攻撃者がデータベース全体を読み取り/変更/削除できるようにします。普遍的な修正:プリペアドステートメント(パラメータ化クエリ)。クエリ構造とデータは別々に送信されるため、ユーザー入力が SQL として解釈されることはありません。ユーザー入力をクエリに連結しないでください。PDO の prepare/execute がエスケープを自動的に処理します。パラメータを型付きでバインドします(PDO::PARAM_INT、PDO::PARAM_STR)。可変項目の IN 句には、プレースホルダを動的に生成します。より良いセキュリティのために PDO::ATTR_EMULATE_PREPARES を false に設定します(本当のサーバー側プリペアドステートメント)。

php
// SQL injection: attacker manipulates queries via unescaped input
// BAD: string concatenation
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
// If id = "1 OR 1=1", returns ALL users!
// If id = "1; DROP TABLE users; --", deletes the table!

// GOOD: prepared statements with parameter binding
$pdo = new PDO('mysql:host=localhost;dbname=myapp', $user, $pass);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  // real prepared statements

$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// Multiple parameters:
$stmt = $pdo->prepare('INSERT INTO users (name, email, age) VALUES (:name, :email, :age)');
$stmt->execute([
    ':name' => $name,
    ':email' => $email,
    ':age' => $age
]);

// IN clause with variable arguments:
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);

パスワードハッシュと認証

password_hash() はランダムソルト付きの bcrypt(または Argon2)を使用します — パスワード保存の業界標準です。ソルトはハッシュに埋め込まれているため、別々に管理する必要はありません。password_verify() は入力を保存されたハッシュに対して安全に比較します(タイミングセーフ)。password_needs_rehash() はコスト係数を増やしたりアルゴリズムを切り替えた時にハッシュをアップグレードできます — 現在の設定にハッシュがマッチするかチェックし、次回ログイン時に再ハッシュします。パスワードに MD5、SHA1、平文を絶対に使用しないでください — 簡単に破解されます。強力なパスワードポリシーを強制しますが、複雑さより長さを優先します(NIST は最低8文字以上を推奨)。

php
// NEVER store plain-text or MD5/SHA1 passwords!
// Use password_hash() (bcrypt/argon2 by default)

// Hash a password (on registration)
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_DEFAULT);
// PASSWORD_DEFAULT = bcrypt (or argon2id in PHP 7.3+)
// Store $hash in the database

// Verify a password (on login)
if (password_verify($inputPassword, $storedHash)) {
    // Password is correct
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user['id'];
} else {
    echo "Invalid credentials";
}

// Rehash if algorithm was upgraded (migration)
if (password_verify($input, $hash) &&
    password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($input, PASSWORD_DEFAULT);
    // Update database with $newHash
}

// Password requirements validation
if (strlen($password) < 8 ||
    !preg_match('/[A-Z]/', $password) ||
    !preg_match('/[a-z]/', $password) ||
    !preg_match('/[0-9]/', $password)) {
    echo "Password must be 8+ chars with upper, lower, and number";
}

ファイルアップロードセキュリティ

ファイルアップロードは主要な攻撃ベクトルです。$_FILES['type'](ブラウザが設定、簡単に偽装)を絶対に信頼しないでください — finfo で本当の MIME タイプを検出します。ユーザー提供のファイル名(../../script.php のようなパストラバーサルを含む可能性)を絶対に使用しないでください — ランダムな名前を生成します。アップロードを Web ルート外、または PHP 実行が無効なディレクトリに保存します。画像の場合、再エンコード(imagecreatefromjpeg + imagejpeg)して EXIF データに隠された埋め込み PHP コードを削除します。サービス拒否を防ぐためファイルサイズを制限します。拡張子、MIME タイプ、マジックバイトを検証します。追加セキュリティのためのアンチウイルス(ClamAV)でのアップロードスキャンを検討してください。

php
// Secure file upload handling
$upload = $_FILES['avatar'];

// 1. Check for upload errors
if ($upload['error'] !== UPLOAD_ERR_OK) {
    die('Upload failed: error ' . $upload['error']);
}

// 2. Validate file size
$maxSize = 2 * 1024 * 1024;  // 2MB
if ($upload['size'] > $maxSize) {
    die('File too large (max 2MB)');
}

// 3. Validate MIME type (don't trust $_FILES['type']!)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($upload['tmp_name']);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mimeType, $allowedTypes)) {
    die('Invalid file type');
}

// 4. Generate a safe filename (never use user-supplied name)
$safeName = bin2hex(random_bytes(16)) . '.jpg';

// 5. Store OUTSIDE the web root (or in a non-executable dir)
$dest = __DIR__ . '/uploads/' . $safeName;
if (!move_uploaded_file($upload['tmp_name'], $dest)) {
    die('Failed to save file');
}

// 6. For images: re-encode to strip malicious metadata
$img = imagecreatefromjpeg($dest);
imagejpeg($img, $dest, 90);  // re-encode strips embedded PHP/scripts
13

cURL と HTTP リクエスト

基本的な cURL GET と POST

cURL は PHP の最も強力な HTTP クライアントで、GET、POST、カスタムメソッド、ヘッダー、Cookie、SSL をサポートします。常に CURLOPT_RETURNTRANSFER を設定してレスポンスを文字列として取得してください(そうしないと直接出力されます)。CURLOPT_TIMEOUT は遅いサーバーでのハングを防ぎます。JSON の POST の場合、Content-Type と Content-Length ヘッダーを明示的に設定します。接続エラーは curl_errno() で、HTTP ステータスは curl_getinfo(CURLINFO_HTTP_CODE) で確認します。リソースを解放するため常に curl_close() で cURL ハンドルを閉じてください。よりシンプルなコードには、Guzzle(よりクリーンな API の cURL ラッパー)を検討してください。

php
// GET request
$ch = curl_init('https://api.example.com/users');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
    echo 'Error: ' . curl_error($ch);
}
curl_close($ch);
$data = json_decode($response, true);

// POST request with JSON body
$ch = curl_init('https://api.example.com/users');
$payload = json_encode(['name' => 'Alice', 'email' => '[email protected]']);
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $token,
        'Content-Length: ' . strlen($payload)
    ],
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);

認証と Cookie を伴う cURL

cURL は複数の認証方式をサポートします。CURLOPT_USERPWD は HTTP Basic 認証を設定します。Bearer トークンは Authorization ヘッダーに置きます。Cookie ベースのセッション(ウェブサイトへのログインなど)では、CURLOPT_COOKIEJAR で Cookie を保存し、CURLOPT_COOKIEFILE で後続リクエストで送信します — これが複数の cURL 呼び出し間でセッションを維持します。Cookie には一時ファイルを使用し、unlink() でクリーンアップします。API 呼び出しでは、Cookie よりトークンベース認証(Bearer)を優先します。常に HTTPS を使用してください(cURL はデフォルトで SSL を検証します — 本番で CURLOPT_SSL_VERIFYPEER を無効にしないでください)。

php
// Basic Auth
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);

// Bearer token (API key)
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
    'Accept: application/json'
]);

// Cookie-based session (login then reuse cookies)
$cookieFile = tempnam(sys_get_temp_dir(), 'cookie');

// Step 1: Login (saves cookies)
$ch = curl_init('https://example.com/login');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query(['user' => 'alice', 'pass' => 'secret']),
    CURLOPT_COOKIEJAR => $cookieFile,   // save cookies here
    CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);

// Step 2: Access protected page (sends saved cookies)
$ch = curl_init('https://example.com/dashboard');
curl_setopt_array($ch, [
    CURLOPT_COOKIEFILE => $cookieFile,  // send cookies from here
    CURLOPT_RETURNTRANSFER => true,
]);
$dashboard = curl_exec($ch);
curl_close($ch);
unlink($cookieFile);  // cleanup

ファイルダウンロードとストリーミング

大きなファイルダウンロードには、CURLOPT_FILE を使用してファイルハンドルに直接書き込みます — レスポンス全体をメモリに読み込むのを避けます。CURLOPT_FOLLOWLOCATION は HTTP リダイレクト(301、302)に従います。ストリーミング(例:リアルタイムデータ)には、CURLOPT_WRITEFUNCTION を使用してチャンクが到着した時に処理します — データをストリーミングする API に便利です。CURLOPT_PROGRESSFUNCTION はダウンロード/アップロードの進捗を監視します。大きなファイルには十分な CURLOPT_TIMEOUT を設定します。非常に大きなアップロードには、メモリに読み込む代わりに CURLOPT_INFILE でファイルからストリーミングします。リソースリークを防ぐため、常にファイルハンドルと cURL ハンドルを閉じてください。

php
// Download a file to disk
$ch = curl_init('https://example.com/large-file.zip');
$fp = fopen('downloaded.zip', 'w');
curl_setopt_array($ch, [
    CURLOPT_FILE => $fp,           // write directly to file
    CURLOPT_FOLLOWLOCATION => true, // follow redirects
    CURLOPT_TIMEOUT => 300,        // 5 min for large files
]);
curl_exec($ch);
fclose($fp);
curl_close($ch);

// Stream response with callback (progress monitoring)
$ch = curl_init('https://example.com/stream');
curl_setopt_array($ch, [
    CURLOPT_WRITEFUNCTION => function($ch, $chunk) {
        echo $chunk;  // stream to output
        ob_flush();
        flush();
        return strlen($chunk);
    },
    CURLOPT_PROGRESSFUNCTION => function($ch, $dlTotal, $dlNow, $ulTotal, $ulNow) {
        if ($dlTotal > 0) {
            printf("
Progress: %.1f%%", ($dlNow / $dlTotal) * 100);
        }
        return 0;
    },
]);
curl_exec($ch);
curl_close($ch);

並行リクエスト(Multi cURL)

curl_multi_exec は複数の HTTP リクエストを並行実行します — 複数のエンドポイントからデータが必要な場合、順次リクエストより劇的に高速です。パターン:マルチハンドルを作成、個別の cURL ハンドルを追加、ループでマルチハンドルを実行(効率のため curl_multi_exec + curl_multi_select)、結果を収集。複数の API からのデータ集計、リソースのプリフェッチ、バッチ操作に便利です。より高度な並行性には、ReactPHP や Amp(非同期 PHP フレームワーク)を検討してください。multi-cURL は依然として PHP プロセスをブロックすることに注意してください — 真の非同期には、イベントループやメッセージキューを使用します。

php
// Fetch multiple URLs in parallel (much faster than sequential)
$urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments'
];

$multi = curl_multi_init();
$handles = [];

// Create individual handles
foreach ($urls as $i => $url) {
    $handles[$i] = curl_init($url);
    curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
    curl_multi_add_handle($multi, $handles[$i]);
}

// Execute all requests concurrently
$active = null;
do {
    $status = curl_multi_exec($multi, $active);
    if ($active) {
        curl_multi_select($multi);  // wait for activity
    }
} while ($active && $status === CURLM_OK);

// Collect results
$responses = [];
foreach ($handles as $i => $ch) {
    $responses[$i] = json_decode(curl_multi_getcontent($ch), true);
    curl_multi_remove_handle($multi, $ch);
    curl_close($ch);
}
curl_multi_close($multi);

// $responses[0], [1], [2] now contain all three API responses

Guzzle の使用(モダン HTTP クライアント)

Guzzle はモダン PHP の標準 HTTP クライアントです — 生の cURL よりはるかにクリーンです。流れるような API、PSR-7 準拠のリクエスト/レスポンスオブジェクト、ミドルウェア(ロギング、再試行)、Promise を介した非同期リクエストを提供します。'json' オプションは本体を自動エンコードし Content-Type を設定します。getAsync/postAsync は multi-cURL の複雑さなしに並行リクエスト用の Promise を返します。例外処理は組み込み:RequestException が HTTP エラー(4xx、5xx)をキャッチします。Guzzle はほとんどのフレームワークで使用されています(Laravel の HTTP クライアントは Guzzle をラップ)。Composer でインストール:composer require guzzlehttp/guzzle。

php
// Guzzle is the most popular PHP HTTP client (composer require guzzlehttp/guzzle)
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$client = new Client([
    'base_uri' => 'https://api.example.com',
    'timeout'  => 30,
    'headers'  => ['Accept' => 'application/json'],
]);

// GET request
$response = $client->get('/users/42');
$body = json_decode($response->getBody(), true);
echo $response->getStatusCode();  // 200

// POST with JSON
$response = $client->post('/users', [
    'json' => ['name' => 'Alice', 'email' => '[email protected]'],
    'headers' => ['Authorization' => 'Bearer ' . $token],
]);

// Concurrent requests (Promise-based)
$promises = [
    'users' => $client->getAsync('/users'),
    'posts' => $client->getAsync('/posts'),
];
$results = GuzzleHttp\Promise\Utils::settle($promises)->wait();

// Error handling with try/catch
try {
    $response = $client->get('/nonexistent');
} catch (RequestException $e) {
    echo $e->getResponse()->getStatusCode();  // 404
}
14

DateTime の深掘り

DateTime の作成と操作

DateTime は PHP の堅牢な日付/時刻クラスです。DateTimeImmutable が DateTime より推奨されます — 変更時に新しいオブジェクトを返し、偶発的な変更バグを防ぎます(同じ日付が複数の場所で使用される場合に重要)。createFromFormat はカスタムフォーマットを解析します。modify() は '+1 week' や 'last day of next month' のような相対式を受け入れます。サーバー設定依存の動作を避けるため、常にタイムゾーンを明示的に指定してください。日付計算(間隔の追加)には、add()/sub() で DateInterval('P1D' = 1日、'P2W' = 2週間、'PT2H' = 2時間)を使用します。

php
$now = new DateTime();  // current time
$now->setTimezone(new DateTimeZone('Asia/Shanghai'));

// From string (many formats supported)
$date = new DateTime('2024-06-15 14:30:00');
$date = new DateTime('first day of next month');
$date = new DateTime('next Friday');

// From format (precise parsing)
$date = DateTime::createFromFormat('d/m/Y', '15/06/2024');

// Modify dates with relative expressions
$date->modify('+1 week');
$date->modify('-2 days');
$date->modify('last day of this month');
$date->modify('+1 year 3 months');

// Immutable version (recommended — doesn't modify original)
$dt = new DateTimeImmutable('2024-06-15');
$next = $dt->modify('+1 day');  // $dt unchanged, $next is new object
echo $dt->format('Y-m-d');   // 2024-06-15 (unchanged!)
echo $next->format('Y-m-d'); // 2024-06-16

フォーマットとタイムゾーン

format() はパターン文字で出力をカスタマイズします — Y(4桁年)、m(2桁月)、d(2桁日)、H(24時間)、i(分)、s(秒)。ISO 8601(API で使用)には 'Y-m-d\TH:i:sP' または 'c' ショートカットを使用します。タイムゾーン変換:ソースタイムゾーンで作成し、setTimezone で変換します。データベースには常に UTC で日付を保存し、表示のためだけにユーザーのタイムゾーンに変換します。PHP のタイムゾーンデータベースは包括的です(DST ルールを含む)。DateTimeZone::listIdentifiers() でサポートされるすべてのゾーンを取得します。format() の 'T' エスケープ文字はリテラル 'T' を出力します(ISO 8601 用)。

php
// Format codes (most common):
// Y=2024  y=24  m=06  n=6  d=15  j=15
// H=14 (24h)  h=02 (12h)  i=30  s=00  A=PM  a=pm
// D=Mon  l=Monday  M=Jun  F=June  N=1 (Mon=1..Sun=7)

$date = new DateTime('2024-06-15 14:30:00');
echo $date->format('Y-m-d H:i:s');  // 2024-06-15 14:30:00
echo $date->format('l, F j, Y');    // Saturday, June 15, 2024
echo $date->format('Y-m-d\TH:i:sP'); // 2024-06-15T14:30:00+08:00 (ISO 8601)

// Timezone conversion
$utc = new DateTime('2024-06-15 14:00:00', new DateTimeZone('UTC'));
$utc->setTimezone(new DateTimeZone('Asia/Shanghai'));
echo $utc->format('H:i');  // 22:00 (UTC+8)

// List supported timezones
$tzs = DateTimeZone::listIdentifiers();
// ['UTC', 'America/New_York', 'Asia/Shanghai', ...]

// Get timezone offset
$tz = new DateTimeZone('America/Los_Angeles');
$offset = $tz->getOffset(new DateTime());  // seconds from UTC

日付間隔と差分

DateInterval は ISO 8601 期間フォーマット(P1Y2M3DT4H5M6S)で時間持続時間を表します。add() と sub() は日付に間隔を適用します。diff() は2つの日付間の差を表す DateInterval を返します — 'days' プロパティは総日数を与え、'y'、'm'、'd' はコンポーネント内訳を与えます。'invert' プロパティは方向を示します(2番目の日付が早い場合 1)。月計算に注意:1月31日に 'P1M' を追加すると2月31日ではなく3月2日になります(2月は28-29日)。営業日計算には、手動で反復して週末/祝日をスキップするか、nesbot/carbon のようなライブラリを使用します。

php
// DateInterval: represents a duration
$interval = new DateInterval('P1Y2M3DT4H5M6S');
// P = period, 1Y = 1 year, 2M = 2 months, 3D = 3 days
// T = time separator, 4H = 4 hours, 5M = 5 minutes, 6S = 6 seconds

$date = new DateTime('2024-06-15');
$date->add(new DateInterval('P1M'));  // +1 month → 2024-07-15
$date->sub(new DateInterval('P10D')); // -10 days → 2024-07-05

// Difference between two dates
$d1 = new DateTime('2024-01-01');
$d2 = new DateTime('2024-12-31');
$diff = $d1->diff($d2);
echo $diff->days;     // 365 (total days)
echo $diff->m;        // 0 (months component)
echo $diff->y;        // 0 (years component)
echo $diff->format('%a days');  // 365 days
echo $diff->format('%y years, %m months, %d days');  // 0 years, 11 months, 30 days

// Check if inverted (d2 < d1)
echo $diff->invert;   // 0 if d2 >= d1, 1 if d2 < d1

DatePeriod(日付範囲の反復)

DatePeriod は指定された間隔で日付範囲を反復します — カレンダー、レポート、定期イベントの生成に最適です。コンストラクタは (start, interval, end) または (start, interval, recurrences) を取ります。終了日は排他的です。一般的なユースケース:カレンダービュー用の月内のすべての日を生成、給与期間のリスト、定期イベントスケジュールの作成。iterator_to_array() で期間を配列に具体化します。複雑な繰り返しルール(例:「毎月第2火曜日」)には、rrule(RFC 5545 繰り返しルール)のような専用ライブラリを検討してください。

php
// DatePeriod iterates over a range of dates
$start = new DateTime('2024-06-01');
$end = new DateTime('2024-06-10');
$interval = new DateInterval('P1D');  // 1 day

$period = new DatePeriod($start, $interval, $end);
foreach ($period as $day) {
    echo $day->format('Y-m-d (D)') . "\n";
}
// 2024-06-01 (Sat)
// 2024-06-02 (Sun)
// ... through 2024-06-09 (end is exclusive)

// Generate next 12 months
$start = new DateTime('first day of this month');
$interval = new DateInterval('P1M');
$period = new DatePeriod($start, $interval, 12);  // 12 recurrences
foreach ($period as $month) {
    echo $month->format('F Y') . "\n";
}

// Every Monday for a year
$start = new DateTime('next Monday');
$end = new DateTime('+1 year');
$period = new DatePeriod($start, new DateInterval('P1W'), $end);
$mondays = iterator_to_array($period);

Carbon ライブラリ(拡張 DateTime)

Carbon は流れるような表現力豊かな API で DateTime を拡張します — PHP エコシステムの事実上の標準です(Laravel で使用)。diffForHumans() は「5 days ago」「3 hours from now」を生成 — UI タイムスタンプに最適。流れるような API はメソッドをチェーンします(addYear()->subMonth()->endOfMonth())。比較メソッド(isWeekend、isPast、isToday)が一般的なチェックを簡素化します。ローカライズは人間が読める出力のために50以上の言語をサポートします。Carbon 3(2024+)はデフォルトでイミュータブルです。Composer でインストール:composer require nesbot/carbon。Laravel を使用している場合、Carbon は既に含まれています。

php
// Carbon: the most popular DateTime library (composer require nesbot/carbon)
use Carbon\Carbon;

$now = Carbon::now('Asia/Shanghai');
$tomorrow = Carbon::tomorrow();
$lastWeek = Carbon::now()->subWeek();

// Human-readable differences
echo Carbon::now()->diffForHumans(Carbon::now()->subDays(5));
// "5 days ago"
echo Carbon::now()->addHours(3)->diffForHumans();
// "3 hours from now"

// Fluent API
$date = Carbon::create(2024, 6, 15, 14, 30, 0)
    ->addYear()
    ->subMonth()
    ->endOfMonth()
    ->setTimezone('UTC');

// Comparison methods
if ($date->isWeekend()) { echo "Weekend!"; }
if ($date->isPast()) { echo "Past"; }
if ($date->isFuture()) { echo "Future"; }
if ($date->isToday()) { echo "Today"; }
if ($date->isLeapYear()) { echo "Leap year"; }

// Localization
Carbon::setLocale('zh');
echo Carbon::now()->subDay()->diffForHumans();  // "1天前"
15

名前空間とオートロード

名前空間の基礎

名前空間はコードを階層パッケージに整理し、ライブラリ間のクラス名衝突を防ぎます。namespace 宣言は最初の文でなければなりません(declare() の後)。'use' 文は他の名前空間からクラスをインポートします — use 文をファイルの先頭に配置します。エイリアス(as)は2つのクラスが同じ名前の場合の衝突を解決します。先頭のバックスラッシュ(\DateTime)はグローバル名前空間を参照します。PHP の名前空間はバックスラッシュ(\)をセパレータとして使用し、PSR-4 オートロードでディレクトリ構造にマッピングします。グループ use 文(use App\Models\{User, Post})がボイラープレートを削減します。

php
<?php
// Namespaces prevent name collisions (like packages in Java)
namespace App\Services;

class UserService {
    public function find($id) { /* ... */ }
}

// Using namespaced classes
use App\Services\UserService;
use App\Models\User;

$service = new UserService();
$user = new User();

// Aliasing (resolve conflicts or shorten names)
use App\Services\UserService as USvc;
use App\Models\User as UserModel;

// Global namespace (backslash prefix)
$now = new \DateTime();  // \ means root/global namespace
$array = new \ArrayObject();

// Multiple use statements grouped
use App\Models\{User, Post, Comment};
use App\Services\{UserService, PostService};

PSR-4 オートロード標準

PSR-4 は標準のオートロード仕様です — 名前空間をディレクトリパスにマッピングするため、手動の require/include 文が不要になります。ルール:App\Services\UserService は src/Services/UserService.php にマッピングされます(App\ → src/)。composer.json の autoload セクションでマッピングを構成します。新しいクラスを追加した後、'composer dump-autoload' を実行してクラスマップを再生成します。vendor/autoload.php ファイル(Composer が生成)が読み込みを処理します — エントリポイント(index.php)で一度インクルードします。PSR-4 はクラス名がファイル名と一致することを強制します(UserService → UserService.php)。

php
// PSR-4: namespace structure maps to file paths
// App\Services\UserService → src/Services/UserService.php

// composer.json PSR-4 configuration:
// {
//   "autoload": {
//     "psr-4": {
//       "App\\": "src/"
//     }
//   }
// }

// File: src/Services/UserService.php
namespace App\Services;

class UserService {
    public function getUser($id) {
        return "User $id";
    }
}

// File: src/Models/User.php
namespace App\Models;

class User {
    public $name;
    public function __construct($name) {
        $this->name = $name;
    }
}

// After adding classes, regenerate autoloader:
// $ composer dump-autoload

// The autoloader is included once in your entry point:
require __DIR__ . '/vendor/autoload.php';

$service = new App\Services\UserService();
$user = new App\Models\User('Alice');

Composer なしのオートロード(spl_autoload)

spl_autoload_register はクラスがまだロードされていない時に呼び出される関数を登録します — 完全修飾クラス名を受け取り、対応するファイルを require すべきです。複数のオートローダーを登録できます(順番に呼ばれます)。これが Composer が内部で使用するものです。本番では、常に Composer の PSR-4 オートローダーを使用してください — 最適化され、エッジケースを処理し、より高速なルックアップのためにクラスマップを生成します。spl_autoload_register を直接使用するのは、小さなプロジェクトや Composer が利用できない場合のみです。class_exists() の 'true' パラメータは、クラスがロードされていない場合にオートロードをトリガーします。

php
<?php
// Simple autoloader for small projects (no Composer needed)
spl_autoload_register(function ($className) {
    // Convert namespace separators to directory separators
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $className) . '.php';

    // App\Services\UserService → src/App/Services/UserService.php
    // Adjust prefix mapping as needed:
    $file = str_replace('App/', '', $file);  // remove 'App' prefix

    if (file_exists($file)) {
        require $file;
    }
});

// Now classes are loaded automatically on first use
$service = new App\Services\UserService();

// Multiple autoloaders (called in order until one loads the class)
spl_autoload_register(function ($class) {
    $path = __DIR__ . '/lib/' . $class . '.php';
    if (file_exists($path)) require $path;
});

// Check if a class is autoloadable
if (class_exists('App\Helper', true)) {  // true = attempt autoload
    $helper = new App\Helper();
}

名前空間の定数と関数

名前空間はクラスだけでなく、定数と関数も含められます。'use const' と 'use function'(PHP 5.6+)でインポートします。これは設定定数とユーティリティ関数に便利です。非修飾関数/定数呼び出しにはフォールバック動作があります:PHP はまず現在の名前空間を探し、次にグローバル名前空間にフォールバックします。これがバックスラッシュなしで strlen() を呼び出せる理由です — ただしパフォーマンスと明確さのため、名前空間コードではグローバル関数に \ を前置します。グループインポート(use App\Config\{const DB_HOST, function connect})が冗長性を削減します。

php
<?php
namespace App\Config;

// Constants in namespaces
const DB_HOST = 'localhost';
const DB_PORT = 3306;

// Functions in namespaces
function connect() {
    return 'Connected to ' . DB_HOST;
}

// Using namespaced constants and functions
use const App\Config\DB_HOST;
use function App\Config\connect;

echo DB_HOST;      // localhost
echo connect();    // Connected to localhost

// Or with fully-qualified names:
echo \App\Config\DB_HOST;
echo \App\Config\connect();

// Namespace-level use for multiple imports
use App\Config\{const DB_HOST, const DB_PORT, function connect};

// Fallback: unqualified function calls fall back to global
// if not found in current namespace
namespace App;
$len = strlen('hello');  // calls global strlen() (fallback)

匿名クラスとオートロード

匿名クラス(PHP 7+)は名前付きクラスを定義せずにシンプルな使い捨てオブジェクトを作成できます — インターフェース、モックオブジェクト、コールバックに便利です。インターフェースの実装、クラスの拡張、コンストラクタの持ち、トレイトの使用ができます。クラスは実行時に自動生成名(class@anonymous)で生成されます。匿名クラスは即座にロードされます(オートロード不要)。シンプルなストラテジーパターン、テストダブル/モック、イベントリスナー、DTO に使用します。再利用可能なクラスには、常に適切な PSR-4 オートロード付きの名前付きクラスを定義してください。匿名クラスはテストで軽量スタブを作成するのに特に便利です。

php
<?php
namespace App\Factory;

// Anonymous class (PHP 7+): create a one-off class inline
interface Logger {
    public function log(string $msg): void;
}

class App {
    private $logger;
    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }
}

$app = new App();
$app->setLogger(new class implements Logger {
    public function log(string $msg): void {
        echo "[LOG] $msg\n";
    }
});

// Anonymous class with constructor
$comparator = new class($ascending = true) {
    private $asc;
    public function __construct(bool $asc) { $this->asc = $asc; }
    public function compare($a, $b): int {
        return $this->asc ? $a <=> $b : $b <=> $a;
    }
};

// Get the auto-generated class name
echo get_class($comparator);  // class@anonymous...
16

OOP の深掘り(トレイト、インターフェース、抽象)

抽象クラスとメソッド

抽象クラスはサブクラスが拡張する共有実装を持つベースを提供します。直接インスタンス化できません。抽象メソッドは具象サブクラスが実装しなければならない契約(シグネチャのみ)を定義します — これは「テンプレートメソッドパターン」です。インターフェースとは異なり、抽象クラスはプロパティ、コンストラクタ、具象メソッドを持てます。サブクラスが重要な実装を共有する場合(「is-a」関係)に抽象クラスを使用します。任意のクラスが実装できる契約が必要な場合(「can-do」関係)にインターフェースを使用します。クラスは1つの抽象クラスしか拡張できませんが、複数のインターフェースを実装できます。

php
<?php
// Abstract class: can't be instantiated, may have abstract methods
abstract class Animal {
    protected $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    // Abstract method: must be implemented by subclasses
    abstract public function makeSound(): string;

    // Concrete method: shared implementation
    public function describe(): string {
        return $this->name . " says " . $this->makeSound();
    }
}

class Dog extends Animal {
    public function makeSound(): string {
        return "Woof!";
    }
}

class Cat extends Animal {
    public function makeSound(): string {
        return "Meow!";
    }
}

$dog = new Dog("Rex");
echo $dog->describe();  // Rex says Woof!
// new Animal("test");  // Error: cannot instantiate abstract class

インターフェースと多重実装

インターフェースは契約を定義します — 実装のないメソッドシグネチャ。クラスは複数のインターフェースを実装できます(クラスの単一継承とは異なります)。インターフェースはポリモーフィズムを可能にします:Comparable を実装する任意のクラスは具象型に関係なくソートできます。クラス階層を横断する能力(Comparable、Serializable、Iterable)を定義するためにインターフェースを使用します。インターフェースでの型ヒンティング(function sort(Comparable $a))は具象クラスより柔軟です。インターフェース継承(interface A extends B, C)が契約を組み合わせます。モダン PHP はインターフェース定数とインターフェース内の静的メソッドもサポートします。

php
<?php
// Interface: pure contract (no implementation)
interface Comparable {
    public function compareTo($other): int;
}

interface Serializable {
    public function serialize(): string;
    public function unserialize(string $data): void;
}

// A class can implement MULTIPLE interfaces
class Product implements Comparable, Serializable {
    private $price;

    public function __construct(float $price) {
        $this->price = $price;
    }

    public function compareTo($other): int {
        return $this->price <=> $other->price;  // spaceship operator
    }

    public function serialize(): string {
        return serialize($this->price);
    }

    public function unserialize(string $data): void {
        $this->price = unserialize($data);
    }
}

// Type hinting with interfaces
function sortItems(array $items): array {
    usort($items, fn($a, $b) => $a->compareTo($b));
    return $items;
}

// Interface inheritance
interface Repository extends Comparable, Serializable {
    public function find(int $id): ?object;
}

トレイト(継承なしのコード再利用)

トレイトは水平的コード再利用を提供します — 継承なしで任意のクラスに「貼り付け」できるメソッド。これはダイヤモンド問題を解決します(PHP は単一継承)。一般的なトレイトの用途:ロギング、シングルトンパターン、ソフトデリート、タイムスタンプ。クラスは複数のトレイトを使用できます。トレイトが競合するメソッドを持つ場合、'insteadof' で1つを選び、'as' で別名を付けます。トレイトは抽象メソッド(使用クラスに実装を強制)と静的メソッド/プロパティを持てます。トレイトを使いすぎないように注意してください — コードの追跡が難しくなります。複雑な動作にはトレイトよりコンポジション(依存性の注入)を優先してください。

php
<?php
// Trait: reusable method groups (PHP's answer to multiple inheritance)
trait Logger {
    protected function log(string $msg, string $level = 'INFO'): void {
        echo "[$level] " . date('Y-m-d H:i:s') . " $msg\n";
        // In real code: write to file/database
    }
}

trait Singleton {
    private static $instance;
    public static function getInstance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

class UserService {
    use Logger, Singleton;

    public function findUser($id) {
        $this->log("Finding user $id");
        return "User $id";
    }
}

$svc = UserService::getInstance();
$svc->findUser(42);  // [INFO] 2024-06-15 14:30:00 Finding user 42

// Conflict resolution when traits have same method
trait A { public function hello() { return 'A'; } }
trait B { public function hello() { return 'B'; } }
class C {
    use A, B {
        B::hello insteadof A;  // use B's hello
        A::hello as helloFromA; // alias A's as helloFromA
    }
}

遅延静的バインディング(static:: vs self::)

遅延静的バインディング(LSB)は self::(コンパイル時、常に定義クラスを参照)と static::(実行時、呼び出しクラスを参照)の違いです。これは継承で重要です:Base が self::$table を使用するメソッドを持つ場合、Child で呼び出されても常に Base の $table を見ます。static::$table を使用すると Child の $table を見ます。LSB はファクトリパターン(new static() が呼び出しクラスのインスタンスを作成)、ActiveRecord(各モデルが独自のテーブルを持つ)、シングルトンパターンに不可欠です。'static' 戻り値型(PHP 8+)はメソッドが呼び出しクラスのインスタンスを返すことを宣言します。

php
<?php
class Base {
    protected static $table = 'base';

    public static function getTable(): string {
        // self:: refers to the class where the method is DEFINED
        return self::$table;  // always 'base'
    }

    public static function getTableStatic(): string {
        // static:: refers to the class that was CALLED (runtime)
        return static::$table;  // late static binding
    }

    public static function create(): static {
        // 'static' return type + new static() = factory pattern
        return new static();
    }
}

class Child extends Base {
    protected static $table = 'child';
}

echo Child::getTable();        // 'base' (self:: = Base)
echo Child::getTableStatic();  // 'child' (static:: = Child)
echo get_class(Child::create()); // 'Child' (new static = Child)

// self:: is resolved at compile time (the defining class)
// static:: is resolved at runtime (the calling class)
// This is "Late Static Binding" — essential for factory patterns

マジックメソッド

マジックメソッドはオブジェクト操作をインターセプトする特別なメソッドです。__get/__set は動的プロパティを作成します(データ転送オブジェクト、ORM に便利)。__toString は echo $object を可能にします。__invoke はオブジェクトを関数のように呼び出し可能にします。__isset/__unset は動的プロパティで isset()/unset() をサポートします。__debugInfo は var_dump 出力をカスタマイズします。他のマジックメソッド:__construct、__destruct、__clone(ディープクローン用)、__call/__callStatic(未定義メソッド用、流れるような API と mixin を可能に)、__serialize/__unserialize(PHP 7.4+ で __sleep/__wakeup を置き換え)。マジックメソッドは控えめに使用してください — デバッグが難しい「マジック」動作を追加します。明確に文書化してください。

php
<?php
class MagicBox {
    private $data = [];

    // Called when accessing undefined properties
    public function __get($name) {
        return $this->data[$name] ?? null;
    }

    // Called when setting undefined properties
    public function __set($name, $value) {
        $this->data[$name] = $value;
    }

    // Called when isset() or empty() on undefined property
    public function __isset($name): bool {
        return isset($this->data[$name]);
    }

    // Called when unset() on undefined property
    public function __unset($name): void {
        unset($this->data[$name]);
    }

    // Called when object is used as string
    public function __toString(): string {
        return json_encode($this->data);
    }

    // Called when object is called as function
    public function __invoke($arg) {
        return "Called with $arg";
    }

    // Called for var_dump/debugging
    public function __debugInfo(): array {
        return ['keys' => array_keys($this->data)];
    }
}

$box = new MagicBox();
$box->name = "Alice";       // __set
echo $box->name;            // __get → Alice
echo $box;                  // __toString → {"name":"Alice"}
echo $box("test");          // __invoke → Called with test
17

Composer パッケージ管理

composer.json の基礎

composer.json は PHP プロジェクトのマニフェストです。require はバージョン制約付きで本番依存関係をリストします(^ はマイナーアップデートを許可、~ はパッチを許可)。autoload は PSR-4 名前空間からディレクトリへのマッピングを定義します。require-dev は開発専用依存関係を保持します。composer install を実行してプロジェクトをセットアップします。

php
{
    "name": "myorg/myapp",
    "type": "project",
    "require": {
        "php": ">=8.1",
        "monolog/monolog": "^3.0",
        "symfony/console": "^7.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": { "MyApp\\": "src/" }
    }
}

インストールと更新

composer install は正確なバージョンの composer.lock を読み取ります(再現可能なビルド)。composer require はパッケージを追加し依存関係を解決します。composer update は制約内でより新しいバージョンを取得します。本番には --no-dev を使用します。--optimize-autoloader は本番でより高速なオートロードのために PSR-4 をクラスマップに変換します。

php
# Install all dependencies from composer.lock
composer install

# Add a package (modifies composer.json)
composer require monolog/monolog
composer require --dev phpunit/phpunit

# Update all packages to latest allowed versions
composer update

# Update a single package
composer update monolog/monolog

# Production install (no dev, optimized autoloader)
composer install --no-dev --optimize-autoloader

# Show installed packages
composer show

バージョン制約

キャレット(^)は最も一般的な制約です:左端の非ゼロ桁を変更しない変更を許可します。チルダ(~)はパッチレベルに固定します。0.x バージョンの場合、^0.3 は 0.3.x を許可しますが 0.4 は不可です。破壊的変更を避けつつセキュリティパッチを取得するため、常に制約を使用します。再現性のために composer.lock で正確なバージョンを固定します。

php
"require": {
    // Caret: >=1.2.0, <2.0.0 (allows minor+patch)
    "vendor/pkg": "^1.2",

    // Tilde: >=1.2.0, <1.3.0 (patch only)
    "vendor/pkg2": "~1.2",

    // Exact version
    "vendor/pkg3": "1.2.3",

    // Range
    "vendor/pkg4": ">=1.0 <2.0",

    // Wildcard
    "vendor/pkg5": "1.2.*",

    // Stability flags
    "vendor/pkg6": "dev-main",
    "vendor/pkg7": "2.0@beta"
}

PSR-4 オートロード

PSR-4 オートロードは名前空間プレフィックスをディレクトリにマッピングします:MyApp\Services\UserService は src/Services/User.php に解決されます。新しいクラスを追加した後、composer dump-autoload を実行します。本番では、--optimize を使用してクラスマップを生成します(ファイルシステムチェックの代わりに1回の配列ルックアップ)。クラスマップオートロードはディレクトリをスキャンし、固定コードベースに最速です。

php
// PSR-4: "MyApp\\": "src/" means
// MyApp\User -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php

namespace MyApp\Services;

class UserService
{
    public function find(int $id): ?User
    {
        // ...
    }
}

// After running composer dump-autoload:
// require 'vendor/autoload.php';
// $service = new \MyApp\Services\UserService();

// Classmap (faster for production)
// "autoload": { "classmap": ["src/", "lib/"] }

スクリプトとフック

Composer スクリプトはプロジェクト固有のコマンドを定義します。composer <name> で実行します。組み込みイベント(post-install-cmd、post-update-cmd、pre-autoload-dump)が自動的に発火します。スクリプトは @name で他のスクリプトを参照できます。スクリプトを使用してチームメンバー間で開発ワークフローを標準化します。

php
{
    "scripts": {
        "test": "phpunit",
        "lint": "phpcs --standard=PSR12 src/",
        "fix": "phpcbf --standard=PSR12 src/",
        "post-install-cmd": [
            "MyApp\\Setup::postInstall",
            "php artisan migrate"
        ],
        "post-update-cmd": "@post-install-cmd"
    }
}

// Run: composer test, composer lint, etc.
18

高度な cURL

マルチリクエスト(並行)

curl_multi_exec は複数のリクエストを並行実行し、バッチ API 呼び出しの総時間を劇的に削減します。curl_multi_select はアクティビティがあるまでブロックし、ビジーウェイトを回避します。リソースを解放するため、常にハンドルとマルチハンドルを閉じてください。これが高性能 HTTP スクレイピングと API 集計の基盤です。

php
<?php
$urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments',
];

$multi = curl_multi_init();
$handles = [];

foreach ($urls as $i => $url) {
    $handles[$i] = curl_init($url);
    curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
    curl_setopt($handles[$i], CURLOPT_TIMEOUT, 10);
    curl_multi_add_handle($multi, $handles[$i]);
}

do {
    $status = curl_multi_exec($multi, $active);
    if ($active) curl_multi_select($multi);
} while ($active && $status === CURLM_OK);

foreach ($handles as $i => $ch) {
    $responses[$i] = curl_multi_getcontent($ch);
    curl_multi_remove_handle($multi, $ch);
}
curl_multi_close($multi);

ストリーミングレスポンス

CURLOPT_WRITEFUNCTION はレスポンスの各チャンクに対するコールバックを提供し、大きなファイルをメモリに完全に読み込むことなくストリーミング処理を可能にします。チャンク長を返して消費をシグナルします。これは大きなファイルのダウンロード、ストリーミング API の処理、CSV/JSON のインクリメンタル解析に不可欠です。

php
<?php
$ch = curl_init('https://example.com/large-file.csv');
$file = fopen('download.csv', 'w');

curl_setopt($ch, CURLOPT_FILE, $file);  // Write to file
// OR use callback for streaming processing:
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) use ($file) {
    fwrite($file, $chunk);
    // Or parse incrementally:
    // $lines = explode("\n", $chunk);
    return strlen($chunk);  // Must return bytes consumed
});

curl_exec($ch);
fclose($file);
curl_close($ch);

認証と Cookie

認証(Bearer トークン、API キー)のために CURLOPT_HTTPHEADER でカスタムヘッダーを設定します。COOKIEJAR/COOKIEFILE はセッションベース認証のためにリクエスト間で Cookie を永続化します。CURLOPT_USERPWD は HTTP Basic 認証を設定します。POST の場合、CURLOPT_POSTFIELDS に JSON と Content-Type ヘッダーを設定します。レスポンス形式を制御するため常に Accept を設定します。

php
<?php
$ch = curl_init('https://api.example.com/data');

// Bearer token
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $token,
    'Content-Type: application/json',
]);

// Cookie jar (persist across requests)
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');   // Save
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');  // Load

// Basic auth
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');

// POST with JSON body
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['key' => 'value']));

$response = curl_exec($ch);

エラー処理と再試行

常に curl_exec の戻り値(失敗時 false)とメッセージの curl_error を確認してください。curl_getinfo は HTTP ステータスコード、タイミング、リダイレクト情報を提供します。レート制限と一時的な障害を処理するため、再試行に指数バックオフを実装します。ネットワークエラー(curl エラー)と HTTP エラー(ステータスコード)を区別します。

php
<?php
function fetchWithRetry(string $url, int $max = 3): ?string
{
    for ($attempt = 1; $attempt <= $max; $attempt++) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);

        $response = curl_exec($ch);
        $error = curl_error($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($response !== false && $code >= 200 && $code < 300) {
            return $response;
        }

        if ($attempt < $max) {
            sleep(pow(2, $attempt));  // Exponential backoff
        }
    }
    throw new RuntimeException("Failed: {$error}");
}

cURL オプションリファレンス

CURLOPT_FOLLOWLOCATION は HTTP リダイレクト(3xx)に従います。MITM 攻撃を防ぐため本番では常に SSL_VERIFYPEER を true に保ちます;cacert.pem を curl.haxx.se からダウンロードします。CURLOPT_ENCODING は圧縮を有効にします。接続問題のデバッグには STDERR 付きの CURLOPT_VERBOSE を使用します。ハングを避けるため合理的なタイムアウトを設定します。

php
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);  // Return not echo
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);   // Follow redirects
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);

// SSL (keep VERIFYPEER true in production!)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');

// Performance
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_ENCODING, 'gzip');

// Debug
curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLOPT_STDERR, fopen('curl.log', 'w'));
19

画像処理(GD)

画像の作成と読み込み

imagecreatetruecolor はトゥルーカラー画像(数百万色)を作成します。imagecolorallocate は色を登録し識別子を返します。imagecreatefromjpeg/png/webp は既存ファイルを読み込みます。常に戻り値を確認してください(失敗時 false)。寸法を取得するには imagesx/imagesy を使用します。完了時に imagedestroy でメモリを解放します。

php
<?php
// Create a blank image
$img = imagecreatetruecolor(400, 300);

// Allocate colors
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);

// Fill background
imagefill($img, 0, 0, $white);

// Load existing images
$photo = imagecreatefromjpeg('photo.jpg');
$png = imagecreatefrompng('logo.png');
$webp = imagecreatefromwebp('image.webp');

// Get dimensions
$width = imagesx($photo);
$height = imagesy($photo);

図形とテキストの描画

GD は描画プリミティブを提供します:長方形、楕円、線、ポリゴン、円弧。塗りつぶしバリアント(imagefilled*)は塗りつぶし図形を描画します。imagettftext は角度とサイズ制御付きで TrueType フォントをレンダリングします。画像データを出力する前に常に Content-Type ヘッダーを送信してください。メモリを解放するため imagedestroy を呼び出します。

php
<?php
$img = imagecreatetruecolor(400, 300);
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);
$blue = imagecolorallocate($img, 0, 0, 255);
imagefill($img, 0, 0, $white);

// Shapes
imagerectangle($img, 50, 50, 150, 120, $red);
imagefilledrectangle($img, 200, 50, 300, 120, $blue);
imageellipse($img, 100, 200, 80, 80, $red);
imageline($img, 0, 0, 400, 300, $red);

// Text with TrueType font
imagettftext($img, 20, 0, 50, 270, $red, 'arial.ttf', 'Hello GD!');

header('Content-Type: image/png');
imagepng($img);
imagedestroy($img);

リサイズとクロップ

imagecopyresampled は imagecopyresized より高品質な結果を生成します(補間を使用)。元から寸法を計算してアスペクト比を維持します。サムネイルには、一貫したレイアウトのために正方形にセンタークロップします。バッチ処理でメモリリークを防ぐため、コピー後に常にソース画像を破棄してください。

php
<?php
function resizeImage($src, $maxW, $maxH) {
    list($w, $h) = getimagesize($src);
    $ratio = min($maxW / $w, $maxH / $h);
    $newW = (int)($w * $ratio);
    $newH = (int)($h * $ratio);

    $srcImg = imagecreatefromjpeg($src);
    $dstImg = imagecreatetruecolor($newW, $newH);

    // High-quality resampling
    imagecopyresampled($dstImg, $srcImg, 0, 0, 0, 0,
        $newW, $newH, $w, $h);

    imagedestroy($srcImg);
    return $dstImg;
}

// Center crop to square
function cropSquare($src, $size) {
    list($w, $h) = getimagesize($src);
    $min = min($w, $h);
    $x = (int)(($w - $min) / 2);
    $y = (int)(($h - $min) / 2);
    $dst = imagecreatetruecolor($size, $size);
    $img = imagecreatefromjpeg($src);
    imagecopyresampled($dst, $img, 0, 0, $x, $y, $size, $size, $min, $min);
    return $dst;
}

フィルターと効果

imagefilter は組み込み効果を適用します:グレースケール、明度(-255 から 255 の範囲)、コントラスト(負で増加)、ぼかし、エッジ検出、ネガ反転、カラー化(RGB + アルファ)。Pixelate はモザイク効果を作成します。これらは高速ですが基本的です;高度な効果には、畳み込み行列とカスタムフィルターをサポートする ImageMagick(Imagick 拡張)を使用します。

php
<?php
$img = imagecreatefromjpeg('photo.jpg');

// Built-in filters
imagefilter($img, IMG_FILTER_GRAYSCALE);      // B&W
imagefilter($img, IMG_FILTER_BRIGHTNESS, 30);  // Brighten
imagefilter($img, IMG_FILTER_CONTRAST, -20);   // More contrast
imagefilter($img, IMG_FILTER_GAUSSIAN_BLUR);   // Blur
imagefilter($img, IMG_FILTER_EDGEDETECT);      // Edge detection
imagefilter($img, IMG_FILTER_NEGATE);          // Invert colors

// Colorize (tint)
imagefilter($img, IMG_FILTER_COLORIZE, 0, 0, 100, 0);  // Blue tint

// Pixelate
imagefilter($img, IMG_FILTER_PIXELATE, 10, true);

imagepng($img, 'filtered.png');
imagedestroy($img);

ウォーターマークと合成

imagecopymerge は調整可能な不透明度(0-100)で一方の画像をもう一方に重ねます。アルファチャネル付きの PNG ウォーターマークは自然にブレンドされます。テキストウォーターマークには、半透明テキストに imagecolorallocatealpha を使用します。imagejpeg の品質は 0(最悪)から 100(最高);75-90 が Web に良いバランスです。両方の画像を常に破棄してください。

php
<?php
$photo = imagecreatefromjpeg('photo.jpg');
$watermark = imagecreatefrompng('logo.png');

$pw = imagesx($photo); $ph = imagesy($photo);
$ww = imagesx($watermark); $wh = imagesy($watermark);

// Position: bottom-right with 20px padding
$destX = $pw - $ww - 20;
$destY = $ph - $wh - 20;

// Merge with 50% opacity
imagecopymerge($photo, $watermark, $destX, $destY, 0, 0, $ww, $wh, 50);

// Text watermark
$color = imagecolorallocatealpha($photo, 255, 255, 255, 60);
imagettftext($photo, 30, 0, 20, $ph - 20, $color, 'arial.ttf', '© 2025');

imagejpeg($photo, 'watermarked.jpg', 90);  // 90% quality
imagedestroy($photo);
imagedestroy($watermark);
20

セッションと Cookie の深掘り

セッションセキュリティ

セキュアなセッションには以下が必要です:HttpOnly Cookie(JavaScript アクセスなし)、Secure フラグ(HTTPS のみ)、SameSite=Strict(CSRF 保護)、厳密モード(未初期化セッション ID を拒否)。セッション固定を防ぐため、権限変更(ログイン、管理者アクセス)後に常にセッション ID を再生成します。PHP を明らかにしないため、カスタムセッション名を使用します。

php
<?php
// php.ini or runtime configuration
ini_set('session.cookie_httponly', 1);    // No JS access
ini_set('session.cookie_secure', 1);       // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);     // Reject uninitialized IDs
ini_set('session.gc_maxlifetime', 3600);   // 1 hour

session_name('APP_SID');  // Custom name
session_start();

// Regenerate ID after login (prevent fixation)
session_regenerate_id(true);

$_SESSION['user_id'] = 123;
$_SESSION['login_time'] = time();

カスタムセッションハンドラー

カスタムセッションハンドラーはファイルの代わりにデータベース、Redis、Memcached にセッションデータを保存します。open、close、read、write、destroy、gc メソッドで SessionHandlerInterface を実装します。データベースストレージは複数サーバー間でのセッション共有を可能にします(ロードバランシング)。セッション ID で SQL インジェクションを防ぐため、常にパラメータ化クエリを使用します。

php
<?php
class DbSessionHandler implements SessionHandlerInterface
{
    private PDO $pdo;
    public function __construct(PDO $pdo) { $this->pdo = $pdo; }

    public function open($path, $name): bool { return true; }
    public function close(): bool { return true; }

    public function read($id): string|false {
        $stmt = $this->pdo->prepare(
            'SELECT data FROM sessions WHERE id = ? AND expires > ?'
        );
        $stmt->execute([$id, time()]);
        return $stmt->fetchColumn() ?: '';
    }

    public function write($id, $data): bool {
        $exp = time() + (int)ini_get('session.gc_maxlifetime');
        return $this->pdo->prepare(
            'REPLACE INTO sessions (id, data, expires) VALUES (?, ?, ?)'
        )->execute([$id, $data, $exp]);
    }

    public function destroy($id): bool {
        return $this->pdo->prepare('DELETE FROM sessions WHERE id = ?')
            ->execute([$id]);
    }

    public function gc($max): int|false {
        return $this->pdo->prepare('DELETE FROM sessions WHERE expires < ?')
            ->execute([time()]);
    }
}

session_set_save_handler(new DbSessionHandler($pdo), true);
session_start();

Cookie 管理

明確さと SameSite 設定のために setcookie のオプション配列形式(PHP 7.3+)を使用します。セキュア Cookie には HTTPS が必要です。HttpOnly は XSS ベースの Cookie 盗難を防ぎます。SameSite=Lax はクロスサイト POST をブロックします(ほとんどの CSRF 保護に十分);Strict はすべてのクロスサイトリクエストをブロックします。同じパス/ドメインで過去の有効期限を設定して Cookie を削除します。

php
<?php
// Set a cookie with all security options
setcookie('preferences', json_encode(['theme' => 'dark']), [
    'expires' => time() + 86400 * 30,  // 30 days
    'path' => '/',
    'domain' => '.example.com',
    'secure' => true,                   // HTTPS only
    'httponly' => true,                 // No JavaScript access
    'samesite' => 'Lax'                 // CSRF protection
]);

// Read cookies
$theme = $_COOKIE['preferences'] ?? 'default';

// Delete a cookie (set expiration in the past)
setcookie('preferences', '', [
    'expires' => time() - 3600,
    'path' => '/',
]);

フラッシュメッセージ

フラッシュメッセージはセッションに一回限り通知を保存し、リダイレクト後に表示します(Post/Redirect/Get パターン)。メッセージはリダイレクト前に設定され、表示後にクリアされます。これにより再送信警告を防ぎ、UI をクリーンに保ちます。複数メッセージのために配列として保存します。古い表示を防ぐため、読み取り後すぐにクリアします。

php
<?php
// Set a flash message (one-time notification)
function flash(string $key, string $message): void {
    $_SESSION['_flash'][$key] = $message;
}

// Get and clear flash message
function getFlash(string $key): ?string {
    $msg = $_SESSION['_flash'][$key] ?? null;
    unset($_SESSION['_flash'][$key]);
    return $msg;
}

// Usage in controller
flash('success', 'Item saved!');
header('Location: /items');
exit;

// In view after redirect
if ($msg = getFlash('success')) {
    echo "<div class='alert'>$msg</div>";
}

JWT 認証

JWT はステートレス認証を可能にします:サーバーはセッションデータを保存せず、API とマイクロサービスに最適です。トークンは秘密で署名されたクレーム(ユーザー ID、ロール、有効期限)を含みます。トレードオフ:トークンは簡単に取り消せません(短い有効期限 + リフレッシュトークンを使用)、リクエストサイズが増加します。XSS トークン盗難を防ぐため HttpOnly Cookie を使用します。

php
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

// Generate token on login
$payload = [
    'user_id' => 123,
    'role' => 'admin',
    'iat' => time(),           // Issued at
    'exp' => time() + 3600,    // Expires in 1 hour
];
$token = JWT::encode($payload, $secretKey, 'HS256');

// Send to client (cookie or Authorization header)
setcookie('auth_token', $token, [
    'expires' => time() + 3600,
    'httponly' => true,
    'secure' => true,
    'samesite' => 'Lax',
]);

// Verify on each request
try {
    $decoded = JWT::decode(
        $_COOKIE['auth_token'],
        new Key($secretKey, 'HS256')
    );
    $userId = $decoded->user_id;
} catch (Exception $e) {
    http_response_code(401);
    exit('Unauthorized');
}
21

REST API の深掘り

ルーティングとリクエスト処理

REST API は HTTP メソッドを CRUD 操作にマッピングします:GET(読み取り)、POST(作成)、PUT/PATCH(更新)、DELETE(削除)。リソース識別のために URL パスを解析します。POST/PUT のリクエストボディは php://input から読み取ります。常に適切な HTTP ステータスコード(200、201、400、404、500)と Content-Type ヘッダー付きの JSON レスポンスを返します。

php
<?php
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($path, '/'));

switch (true) {
    case $method === 'GET' && $segments[0] === 'users':
        if (isset($segments[1])) getUser((int)$segments[1]);
        else listUsers();
        break;

    case $method === 'POST' && $segments[0] === 'users':
        $data = json_decode(file_get_contents('php://input'), true);
        createUser($data);
        break;

    case $method === 'PUT' && $segments[0] === 'users' && isset($segments[1]):
        $data = json_decode(file_get_contents('php://input'), true);
        updateUser((int)$segments[1], $data);
        break;

    default:
        http_response_code(404);
        echo json_encode(['error' => 'Not Found']);
}

レスポンスとステータスコード

API レスポンスには常に Content-Type: application/json を設定します。正しいステータスコードを使用:200(OK)、201(作成済み)、204(コンテンツなし)、400(不正リクエスト)、401(未認証)、403(禁止)、404(見つからない)、422(処理不可能エンティティ)、429(リクエスト多すぎ)、500(サーバーエラー)。デバッグのためにエラー詳細を含めますが、本番ではスタックトレースを絶対に公開しないでください。

php
<?php
function jsonResponse($data, int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json');
    echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    exit;
}

// Success responses
jsonResponse(['data' => $users], 200);          // OK
jsonResponse(['data' => $user], 201);           // Created

// Error responses
jsonResponse(['error' => 'Validation failed'], 400);
jsonResponse(['error' => 'Unauthorized'], 401);
jsonResponse(['error' => 'Forbidden'], 403);
jsonResponse(['error' => 'Not Found'], 404);
jsonResponse(['error' => 'Server error'], 500);

ページネーションとフィルタリング

LIMIT/OFFSET でページネーションを実装し、メタデータ(合計、現在ページ、総ページ数)を返します。SQL インジェクションを防ぐため、ソート列をホワイトリストに対して検証してサニタイズします。過剰なクエリを防ぐため per_page に上限を設定します。検索にはワイルドカード付きの LIKE を使用します。データとは別の meta オブジェクトにページネーションメタデータを返します。

php
<?php
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = min(100, max(1, (int)($_GET['per_page'] ?? 20)));
$offset = ($page - 1) * $perPage;

// Whitelist sort columns (prevent SQL injection)
$sort = in_array($_GET['sort'] ?? '', ['name', 'email'])
    ? $_GET['sort'] : 'id';
$order = strtolower($_GET['order'] ?? 'asc') === 'desc' ? 'DESC' : 'ASC';

$sql = "SELECT * FROM users ORDER BY $sort $order LIMIT ? OFFSET ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$perPage, $offset]);
$users = $stmt->fetchAll();

// Total count for pagination metadata
$total = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();

jsonResponse([
    'data' => $users,
    'meta' => [
        'page' => $page,
        'per_page' => $perPage,
        'total' => $total,
        'total_pages' => ceil($total / $perPage),
    ],
]);

レート制限

レート制限は API 悪用を防ぎます。固定ウィンドウ(シンプル)またはスライディングウィンドウ(より正確)アルゴリズムを使用します。分散システムのために Redis にカウンタを保存します。クライアントが自己調整できるよう X-RateLimit ヘッダー(Limit、Remaining、Reset)を返します。HTTP 429 と Retry-After でクライアントに再試行タイミングを伝えます。本番では、Redis または専用レートリミッターを使用します。

php
<?php
function checkRateLimit(int $userId, int $max = 100, int $window = 3600): bool
{
    $file = sys_get_temp_dir() . "/rate_{$userId}_" . floor(time() / $window);
    $count = file_exists($file) ? (int)file_get_contents($file) : 0;

    if ($count >= $max) {
        $reset = (floor(time() / $window) + 1) * $window;
        header("X-RateLimit-Limit: $max");
        header("X-RateLimit-Remaining: 0");
        header("Retry-After: " . ($reset - time()));
        http_response_code(429);
        echo json_encode(['error' => 'Rate limit exceeded']);
        return false;
    }

    file_put_contents($file, $count + 1);
    header("X-RateLimit-Remaining: " . ($max - $count - 1));
    return true;
}

if (!checkRateLimit($userId)) exit;

API バージョニング

API バージョニング戦略:URL プレフィックス(/v1/)が最も明示的でキャッシュフレンドリ;Accept ヘッダーは RESTful ですがテストが困難です。OpenAPI(Swagger)アノテーションで API を文書化します。swagger-php のようなツールでインタラクティブなドキュメントを生成します。最初からバージョニングします;破壊的変更には新しいバージョンが必要です。Sunset ヘッダーで古いバージョンを非推奨にします。

php
<?php
// Versioning via URL prefix: /v1/users, /v2/users
$version = $segments[0] ?? 'v1';

// Versioning via Accept header
// Accept: application/vnd.myapp.v2+json
preg_match('/vnd\.myapp\.(v\d+)\+json/',
    $_SERVER['HTTP_ACCEPT'] ?? '', $m);
$version = $m[1] ?? 'v1';

// OpenAPI/Swagger documentation
/**
 * @OA\Get(path="/api/users",
 *   @OA\Response(response=200, description="List users")
 * )
 */
22

セキュリティの深掘り(XSS/CSRF)

XSS 防止

XSS(クロスサイトスクリプティング)は悪意のあるスクリプトを Web ページに注入します。コンテキストに基づいて出力をエンコードして防ぎます:HTML には htmlspecialchars、JavaScript には json_encode、URL には urlencode。ENT_QUOTES は単一引用符と二重引用符の両方をエスケープします。Content-Security-Policy(CSP)はスクリプトソースを制限して防御を深化します。ユーザー入力を絶対に信頼しないでください。

php
<?php
// Output encoding (prevents XSS)
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// JavaScript string (use json_encode)
echo '<script>var name = ' . json_encode($name) . ';</script>';

// URL parameter
echo 'redirect=' . urlencode($url);

// Content-Security-Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

// Disable inline scripts
header("X-XSS-Protection: 1; mode=block");

CSRF 保護

CSRF(クロスサイトリクエストフォージェリ)はユーザーを騙して望まないアクションを送信させます。アンチ CSRF トークンで防ぎます:セッションごとにランダムトークンを生成し、フォームに隠しフィールドとして含め、POST/PUT/DELETE で検証します。タイミングセーフな比較には hash_equals を使用します。AJAX では、カスタムヘッダーでトークンを送信します。SameSite=Strict Cookie が追加の保護を提供します。

php
<?php
// Generate CSRF token
function csrfToken(): string {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

// Verify CSRF token (timing-safe)
function verifyCsrf(string $token): bool {
    return !empty($_SESSION['csrf_token'])
        && hash_equals($_SESSION['csrf_token'], $token);
}

// In the form
echo '<input type="hidden" name="csrf_token" value="' . csrfToken() . '">';

// On POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!verifyCsrf($_POST['csrf_token'] ?? '')) {
        http_response_code(403);
        die('CSRF token validation failed');
    }
}

SQL インジェクション防止

SQL インジェクションは攻撃者が任意の SQL を実行できるようにします。常にパラメータ化クエリ付きのプリペアドステートメントを使用します:データベースが SQL ロジックとデータを分離し、インジェクションを不可能にします。ユーザー入力を SQL 文字列に連結しないでください。動的クエリ(IN 句、ORDER BY)には、プレースホルダで SQL 構造を構築し、値をパラメータとして渡します。

php
<?php
// BAD: string concatenation (SQL injection vulnerable)
$sql = "SELECT * FROM users WHERE name = '" . $_GET['name'] . "'";
// Attack: ?name=' OR '1'='1

// GOOD: prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = ? AND status = ?');
$stmt->execute([$_GET['name'], 'active']);
$users = $stmt->fetchAll();

// Named parameters
$stmt = $pdo->prepare(
    'INSERT INTO users (name, email) VALUES (:name, :email)'
);
$stmt->execute([':name' => $name, ':email' => $email]);

// Dynamic IN clause (still safe)
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);

パスワードハッシュ

平文パスワードを絶対に保存しないでください。password_hash は自動ソルト生成付きの bcrypt(または利用可能な場合は Argon2)を使用します。ハッシュにはアルゴリズム、コスト、ソルトが含まれるため、password_verify は任意のフォーマットに対してチェックできます。コスト係数を増やしたりアルゴリズムを切り替えた時に password_needs_rehash でハッシュをアップグレードできます。新しいアプリケーションには Argon2 が推奨されます。

php
<?php
// Hash a password (uses bcrypt by default, auto-salts)
$hash = password_hash('myPassword123', PASSWORD_DEFAULT);
// $2y$10$... (bcrypt with cost 10)

// Verify a password
if (password_verify($inputPassword, $storedHash)) {
    // Check if rehash is needed (algorithm upgrade)
    if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
        $newHash = password_hash($inputPassword, PASSWORD_DEFAULT);
        // Update stored hash in database
    }
    // Log user in
}

// Argon2 (PHP 7.2+, requires libsodium)
$hash = password_hash('password', PASSWORD_ARGON2ID, [
    'memory_cost' => 65536,  // 64 MB
    'time_cost'   => 4,      // iterations
    'threads'     => 2,
]);

入力検証

常にサーバー側で入力を検証します(クライアント側検証は UX のみ)。型チェックには FILTER_VALIDATE_* 付きの filter_input を、クリーニングには FILTER_SANITIZE_* を使用します。カスタムルールには、正規表現または専用検証ライブラリ(Respect/Validation、Symfony Validator)を使用します。ホワイトリストアプローチを使用:既知のフィールドのみを受け入れ、それ以外はすべて拒否します。

php
<?php
// Validate email
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false) {
    $errors[] = 'Invalid email';
}

// Validate integer with range
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 1, 'max_range' => 150]
]);

// Validate URL
$url = filter_input(INPUT_POST, 'website', FILTER_VALIDATE_URL);

// Custom validation with regex
function validateUsername(string $u): ?string {
    if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $u)) {
        return 'Username must be 3-20 alphanumeric chars';
    }
    return null;
}

// Whitelist approach for arrays
$allowed = ['name', 'email', 'age'];
$input = array_intersect_key($_POST, array_flip($allowed));
23

名前空間とオートロードの深掘り

名前空間宣言

名前空間はクラス名の衝突を防ぎ、コードを階層的に整理します。namespace 宣言は最初の文でなければなりません。use はクラス、関数、定数をインポートします。エイリアス(as)が衝突を解決します。PHP の名前空間付けはバックスラッシュを使用します。PSR-4 標準は名前空間セパレータをディレクトリセパレータにマッピングします:MyApp\Services\UserService -> src/Services/UserService.php。

php
<?php
// File: src/Services/UserService.php
namespace MyApp\Services;

use MyApp\Models\User;
use MyApp\Repositories\UserRepository;
use MyApp\Exceptions\NotFoundException;

class UserService
{
    public function __construct(
        private UserRepository $repo
    ) {}

    public function find(int $id): User
    {
        $user = $this->repo->findById($id);
        if (!$user) {
            throw new NotFoundException("User {$id} not found");
        }
        return $user;
    }
}

PSR-4 オートロード

PSR-4 は標準のオートロード仕様です:名前空間プレフィックスがベースディレクトリにマッピングされ、各名前空間セパレータがディレクトリセパレータになります。Composer がクラス名をファイルパスに自動的に解決するオートローダーを生成します。新しいクラスを追加した後、composer dump-autoload を実行します。オートローダーはクラスが最初に参照された時にのみロードします(遅延読み込み)。

php
// composer.json
{
    "autoload": {
        "psr-4": {
            "MyApp\\": "src/",
            "Tests\\": "tests/"
        }
    }
}

// After composer install:
require __DIR__ . '/vendor/autoload.php';

// Now all classes auto-load:
use MyApp\Services\UserService;
use MyApp\Controllers\UserController;

// PSR-4 rules:
// MyApp\User          -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php
// MyApp\Models\User\Profile -> src/Models/User/Profile.php

カスタムオートローダー

spl_autoload_register はオートローダースタックに機能を追加します。クラスが参照されたがロードされていない時、PHP は1つがクラスをロードするまで登録された各オートローダーを順番に呼び出します。複数のオートローダーが共存できます(例:1つは PSR-4 用、1つはレガシークラス用)。エラーを避けるため、require する前にファイルが存在するか常に確認します。Composer はこのメカニズムを内部で使用します。

php
<?php
spl_autoload_register(function (string $class): void {
    $prefix = 'MyApp\\';
    $baseDir = __DIR__ . '/src/';

    // Check if class uses our namespace prefix
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        return;  // Not our class
    }

    // Get relative class name
    $relativeClass = substr($class, $len);

    // Replace namespace separators with directory separators
    $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';

    if (file_exists($file)) {
        require $file;
    }
});

// Multiple autoloaders can be registered (stack-based)
spl_autoload_register(function ($class) {
    $path = __DIR__ . '/legacy/' . $class . '.php';
    if (file_exists($path)) require $path;
});

クラスマップとファイルオートロード

クラスマップオートロードは dump-autoload 時にディレクトリをスキャンし、クラス名からファイルパスへのマッピング配列を構築します。これは PSR-4 より高速(ファイルシステムチェックの代わりに1回の配列ルックアップ)で本番に推奨されます。files オートロードは毎リクエストで特定のファイルを読み込み、クラスとしてオートロードできないヘルパー関数と定数に便利です。本番クラスマップには --optimize を使用します。

php
{
    "autoload": {
        "psr-4": { "MyApp\\": "src/" },
        "classmap": ["src/Legacy/", "lib/OldClasses.php"],
        "files": ["src/helpers.php", "src/constants.php"]
    }
}

// classmap: scans directories and builds class-to-file map
// Faster than PSR-4 for production (no filesystem checks)

// files: always-loaded files (for functions and constants)
// src/helpers.php:
<?php
function dd($var) { var_dump($var); die; }
function env(string $key, $default = null) { /* ... */ }

名前空間の解決

名前空間コードでは、非修飾クラス名はまずインポートを通じて、次に現在の名前空間で解決されます。組み込みクラス(DateTime、PDO、Exception)はグローバル名前空間に存在;先頭のバックスラッシュで参照するかインポートします。関数と定数はローカルに見つからない場合グローバル名前空間にフォールバックします。絶対参照には FQCN(先頭のバックスラッシュ)を使用します。

php
<?php
namespace MyApp\Services;

use MyApp\Models\User;

class UserService
{
    public function create(): User
    {
        // User resolves to MyApp\Models\User (imported)
        return new User();
    }

    public function find(): \MyApp\Models\User
    {
        // Fully Qualified Class Name (leading backslash)
        return new \MyApp\Models\User();
    }

    public function date(): \DateTime
    {
        // Global classes need backslash or import
        return new \DateTime();
    }
}
24

ジェネレータと Yield

基本的なジェネレータ

ジェネレータは yield で値を遅延生成し、コレクション全体をメモリに構築せずに一度に1つずつ生成します。これは大きなシーケンスや無限シーケンスにメモリ効率が良いです。関数は Iterator を実装する Generator オブジェクトを返します。各 yield は実行を一時停止し、次の反復で再開します。大きなファイルの読み込み、データベースカーソル、計算シーケンスにジェネレータを使用します。

php
<?php
function rangeGen(int $start, int $end, int $step = 1): Generator
{
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i;
    }
}

// Iterate without loading all values into memory
foreach (rangeGen(1, 5) as $value) {
    echo $value . ' ';  // 1 2 3 4 5
}

// Memory efficient for large sequences
foreach (rangeGen(1, 1000000) as $value) {
    if ($value % 100000 === 0) echo "Reached {$value}\n";
}

Yield キー値ペア

ジェネレータは連想配列のように yield key => value 構文でキー値ペアを yield できます。これにより変換を通じてキーが保持されます。値をフィルタするには、単にそれらを yield しないようにします。Generator は反復内の位置を維持するため、各ジェネレータがストリームを変換またはフィルタするパイプラインスタイルの処理を構築できます。

php
<?php
function mapGen(array $data): Generator
{
    foreach ($data as $key => $value) {
        yield $key => strtoupper($value);
    }
}

foreach (mapGen(['a' => 'hello', 'b' => 'world']) as $key => $value) {
    echo "{$key} => {$value}\n";
}
// a => HELLO
// b => WORLD

// Filter by not yielding unwanted values
function filterGen(array $data): Generator
{
    foreach ($data as $value) {
        if ($value > 0) yield $value;
    }
}

ジェネレータへの値の送信

send() メソッドはジェネレータに値を渡し、それが yield 式の結果になります。これにより双方向通信が可能になり、コルーチンとステートマシンに便利です。current() はジェネレータを開始します。getReturn() はジェネレータ完了後に戻り値を取得します。finally ブロックはジェネレータが破棄された時に実行され、リソースクリーンアップを可能にします。

php
<?php
function accumulator(): Generator
{
    $total = 0;
    while (true) {
        $value = yield $total;
        if ($value === null) break;
        $total += $value;
    }
    return $total;
}

$gen = accumulator();
$gen->current();  // Start: 0
$gen->send(10);   // Returns 10
$gen->send(20);   // Returns 30
$gen->send(5);    // Returns 35
$gen->send(null); // Triggers return
echo $gen->getReturn();  // 35

Yield from(委譲)

yield from は別のジェネレータ、配列、または Traversable に委譲し、その値を外側のジェネレータに平坦化します。内部ジェネレータの戻り値は外側のジェネレータで利用可能です。これにより合成が可能になります:シンプルなジェネレータから複雑なパイプラインを構築します。yield from は手動で反復して再 yield するより効率的です。

php
<?php
function innerGen(): Generator
{
    yield 1;
    yield 2;
    return 'inner done';
}

function outerGen(): Generator
{
    yield 0;
    $result = yield from innerGen();
    echo "Inner returned: {$result}\n";
    yield 3;
}

foreach (outerGen() as $value) {
    echo $value . ' ';  // 0 1 2 3
}
// Inner returned: inner done

// Yield from arrays
function mixedGen(): Generator {
    yield from [10, 20, 30];
    yield from new ArrayObject([40, 50]);
}

実用的なユースケース

ジェネレータは大きなデータストリームや無限データストリームの処理に優れています:ファイルの行ごとの読み込み、データベースカーソル反復、ページネーションされた API フェッチ、数学的シーケンス。take() パターンは無限ジェネレータを制限します。ジェネレータはうまく合成されます:フィルタ、マップ、リデュースのために複数のジェネレータを通じてデータをパイプします。データサイズに関わらずメモリは一定です。

php
<?php
// 1. Read large files line by line (memory efficient)
function readLines(string $file): Generator {
    $handle = fopen($file, 'r');
    while (!feof($handle)) {
        $line = fgets($handle);
        if ($line !== false) yield rtrim($line);
    }
    fclose($handle);
}

foreach (readLines('large.log') as $line) {
    if (str_contains($line, 'ERROR')) {
        echo "{$line}\n";
    }
}

// 2. Infinite sequence with take()
function naturals(): Generator {
    $n = 1;
    while (true) yield $n++;
}

function take(Generator $gen, int $n): Generator {
    for ($i = 0; $i < $n; $i++) {
        yield $gen->current();
        $gen->next();
    }
}

foreach (take(naturals(), 5) as $num) {
    echo $num . ' ';  // 1 2 3 4 5
}
25

セキュリティ

SQL インジェクション防止

SQL インジェクションはユーザー入力が SQL に連結された時に発生します。常にパラメータ化クエリ付きのプリペアドステートメントを使用します。PDO と MySQLi の両方がサポートしています。ユーザー入力を絶対に信頼しないでください。すべての外部データを検証してサニタイズします。

php
// BAD: vulnerable
$sql = "SELECT * FROM users WHERE name = '" . $_POST['name'] . "'";
// GOOD: prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute([':name' => $_POST['name']]);
$users = $stmt->fetchAll();

XSS 防止

XSS(クロスサイトスクリプティング)は悪意のあるスクリプトを注入します。htmlspecialchars は特殊文字を HTML エンティティに変換します。ENT_QUOTES は単一引用符と二重引用符の両方をエスケープします。ユーザーデータを HTML に出力する時に常にエスケープします。防御の深化のために Content-Security-Policy ヘッダーを使用します。

php
// BAD: output without escaping
echo $_GET['name'];
// GOOD: escape output
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
// For HTML attributes
echo 'value="' . htmlspecialchars($value, ENT_QUOTES) . '"';

パスワードハッシュ

password_hash は自動ソルト生成付きの bcrypt(または argon2)を使用します。パスワードに md5 や sha1 を絶対に使用しないでください。password_verify はパスワードをハッシュに対してチェックします。password_needs_rehash はハッシュアルゴリズムのアップグレードを可能にします。ソルトはハッシュ文字列に埋め込まれています。

php
// Hash a password
$hash = password_hash('mypassword', PASSWORD_DEFAULT);
// Verify
if (password_verify('mypassword', $hash)) {
    echo 'Valid password';
}
// Check if needs rehash
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash('mypassword', PASSWORD_DEFAULT);
}

CSRF 保護

CSRF(クロスサイトリクエストフォージェリ)はユーザーを望まないアクションに騙します。セッションごとにランダムトークンを生成します。フォームに隠しフィールドとして含めます。hash_equals(タイミングセーフな比較)を使用して POST で検証します。SameSite Cookie が追加の保護を提供します。

php
session_start();
if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
// In form
echo '<input type="hidden" name="token" value="' . $_SESSION['token'] . '">';
// Verify
if (!hash_equals($_SESSION['token'], $_POST['token'] ?? '')) {
    die('CSRF token mismatch');
}

セッションセキュリティ

cookie_httponly は JavaScript アクセスを防ぎます。cookie_secure は HTTPS のみを保証します。samesite=Strict は CSRF を防ぎます。use_strict_mode は未初期化セッション ID を拒否します。session_regenerate_id はセッション固定を防ぎます。権限変更後に常に再生成してください。

php
// Secure session settings
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);  // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
session_start();
// Regenerate ID after login
session_regenerate_id(true);

Was this helpful?