【PHP】ディレクトリを再帰的にコピーする方法|copy()・RecursiveDirectoryIterator・Laravelの使い分け
PHPには copy() 関数がありますが、ディレクトリをまるごとコピーする関数は標準では存在しません。
「サブディレクトリも含めて全部コピーしたい」
「Laravelを使っているなら楽な方法はある?」
「パーミッションも一緒にコピーしたい場合は?」
この記事では、PHP標準の再帰処理からLaravelの組み込みメソッドまで、用途に合わせた実装方法を具体的なコードとともに解説します。
1. なぜ copy() だけでは不十分なのか
PHPの copy() 関数はファイル1つのコピーのみに対応しており、ディレクトリには使えません。
copy('/path/to/dir', '/path/to/new-dir'); // エラー
// Warning: copy(): Is a directory
ディレクトリをコピーするには、再帰的にディレクトリをたどってファイルを1つずつコピーする処理が必要です。
2. 基本実装:再帰関数で実現する
最もシンプルな実装です。scandir() でディレクトリの内容を取得し、再帰的に処理します。
function copyDir(string $src, string $dst): void
{
// コピー元が存在しない場合は終了
if (!is_dir($src)) {
throw new \RuntimeException("コピー元ディレクトリが存在しません: {$src}");
}
// コピー先がなければ作成
if (!is_dir($dst)) {
mkdir($dst, 0755, true);
}
$files = scandir($src);
foreach ($files as $file) {
// カレント・親ディレクトリはスキップ
if ($file === '.' || $file === '..') {
continue;
}
$srcPath = $src . DIRECTORY_SEPARATOR . $file;
$dstPath = $dst . DIRECTORY_SEPARATOR . $file;
if (is_dir($srcPath)) {
// ディレクトリなら再帰処理
copyDir($srcPath, $dstPath);
} else {
// ファイルならコピー
copy($srcPath, $dstPath);
}
}
}
使い方
copyDir('/var/www/html/src', '/var/www/html/dst');
ポイント
DIRECTORY_SEPARATORを使うことでOS差異(/vs\)を吸収できますmkdir()の第3引数をtrueにすると、中間ディレクトリも一括作成できます- 例外を投げる設計にしておくと、呼び出し側でエラーハンドリングしやすくなります
3. 応用実装:RecursiveDirectoryIteratorを使う
scandir() ベースより柔軟な制御が必要な場合は RecursiveDirectoryIterator を使います。大量ファイルの処理やフィルタリングが必要なケースに向いています。
function copyDirIterator(string $src, string $dst): void
{
if (!is_dir($src)) {
throw new \RuntimeException("コピー元ディレクトリが存在しません: {$src}");
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(
$src,
\RecursiveDirectoryIterator::SKIP_DOTS
),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
$dstPath = $dst . DIRECTORY_SEPARATOR . $iterator->getSubPathname();
if ($item->isDir()) {
if (!is_dir($dstPath)) {
mkdir($dstPath, 0755, true);
}
} else {
copy($item->getPathname(), $dstPath);
}
}
}
scandir() との違い
| scandir() + 再帰 | RecursiveDirectoryIterator | |
|---|---|---|
| コードの簡潔さ | ✅ シンプル | 🔺 やや複雑 |
| 大量ファイル対応 | 🔺 メモリ消費あり | ✅ イテレーター処理 |
| フィルタリング | 🔺 自前実装 | ✅ 拡張しやすい |
| 実務での使用頻度 | ✅ 高い | 中程度 |
4. Laravelで使う場合
Laravelを使っているなら Illuminate\Support\Facades\File に便利なメソッドがあります。
use Illuminate\Support\Facades\File;
// ディレクトリをまるごとコピー
File::copyDirectory('/path/to/src', '/path/to/dst');
内部的には RecursiveDirectoryIterator を使っており、サブディレクトリも含めて再帰的にコピーされます。自前で実装する必要はありません。
その他の便利なメソッド
// ディレクトリの存在確認
File::isDirectory('/path/to/dir'); // bool
// ディレクトリを作成(再帰的)
File::makeDirectory('/path/to/dir', 0755, true);
// ディレクトリを削除(中身ごと)
File::deleteDirectory('/path/to/dir');
// ディレクトリを移動
File::moveDirectory('/path/to/src', '/path/to/dst');
5. パーミッションも含めてコピーしたい場合
copy() はファイルの内容はコピーしますが、パーミッションはコピーしません。パーミッションも引き継ぎたい場合は chmod() を組み合わせます。
function copyDirWithPermissions(string $src, string $dst): void
{
if (!is_dir($src)) {
throw new \RuntimeException("コピー元ディレクトリが存在しません: {$src}");
}
$perms = fileperms($src) & 0777;
if (!is_dir($dst)) {
mkdir($dst, $perms, true);
}
$files = scandir($src);
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$srcPath = $src . DIRECTORY_SEPARATOR . $file;
$dstPath = $dst . DIRECTORY_SEPARATOR . $file;
if (is_dir($srcPath)) {
copyDirWithPermissions($srcPath, $dstPath);
} else {
copy($srcPath, $dstPath);
chmod($dstPath, fileperms($srcPath) & 0777);
}
}
}
注意点
chmod()はWebサーバーの実行ユーザー権限に依存します- Docker環境ではコンテナ内のユーザーとホストのユーザーが異なる場合があり、期待通りに動かないケースがあります
- 本番環境での
0777設定は避け、0755(ディレクトリ)・0644(ファイル)が一般的です
6. 実務での使用シーン
30年のPHP開発の中でディレクトリコピーが必要になる主なシーンはこちらです。
テンプレートファイルのコピー
// 新規プロジェクト作成時にテンプレートをコピー
copyDir('/app/templates/default', "/app/projects/{$projectId}");
デプロイ前のバックアップ
// 本番ファイルをバックアップしてから更新
$backup = '/var/www/backup/' . date('YmdHis');
copyDir('/var/www/html', $backup);
ユニットテスト用のフィクスチャ準備
// テスト前にフィクスチャディレクトリをコピー
protected function setUp(): void
{
copyDir(__DIR__ . '/fixtures/base', __DIR__ . '/fixtures/tmp');
}
まとめ
| 状況 | 推奨する方法 |
|---|---|
| Laravelを使っている | File::copyDirectory() |
| 素のPHPでシンプルに実装 | scandir() • 再帰関数 |
| 大量ファイル・フィルタリングが必要 | RecursiveDirectoryIterator |
| パーミッションも引き継ぎたい | fileperms() • chmod() を組み合わせる |
Laravelを使っているなら File::copyDirectory() 一択です。素のPHPであれば scandir() ベースの再帰関数がコードの見通しもよく、実務でも使いやすいです。

