[PHP]rmdir関数の使い方を徹底解説!ディレクトリ削除の完全マスター

PHP

はじめに

PHPでファイルシステムを操作する際、不要になったディレクトリを削除する必要がよくあります。しかし、単純にディレクトリを削除するだけでも、様々な注意点やエラーケースがあります。

rmdir関数は、ディレクトリを削除するための基本的な関数ですが、正しく使いこなすにはいくつかの重要なポイントがあります。この記事では、rmdir関数の基本から実践的な使い方、よくある問題と解決策まで、詳しく解説していきます。

rmdir関数とは?

rmdir関数は、空のディレクトリを削除する関数です。ディレクトリ内にファイルやサブディレクトリが存在する場合は削除できません。

基本的な構文

<?php
rmdir(string $directory, ?resource $context = null): bool
?>
  • $directory: 削除するディレクトリのパス
  • $context: ストリームコンテキスト(オプション)
  • 戻り値: 成功時はtrue、失敗時はfalse

最もシンプルな使用例

<?php
// 空のディレクトリを作成
mkdir('test_dir');

// ディレクトリを削除
if (rmdir('test_dir')) {
    echo "ディレクトリを削除しました\n";
} else {
    echo "削除に失敗しました\n";
}
?>

rmdir関数の重要な制限

⚠️ 空のディレクトリのみ削除可能

これが最も重要なポイントです!

<?php
// 空のディレクトリ → 削除成功
mkdir('empty_dir');
rmdir('empty_dir');  // OK

// ファイルが入っているディレクトリ → 削除失敗
mkdir('non_empty_dir');
file_put_contents('non_empty_dir/file.txt', 'content');
rmdir('non_empty_dir');  // エラー!

// 正しい手順: ファイルを先に削除
unlink('non_empty_dir/file.txt');
rmdir('non_empty_dir');  // OK
?>

実践的な使用例

1. 空ディレクトリのチェックと削除

<?php
function removeEmptyDirectory($dir) {
    // ディレクトリが存在するかチェック
    if (!file_exists($dir)) {
        echo "ディレクトリが存在しません: {$dir}\n";
        return false;
    }
    
    // ディレクトリかどうかチェック
    if (!is_dir($dir)) {
        echo "ディレクトリではありません: {$dir}\n";
        return false;
    }
    
    // 空かどうかチェック
    $files = scandir($dir);
    $files = array_diff($files, ['.', '..']);
    
    if (!empty($files)) {
        echo "ディレクトリが空ではありません: {$dir}\n";
        echo "  内容: " . implode(', ', $files) . "\n";
        return false;
    }
    
    // 削除実行
    if (rmdir($dir)) {
        echo "ディレクトリを削除しました: {$dir}\n";
        return true;
    } else {
        echo "削除に失敗しました: {$dir}\n";
        return false;
    }
}

// 使用例
removeEmptyDirectory('./temp');
?>

2. 再帰的なディレクトリ削除

rmdir関数は空のディレクトリしか削除できないため、中身がある場合は再帰的に削除する必要があります。

<?php
/**
 * ディレクトリを中身ごと削除
 */
function removeDirectory($dir) {
    if (!file_exists($dir)) {
        return true;
    }
    
    if (!is_dir($dir)) {
        return unlink($dir);
    }
    
    // ディレクトリ内のすべての項目を取得
    $items = scandir($dir);
    
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue;
        }
        
        $path = $dir . DIRECTORY_SEPARATOR . $item;
        
        // 再帰的に削除
        if (is_dir($path)) {
            removeDirectory($path);
        } else {
            unlink($path);
        }
    }
    
    // 最後にディレクトリ自体を削除
    return rmdir($dir);
}

// 使用例
if (removeDirectory('./old_project')) {
    echo "ディレクトリを削除しました\n";
} else {
    echo "削除に失敗しました\n";
}
?>

3. 安全な削除(確認付き)

<?php
/**
 * 削除前に確認を行う安全な削除
 */
function safeRemoveDirectory($dir, $dryRun = false) {
    if (!file_exists($dir)) {
        echo "ディレクトリが存在しません: {$dir}\n";
        return false;
    }
    
    // 削除対象をリストアップ
    $toDelete = [];
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
        RecursiveIteratorIterator::CHILD_FIRST
    );
    
    foreach ($iterator as $file) {
        $toDelete[] = [
            'path' => $file->getPathname(),
            'type' => $file->isDir() ? 'dir' : 'file',
            'size' => $file->isFile() ? $file->getSize() : 0
        ];
    }
    
    // ディレクトリ本体も追加
    $toDelete[] = [
        'path' => $dir,
        'type' => 'dir',
        'size' => 0
    ];
    
    // 削除内容を表示
    echo "=== 削除対象 ===\n";
    $totalSize = 0;
    $fileCount = 0;
    $dirCount = 0;
    
    foreach ($toDelete as $item) {
        echo "[{$item['type']}] {$item['path']}";
        if ($item['size'] > 0) {
            echo " (" . formatBytes($item['size']) . ")";
        }
        echo "\n";
        
        $totalSize += $item['size'];
        if ($item['type'] === 'file') {
            $fileCount++;
        } else {
            $dirCount++;
        }
    }
    
    echo "\n合計: ファイル {$fileCount}件, ディレクトリ {$dirCount}件, サイズ " . 
         formatBytes($totalSize) . "\n\n";
    
    // ドライランモードの場合はここで終了
    if ($dryRun) {
        echo "ドライランモード: 実際の削除は行いません\n";
        return true;
    }
    
    // 実際に削除
    foreach ($toDelete as $item) {
        if ($item['type'] === 'file') {
            if (!unlink($item['path'])) {
                echo "ファイル削除失敗: {$item['path']}\n";
                return false;
            }
        } else {
            if (!rmdir($item['path'])) {
                echo "ディレクトリ削除失敗: {$item['path']}\n";
                return false;
            }
        }
    }
    
    echo "削除完了しました\n";
    return true;
}

function formatBytes($bytes) {
    $units = ['B', 'KB', 'MB', 'GB'];
    $i = 0;
    while ($bytes >= 1024 && $i < count($units) - 1) {
        $bytes /= 1024;
        $i++;
    }
    return round($bytes, 2) . ' ' . $units[$i];
}

// 使用例
// ドライラン(削除せずに確認のみ)
safeRemoveDirectory('./temp', true);

// 実際に削除
// safeRemoveDirectory('./temp', false);
?>

4. 条件付き削除(古いディレクトリのみ)

<?php
/**
 * 指定日数より古いディレクトリを削除
 */
function removeOldDirectories($parentDir, $days = 30) {
    $threshold = time() - ($days * 86400);
    $removed = [];
    
    if (!is_dir($parentDir)) {
        echo "親ディレクトリが存在しません: {$parentDir}\n";
        return $removed;
    }
    
    $items = scandir($parentDir);
    
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue;
        }
        
        $path = $parentDir . DIRECTORY_SEPARATOR . $item;
        
        if (!is_dir($path)) {
            continue;
        }
        
        // 最終更新日時をチェック
        $mtime = filemtime($path);
        
        if ($mtime < $threshold) {
            $age = floor((time() - $mtime) / 86400);
            echo "削除対象: {$item} ({$age}日前)\n";
            
            // 再帰的に削除
            if (removeDirectory($path)) {
                $removed[] = [
                    'path' => $path,
                    'age_days' => $age,
                    'deleted_at' => date('Y-m-d H:i:s')
                ];
            }
        }
    }
    
    echo "\n削除完了: " . count($removed) . "個のディレクトリ\n";
    return $removed;
}

// 使用例
// 30日以上古いディレクトリを削除
$removed = removeOldDirectories('./backups', 30);
?>

5. パーミッションエラーの処理

<?php
/**
 * パーミッションエラーを処理しながら削除
 */
function forceRemoveDirectory($dir) {
    if (!file_exists($dir)) {
        return true;
    }
    
    // 削除前にパーミッションを変更
    @chmod($dir, 0755);
    
    if (!is_dir($dir)) {
        @chmod($dir, 0644);
        return @unlink($dir);
    }
    
    $items = @scandir($dir);
    
    if ($items === false) {
        echo "ディレクトリの読み取りに失敗: {$dir}\n";
        return false;
    }
    
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue;
        }
        
        $path = $dir . DIRECTORY_SEPARATOR . $item;
        
        if (is_dir($path)) {
            forceRemoveDirectory($path);
        } else {
            @chmod($path, 0644);
            if (!@unlink($path)) {
                echo "ファイル削除失敗: {$path}\n";
            }
        }
    }
    
    // ディレクトリを削除
    @chmod($dir, 0755);
    if (@rmdir($dir)) {
        return true;
    } else {
        echo "ディレクトリ削除失敗: {$dir}\n";
        return false;
    }
}

// 使用例
forceRemoveDirectory('./protected_dir');
?>

6. シンボリックリンクの安全な処理

<?php
/**
 * シンボリックリンクを安全に処理しながら削除
 */
function removeDirWithSymlinks($dir) {
    if (!file_exists($dir) && !is_link($dir)) {
        return true;
    }
    
    // シンボリックリンクの場合
    if (is_link($dir)) {
        echo "シンボリックリンクを削除: {$dir}\n";
        return unlink($dir);
    }
    
    if (!is_dir($dir)) {
        return unlink($dir);
    }
    
    $items = scandir($dir);
    
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue;
        }
        
        $path = $dir . DIRECTORY_SEPARATOR . $item;
        
        // シンボリックリンクはunlinkで削除
        if (is_link($path)) {
            echo "シンボリックリンク削除: {$path}\n";
            unlink($path);
        } elseif (is_dir($path)) {
            removeDirWithSymlinks($path);
        } else {
            unlink($path);
        }
    }
    
    return rmdir($dir);
}

// 使用例
removeDirWithSymlinks('./symlink_dir');
?>

7. ログ記録付き削除

<?php
/**
 * 削除処理をログに記録
 */
class DirectoryRemover {
    private $logFile;
    private $errors = [];
    
    public function __construct($logFile = null) {
        $this->logFile = $logFile ?? sys_get_temp_dir() . '/rmdir.log';
    }
    
    public function remove($dir, $recursive = true) {
        $startTime = microtime(true);
        $this->errors = [];
        
        $this->log("削除開始: {$dir}");
        
        if (!file_exists($dir)) {
            $this->log("エラー: ディレクトリが存在しません");
            return false;
        }
        
        $result = $recursive ? 
            $this->removeRecursive($dir) : 
            $this->removeEmpty($dir);
        
        $duration = round(microtime(true) - $startTime, 3);
        
        if ($result) {
            $this->log("削除完了 ({$duration}秒)");
        } else {
            $this->log("削除失敗 ({$duration}秒)");
            $this->log("エラー: " . implode(', ', $this->errors));
        }
        
        return $result;
    }
    
    private function removeRecursive($dir) {
        if (!is_dir($dir)) {
            if (unlink($dir)) {
                $this->log("ファイル削除: {$dir}");
                return true;
            } else {
                $this->errors[] = "ファイル削除失敗: {$dir}";
                return false;
            }
        }
        
        $items = scandir($dir);
        $count = 0;
        
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            
            $path = $dir . DIRECTORY_SEPARATOR . $item;
            
            if (is_dir($path)) {
                if ($this->removeRecursive($path)) {
                    $count++;
                }
            } else {
                if (unlink($path)) {
                    $this->log("ファイル削除: {$path}");
                    $count++;
                } else {
                    $this->errors[] = "ファイル削除失敗: {$path}";
                }
            }
        }
        
        if (rmdir($dir)) {
            $this->log("ディレクトリ削除: {$dir} ({$count}項目)");
            return true;
        } else {
            $this->errors[] = "ディレクトリ削除失敗: {$dir}";
            return false;
        }
    }
    
    private function removeEmpty($dir) {
        if (!is_dir($dir)) {
            $this->errors[] = "ディレクトリではありません: {$dir}";
            return false;
        }
        
        $items = scandir($dir);
        $items = array_diff($items, ['.', '..']);
        
        if (!empty($items)) {
            $this->errors[] = "ディレクトリが空ではありません: " . implode(', ', $items);
            return false;
        }
        
        if (rmdir($dir)) {
            $this->log("空ディレクトリ削除: {$dir}");
            return true;
        } else {
            $this->errors[] = "削除失敗: {$dir}";
            return false;
        }
    }
    
    private function log($message) {
        $logEntry = sprintf(
            "[%s] %s\n",
            date('Y-m-d H:i:s'),
            $message
        );
        
        file_put_contents($this->logFile, $logEntry, FILE_APPEND);
        echo $logEntry;
    }
    
    public function getErrors() {
        return $this->errors;
    }
    
    public function getLogFile() {
        return $this->logFile;
    }
}

// 使用例
$remover = new DirectoryRemover('./rmdir_log.txt');

if ($remover->remove('./old_data', true)) {
    echo "削除成功\n";
} else {
    echo "削除失敗\n";
    print_r($remover->getErrors());
}

echo "ログファイル: {$remover->getLogFile()}\n";
?>

よくある問題と解決策

問題1: “Directory not empty” エラー

<?php
// ❌ エラーが発生
mkdir('test');
file_put_contents('test/file.txt', 'data');
rmdir('test');  // Warning: rmdir(test): Directory not empty

// ✅ 解決策1: 中身を削除してから
unlink('test/file.txt');
rmdir('test');

// ✅ 解決策2: 再帰的削除関数を使用
removeDirectory('test');
?>

問題2: パーミッションエラー

<?php
// ❌ パーミッションがない
rmdir('/root/protected');  // Permission denied

// ✅ 解決策: パーミッションを確認・変更
if (is_writable(dirname('/path/to/dir'))) {
    rmdir('/path/to/dir');
} else {
    echo "書き込み権限がありません\n";
    // 必要に応じてchmod()で権限変更
    @chmod('/path/to/dir', 0755);
    rmdir('/path/to/dir');
}
?>

問題3: カレントディレクトリの削除

<?php
// ❌ カレントディレクトリは削除できない
chdir('/tmp/test');
rmdir('/tmp/test');  // エラー

// ✅ 解決策: 別のディレクトリに移動してから
chdir('/tmp/test');
chdir('..');  // 親ディレクトリに移動
rmdir('/tmp/test');  // OK
?>

問題4: シンボリックリンク

<?php
// シンボリックリンクの場合
symlink('/path/to/target', 'link_to_dir');

// ❌ rmdirではなくunlinkを使う
// rmdir('link_to_dir');  // エラーになる可能性

// ✅ 正しい方法
if (is_link('link_to_dir')) {
    unlink('link_to_dir');
} else {
    rmdir('link_to_dir');
}
?>

セキュリティ上の注意点

<?php
/**
 * 安全なディレクトリ削除(パストラバーサル対策)
 */
function secureRemoveDirectory($baseDir, $targetDir) {
    // ベースディレクトリを正規化
    $realBase = realpath($baseDir);
    
    if ($realBase === false) {
        throw new Exception("ベースディレクトリが存在しません");
    }
    
    // 削除対象パスを構築
    $fullPath = $baseDir . DIRECTORY_SEPARATOR . $targetDir;
    $realPath = realpath($fullPath);
    
    // パストラバーサル攻撃をチェック
    if ($realPath === false) {
        throw new Exception("ディレクトリが存在しません");
    }
    
    if (strpos($realPath, $realBase) !== 0) {
        throw new Exception("不正なパス: ベースディレクトリ外へのアクセス");
    }
    
    // 削除実行
    return removeDirectory($realPath);
}

// 使用例
try {
    // ✅ 安全
    secureRemoveDirectory('/var/www/uploads', 'temp');
    
    // ❌ 拒否される
    // secureRemoveDirectory('/var/www/uploads', '../../../etc');
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

まとめ

rmdir関数のポイントをおさらいしましょう:

  1. 空のディレクトリのみ削除可能(最重要!)
  2. 中身がある場合は再帰的に削除する必要がある
  3. シンボリックリンクはunlinkで削除
  4. パーミッションエラーに注意
  5. カレントディレクトリは削除できない
  6. セキュリティ対策(パストラバーサル)が重要
  7. 削除前に確認やログ記録を実装すると安全

rmdir関数は基本的な関数ですが、実務では様々なエッジケースに対応する必要があります。この記事で紹介した実践例を参考に、安全で確実なディレクトリ削除を実装してください!

参考リンク


この記事が役に立ったら、ぜひシェアしてください!PHPに関する他の記事もお楽しみに。

タイトルとURLをコピーしました