【PHP】ディレクトリを再帰的にコピーする方法|copy()・RecursiveDirectoryIterator・Laravelの使い分け

2026-04-06

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() ベースの再帰関数がコードの見通しもよく、実務でも使いやすいです。

PHPPHP

Posted by 千原 耕司