PHPにおけるバッチ処理とは、主にCLI(コマンドライン)から実行するPHPスクリプトを使って、定期実行・大量データ処理・裏側の自動処理を行う仕組みです。
PHPはWebアプリ用の言語という印象が強いですが、公式にCLI実行がサポートされており、コマンドとしてスクリプトを動かせます。
典型例としては、次のような処理があります。
- 毎日深夜の売上集計
- CSVの一括取り込み
- 会員向けメールの一括送信
- 外部APIからの定期データ取得
- 古いログや一時ファイルの削除
- 在庫やレポートデータの更新
つまりバッチ処理は、人がブラウザを開かなくても、自動でまとまった処理を進めるための実装です。
Web処理との違い
PHPには大きく分けて、Web経由で動く処理とCLIで動く処理があります。
Web処理
Webサーバー経由でHTTPリクエストを受けたときに実行される処理です。
ページ表示、フォーム送信、検索、ログインなどがこれにあたります。
バッチ処理
CLIから php script.php のように起動する処理です。
HTTPリクエストとは切り離されており、定期実行や大量処理に向いています。
PHP公式もCLI SAPIでの実行を案内しています。
たとえば次のように実行します。
php batch.php
なぜバッチ処理を使うのか
重い処理をすべてWebリクエスト内で実行すると、次のような問題が起こりやすくなります。
- ページ表示が遅くなる
- タイムアウトしやすい
- 同時アクセス時に不安定になる
- ユーザー操作と重い処理が密結合になる
そのため、重い処理・定期処理・大量更新処理はWebから分離してバッチ化するのが一般的です。
絶対に分離しなければならないわけではありませんが、安定運用の観点では非常に有効です。
PHPバッチ処理の基本形
最も単純な例は次のようなものです。
<?php
echo "バッチ開始\n";
echo "実行時刻: " . date('Y-m-d H:i:s') . "\n";
for ($i = 1; $i <= 5; $i++) {
echo "処理中: {$i}\n";
sleep(1);
}
echo "バッチ終了\n";
これを sample_batch.php として保存し、CLIから実行します。
php sample_batch.php
このように、バッチ処理ではHTMLではなく、標準出力への文字列出力やログ出力を使うのが基本です。
CLIで動くことを前提にした設計
バッチ処理は通常CLIで実行されるため、Web処理とは設計の前提が少し異なります。
CLI専用チェック
PHPでは php_sapi_name() や PHP_SAPI により、現在の実行環境を判定できます。
CLI判定に使う方法自体は公式に案内されています。
<?php
if (PHP_SAPI !== 'cli') {
exit("このスクリプトはCLI専用です。\n");
}
ただし、環境によっては phpdbg を許容したいケースもあるため、より広めに判定するならこう書くこともあります。
<?php
if (!in_array(PHP_SAPI, ['cli', 'phpdbg'], true)) {
exit("CLI系SAPI専用です。\n");
}
前者でも多くの現場では十分ですが、後者のほうが少し厳密です。
入力はHTTPではなく引数で受ける
CLI実行では、通常のHTTPリクエスト由来の $_GET や $_POST を前提にしません。
その代わりに、コマンドライン引数や設定ファイル、環境変数などで制御します。
CLI用の引数は $argv から取得できます。
<?php
$targetDate = $argv[1] ?? date('Y-m-d', strtotime('-1 day'));
echo "対象日: {$targetDate}\n";
$argv はCLI引数の配列で、通常 $argv[0] にはスクリプト名が入ります。
なお、PHPの設定次第では register_argc_argv に依存する点は把握しておいたほうが安全です。
オプションを扱うなら getopt()
簡単なCLIオプション処理なら getopt() が使えます。
PHP公式にも用意されています。
<?php
$options = getopt('', ['date:', 'dry-run']);
$date = $options['date'] ?? date('Y-m-d', strtotime('-1 day'));
$isDryRun = isset($options['dry-run']);
echo "date={$date}\n";
echo $isDryRun ? "dry-runです\n" : "本実行です\n";
実行例
php script.php --date=2026-04-10 --dry-run
ただし getopt() には、最初の非オプション引数の位置で解析が打ち切られる などの仕様があるため、複雑なCLIツールではライブラリやフレームワークのコマンド機能を使う方が安全です。
ログ出力は必須
バッチ処理は無人で動くことが多いため、何が起きたかを後から追えることが非常に重要です。
<?php
function logMessage(string $message): void
{
echo '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL;
}
logMessage('バッチ開始');
logMessage('対象データ取得開始');
logMessage('バッチ終了');
ファイルに保存したい場合は error_log() を使う方法もあります。
<?php
error_log(
'[' . date('Y-m-d H:i:s') . '] バッチエラー' . PHP_EOL,
3,
__DIR__ . '/batch.log'
);
特に残しておきたいのは、開始時刻、終了時刻、対象件数、成功件数、失敗件数、エラー内容、実行引数です。
例外処理と終了コード
バッチ処理は、人が画面を見ながら操作する前提ではありません。
そのため、失敗時の扱いをコード上ではっきりさせる必要があります。
<?php
try {
// 何らかの処理
exit(0);
} catch (Throwable $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
exit(1);
}
終了コードは、一般的に 0 が正常終了、非0が異常終了です。
これはシェルや監視ツールと連携するときに重要です。
定期実行の方法
Linux系では cron が定番
cronを使うと、PHPスクリプトを定期実行できます。
crontabの基本書式は公式manページで定義されています。
ユーザーcrontabなら、たとえばこうです。
0 3 * * * /usr/bin/php /var/www/project/batch/daily_report.php >> /var/log/daily_report.log 2>&1
これは「毎日3時にPHPスクリプトを実行し、標準出力と標準エラーをログに追記する」という意味です。
ただし、/etc/crontab などのsystem crontabでは実行ユーザー名の列が必要です。
そこは混同しやすいので注意が必要です。
例
0 3 * * * www-data /usr/bin/php /var/www/project/batch/daily_report.php >> /var/log/daily_report.log 2>&1
Windowsではタスクスケジューラ
Windowsでは、タスクスケジューラから php.exe を実行する形が一般的です。
PHP公式にもWindowsでのコマンドライン実行方法があります。
php C:\path\to\batch.php
cron運用で注意すべきこと
これは実務上かなり大事です。
cronは通常のシェル環境よりも、使える環境変数が少ないことがあります。
そのため、絶対パスを使うことが重要です。
- PHP実行ファイルは
/usr/bin/phpのように絶対パスで指定する - スクリプト内で参照するファイルも
__DIR__ベースで組む - 相対パス依存を避ける
CLIでは、カレントディレクトリに対する感覚がWeb実行時とずれやすいため、この点はかなり重要です。
PHP公式にもCLI SAPIと他SAPIとの差分があります。
実務で重要な設計ポイント
冪等性
同じバッチを2回実行しても、結果が壊れない設計が重要です。
たとえば売上集計や請求生成で二重登録が起こると致命的です。
対策としては、
- すでに処理済みならスキップする
- 一意制約を使う
- 実行履歴テーブルを持つ
などがあります。
<?php
$stmt = $pdo->prepare('SELECT COUNT(*) FROM daily_reports WHERE report_date = :date');
$stmt->execute([':date' => $targetDate]);
if ((int)$stmt->fetchColumn() > 0) {
echo "すでに処理済みです。\n";
exit(0);
}
多重起動防止
前回の処理がまだ終わっていないのに、次回のcronが起動してしまうことがあります。
これを防がないと二重更新や不整合が起きます。
flock() を使ったロックは、PHPでの簡単な実装としてよく使われます。
PHP公式にもあります。
<?php
$lockFile = __DIR__ . '/batch.lock';
$fp = fopen($lockFile, 'c');
if ($fp === false) {
exit("ロックファイルを開けませんでした。\n");
}
if (!flock($fp, LOCK_EX | LOCK_NB)) {
exit("別プロセスが実行中です。\n");
}
try {
echo "処理開始\n";
sleep(10);
} finally {
flock($fp, LOCK_UN);
fclose($fp);
}
ただし flock() は単一サーバーでは有効でも、複数台構成や共有ストレージでは向かないことがあります。
そういう場合はDBロックやRedisロックのほうが適しています。
分割処理
大量データを一気に読み込むと、メモリ不足や処理遅延の原因になります。
そのため、件数を区切って処理するのが基本です。
<?php
$lastId = 0;
$limit = 1000;
while (true) {
$stmt = $pdo->prepare("
SELECT id, name
FROM users
WHERE id > :last_id
ORDER BY id ASC
LIMIT :limit
");
$stmt->bindValue(':last_id', $lastId, PDO::PARAM_INT);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) {
break;
}
foreach ($rows as $row) {
echo "処理中 user_id=" . $row['id'] . PHP_EOL;
$lastId = (int)$row['id'];
}
}
OFFSET を使ったページングもできますが、大量データでは重くなりやすいため、IDベースのページングの方が向く場面が多いです。
ただし、これは常に絶対ではなく、DB設計やインデックス次第です。
再実行性
バッチは途中失敗することがあります。
そのため、失敗後に安全に再実行できる設計が重要です。
- 引数で対象日を指定できる
- 処理済み判定を持つ
- 途中から再開しやすい
- 二重更新を避ける
<?php
$targetDate = $argv[1] ?? date('Y-m-d', strtotime('-1 day'));
echo "対象日: {$targetDate}\n";
このように対象日を指定できるだけでも、運用はかなり楽になります。
トランザクション
複数のDB更新をまとめて整合性を保ちたい場合は、トランザクションを使います。
<?php
try {
$pdo->beginTransaction();
$pdo->exec("UPDATE accounts SET balance = balance - 1000 WHERE id = 1");
$pdo->exec("UPDATE accounts SET balance = balance + 1000 WHERE id = 2");
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
ただし、実際の挙動はDBやストレージエンジンに依存します。
また、長時間トランザクションはロック競合を起こしやすいため、大量処理では範囲を小さく設計する必要があります。
dry-runは用意したほうが良い
本番更新を伴うバッチでは、実際には変更せず、何が起きるかだけ確認するモードがあると安全です。
<?php
$options = getopt('', ['dry-run']);
$isDryRun = isset($options['dry-run']);
if ($isDryRun) {
echo "dry-runモードです。更新は行いません。\n";
}
これがあるだけで、事故防止にかなり役立ちます。
Laravelでのバッチ処理
Laravelでは、バッチ処理は主にArtisanコマンドとして実装します。
Laravel公式にもカスタムArtisanコマンド作成機能があります。
php artisan make:command DailyReportCommand
そして、スケジューラで定期実行対象として登録できます。
ただし、ここはLaravelのバージョンによって書き方の案内が異なる点に注意が必要です。
現行のLaravel 13.x公式では、スケジュール定義は routes/console.php に書く形が案内されています。
旧バージョンでは app/Console/Kernel.php の schedule() を使う形が一般的でした。
したがって、Laravelの説明をするときは使っているバージョン前提で書くのが正確です。
よくある誤解
PHPはWeb専用ではない
これは誤解です。
PHPはCLIでも正式に使えます。
バッチ処理や運用スクリプトにも十分使えます。
cronに登録すれば終わり
実際にはそこからが本番です。
重要なのは、失敗時にどうするか、再実行できるか、多重起動を防げるか、ログが残るかです。
1回動けばOK
バッチは継続運用されるものなので、数か月後や担当交代後でも安全に回る設計が必要です。
まとめ
PHPにおけるバッチ処理とは、主にCLIで実行するPHPスクリプトを使って、自動・定期・大量処理を行う仕組みです。
PHP公式もCLI利用を正式にサポートしています。
実務で重要なのは、単に「動くこと」ではなく、次の性質を備えることです。
- CLI前提で設計する
- 引数や設定ファイルで制御する
- ログを残す
- 終了コードを返す
- 多重起動を防ぐ
- 冪等性を持たせる
- 再実行しやすくする
- 大量データは分割処理する
- cronでは絶対パスを徹底する
- フレームワークではバージョンごとの流儀に従う
つまりPHPのバッチ処理は、単なる「裏で動くスクリプト」ではなく、安定運用を前提に設計する実務的な仕組みだと考えるのが一番正確です。
以上、PHPにおけるバッチ処理についてでした。
最後までお読みいただき、ありがとうございました。










