PHPでサーバー上のディレクトリを削除する場合、基本的には rmdir() を使います。
ただし、rmdir() で削除できるのは 中身が空のディレクトリのみ です。
ディレクトリ内にファイルやサブディレクトリがある場合は、先に中身を削除してから、最後にディレクトリ本体を削除する必要があります。
この記事では、PHPでディレクトリを削除する基本的な方法から、中身ごと削除する方法、実務で注意すべきセキュリティ対策まで詳しく解説します。
空のディレクトリを削除する方法
rmdir()を使う
空のディレクトリを削除する場合は、PHPの rmdir() を使います。
<?php
$dir = __DIR__ . '/sample_dir';
if (is_dir($dir)) {
if (rmdir($dir)) {
echo 'ディレクトリを削除しました。';
} else {
echo 'ディレクトリの削除に失敗しました。';
}
} else {
echo '指定されたディレクトリは存在しません。';
}
?>
rmdir() は、指定したディレクトリを削除する関数です。
ただし、削除できるのは空のディレクトリだけです。
例えば、以下のように中身がないディレクトリであれば削除できます。
sample_dir/
一方で、以下のようにファイルが入っている場合は削除できません。
sample_dir/
└── test.txt
この場合、先に test.txt を削除してから、sample_dir を削除する必要があります。
ファイルが入っているディレクトリを削除する方法
ファイル削除にはunlink()を使う
PHPでは、ファイルを削除するときに unlink() を使います。
unlink('削除したいファイルのパス');
一方、ディレクトリを削除するときは rmdir() を使います。
rmdir('削除したいディレクトリのパス');
つまり、ファイルとディレクトリでは削除に使う関数が異なります。
| 処理 | 使用する関数 |
|---|---|
| ファイルを削除する | unlink() |
| 空のディレクトリを削除する | rmdir() |
例えば、ディレクトリ内に1つだけファイルがある場合は、次のように削除できます。
<?php
$dir = __DIR__ . '/sample_dir';
$file = $dir . '/test.txt';
if (file_exists($file)) {
unlink($file);
}
if (is_dir($dir)) {
rmdir($dir);
}
?>
このコードでは、まず test.txt を削除し、その後で sample_dir を削除しています。
ディレクトリを中身ごと削除する方法
再帰処理でファイルとサブディレクトリを削除する
実務では、ディレクトリの中に複数のファイルやサブディレクトリが入っていることが多いです。
例えば、以下のような構成です。
uploads/
├── image01.jpg
├── image02.jpg
└── old/
└── image03.jpg
このようなディレクトリを丸ごと削除するには、ディレクトリの中身を順番に確認し、ファイルなら unlink()、サブディレクトリなら同じ処理を再帰的に実行します。
<?php
function removeDir(string $dir): bool
{
if (!is_dir($dir)) {
return false;
}
$items = scandir($dir);
if ($items === false) {
return false;
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $item;
if (is_dir($path) && !is_link($path)) {
removeDir($path);
} else {
unlink($path);
}
}
return rmdir($dir);
}
$targetDir = __DIR__ . '/uploads/sample';
if (removeDir($targetDir)) {
echo 'ディレクトリを削除しました。';
} else {
echo 'ディレクトリを削除できませんでした。';
}
?>
このコードでは、scandir() でディレクトリ内のファイルやフォルダを取得しています。
取得した項目がファイルであれば unlink() で削除し、ディレクトリであれば removeDir() を再度呼び出して中身を削除します。
最後に、空になったディレクトリを rmdir() で削除します。
.と..は除外する
scandir() を使うと、通常のファイル名だけでなく、. と .. も取得されます。
.
..
image01.jpg
image02.jpg
old
. は現在のディレクトリ、.. は親ディレクトリを表します。
これらを削除対象に含めると正しく処理できないため、以下のように除外します。
if ($item === '.' || $item === '..') {
continue;
}
シンボリックリンクに注意する
再帰的にディレクトリを削除する場合は、シンボリックリンクに注意が必要です。
シンボリックリンクとは、別のファイルやディレクトリを参照するリンクのことです。
特に、ディレクトリへのシンボリックリンクを通常のディレクトリとして扱ってしまうと、想定外の場所まで削除処理が進んでしまう可能性があります。
そのため、再帰処理では次のように is_link() を使い、シンボリックリンク先へ入らないようにします。
if (is_dir($path) && !is_link($path)) {
removeDir($path);
} else {
unlink($path);
}
このようにすることで、通常のディレクトリだけを再帰的に処理し、シンボリックリンクはリンク自体を削除する形にできます。
なお、一般的なLinuxサーバーではシンボリックリンクは unlink() で削除できます。
ただし、Windows環境ではディレクトリへのシンボリックリンクを削除する際に rmdir() が必要になる場合があります。
より安全にディレクトリを削除する実務向けコード
削除できる範囲を制限する
サーバー上でディレクトリ削除を行う処理は、非常に慎重に実装する必要があります。
特に、ユーザー入力をもとに削除対象を決める場合は危険です。
例えば、次のようなコードは避けるべきです。
<?php
$dir = $_GET['dir'];
removeDir($dir);
?>
このようなコードでは、URLパラメータに危険な値を入れられる可能性があります。
delete.php?dir=../../
もしこの値がそのまま削除処理に使われると、想定外のディレクトリを削除してしまうおそれがあります。
そのため、削除処理では次のような対策が重要です。
- 削除対象が本当にディレクトリか確認する
- 削除できる親ディレクトリを限定する
- ベースディレクトリ自体は削除できないようにする
../などを含む不正なパスを防ぐ- シンボリックリンク先へ再帰しない
- 削除失敗時は例外で処理する
realpath()で実際のパスを確認する
安全に削除処理を行うには、realpath() を使って実際の絶対パスを確認するとよいです。
realpath() は、指定したパスを正規化された絶対パスに変換します。
例えば、次のようなパスがあるとします。
$dir = __DIR__ . '/uploads/../';
このようなパスには ../ が含まれているため、見た目だけでは実際にどのディレクトリを指しているのか分かりにくい場合があります。
realpath() を使うと、こうした ../ を解決した実際のパスを確認できます。
$realDir = realpath($dir);
ただし、realpath() は存在するパスに対して有効です。
存在しないディレクトリを指定した場合は false を返します。
今回のように「既存のディレクトリを削除する」処理では、realpath() で存在確認とパスの正規化を行うのが有効です。
実務向けの安全な削除関数
以下は、指定したベースディレクトリ配下のディレクトリだけを削除できるようにした実務向けのサンプルです。
<?php
declare(strict_types=1);
function deleteDirectorySafely(string $dir, string $allowedBaseDir): bool
{
$realDir = realpath($dir);
$realBase = realpath($allowedBaseDir);
if ($realDir === false || $realBase === false) {
throw new RuntimeException('指定されたパスが存在しません。');
}
if (!is_dir($realDir)) {
throw new RuntimeException('削除対象がディレクトリではありません。');
}
if ($realDir === $realBase) {
throw new RuntimeException('ベースディレクトリ自体は削除できません。');
}
if (strpos($realDir, $realBase . DIRECTORY_SEPARATOR) !== 0) {
throw new RuntimeException('許可されていないディレクトリです。');
}
$items = scandir($realDir);
if ($items === false) {
throw new RuntimeException('ディレクトリを読み込めません。');
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $realDir . DIRECTORY_SEPARATOR . $item;
if (is_link($path)) {
if (!unlink($path)) {
throw new RuntimeException('シンボリックリンクを削除できません: ' . $path);
}
continue;
}
if (is_dir($path)) {
deleteDirectorySafely($path, $realBase);
continue;
}
if (is_file($path)) {
if (!unlink($path)) {
throw new RuntimeException('ファイルを削除できません: ' . $path);
}
continue;
}
throw new RuntimeException('削除できない種類のファイルです: ' . $path);
}
if (!rmdir($realDir)) {
throw new RuntimeException('ディレクトリを削除できません: ' . $realDir);
}
return true;
}
使用例は以下の通りです。
<?php
$allowedBaseDir = __DIR__ . '/uploads';
$targetDir = __DIR__ . '/uploads/old';
try {
deleteDirectorySafely($targetDir, $allowedBaseDir);
echo '削除が完了しました。';
} catch (RuntimeException $e) {
echo '削除に失敗しました。';
}
?>
このコードでは、uploads 配下のディレクトリだけを削除できるようにしています。
例えば、以下のようなディレクトリは削除対象にできます。
/uploads/old
/uploads/tmp
/uploads/user_123
一方で、以下のような場所は削除できません。
/
/var/www
/etc
/uploads
ベースディレクトリ自体を削除できないようにしているため、uploads 全体を誤って削除する事故も防ぎやすくなります。
ユーザー入力から削除対象を指定する場合の注意点
ディレクトリ名の形式を制限する
フォームやURLパラメータから削除対象のディレクトリ名を受け取る場合は、入力値を必ず制限してください。
例えば、ディレクトリ名として英数字、ハイフン、アンダースコアのみを許可する場合は、次のようにします。
<?php
$dirName = $_POST['dir'] ?? '';
if (!preg_match('/\A[a-zA-Z0-9_-]+\z/', $dirName)) {
exit('不正なディレクトリ名です。');
}
?>
このようにすることで、次のような危険な値を拒否できます。
../
../../
/etc
uploads/test
sample/../../
削除処理では、ユーザーが入力した値をそのままパスとして使わないことが重要です。
GETではなくPOSTで削除する
削除処理は、基本的にGETリクエストではなくPOSTリクエストで実行するべきです。
避けるべき例は次のようなURLです。
/delete.php?dir=sample_dir
このような設計では、URLにアクセスしただけで削除処理が実行されてしまいます。
誤クリックや外部サイトからの誘導によって、意図せず削除が行われる可能性があります。
そのため、削除処理は以下のような流れにするのが安全です。
一覧画面
↓
削除ボタン
↓
確認画面
↓
POSTで削除実行
↓
完了画面
CSRF対策を行う
管理画面などから削除機能を実行する場合は、CSRF対策も必要です。
CSRFとは、ログイン済みのユーザーに意図しないリクエストを送らせる攻撃のことです。
削除フォームにはトークンを埋め込み、削除処理側で正しいトークンかどうかを確認します。
フォーム側の例です。
<?php
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<form method="post" action="delete.php">
<input
type="hidden"
name="csrf_token"
value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8') ?>"
>
<input type="hidden" name="dir" value="sample_dir">
<button type="submit">削除する</button>
</form>
削除処理側の例です。
<?php
session_start();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit('不正なリクエストです。');
}
if (
empty($_POST['csrf_token']) ||
empty($_SESSION['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])
) {
exit('CSRFトークンが不正です。');
}
// ここで削除処理を実行する
?>
hash_equals() を使うことで、トークン比較時の安全性を高めることができます。
実務向けの削除処理サンプル
POST・CSRF・パス制限を含めたコード例
以下は、POSTリクエスト、CSRF対策、ディレクトリ名の制限、ベースディレクトリ制限を組み合わせたサンプルです。
<?php
declare(strict_types=1);
session_start();
function deleteDirectorySafely(string $dir, string $allowedBaseDir): bool
{
$realDir = realpath($dir);
$realBase = realpath($allowedBaseDir);
if ($realDir === false || $realBase === false) {
throw new RuntimeException('指定されたパスが存在しません。');
}
if (!is_dir($realDir)) {
throw new RuntimeException('削除対象がディレクトリではありません。');
}
if ($realDir === $realBase) {
throw new RuntimeException('ベースディレクトリ自体は削除できません。');
}
if (strpos($realDir, $realBase . DIRECTORY_SEPARATOR) !== 0) {
throw new RuntimeException('許可されていないディレクトリです。');
}
$items = scandir($realDir);
if ($items === false) {
throw new RuntimeException('ディレクトリを読み込めません。');
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $realDir . DIRECTORY_SEPARATOR . $item;
if (is_link($path)) {
if (!unlink($path)) {
throw new RuntimeException('シンボリックリンクを削除できません。');
}
continue;
}
if (is_dir($path)) {
deleteDirectorySafely($path, $realBase);
continue;
}
if (is_file($path)) {
if (!unlink($path)) {
throw new RuntimeException('ファイルを削除できません。');
}
continue;
}
throw new RuntimeException('削除できない種類のファイルです。');
}
if (!rmdir($realDir)) {
throw new RuntimeException('ディレクトリを削除できません。');
}
return true;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit('不正なリクエストです。');
}
if (
empty($_POST['csrf_token']) ||
empty($_SESSION['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])
) {
exit('不正なリクエストです。');
}
$dirName = $_POST['dir'] ?? '';
if (!preg_match('/\A[a-zA-Z0-9_-]+\z/', $dirName)) {
exit('不正なディレクトリ名です。');
}
$baseDir = __DIR__ . '/uploads';
$targetDir = $baseDir . DIRECTORY_SEPARATOR . $dirName;
try {
deleteDirectorySafely($targetDir, $baseDir);
echo '削除が完了しました。';
} catch (RuntimeException $e) {
echo '削除に失敗しました。';
}
?>
このサンプルでは、削除対象を uploads 配下に限定しています。
また、ディレクトリ名に使える文字を制限しているため、../ などを使った不正なパス指定を防ぎやすくなります。
エラーメッセージについては、実際の運用では詳細をそのままユーザーに表示しないほうが安全です。
詳細なエラーはログに記録し、画面には「削除に失敗しました」のような一般的なメッセージだけを表示するのがよいでしょう。
glob()を使って削除する方法
ファイルだけなら簡易的に削除できる
ディレクトリ内にファイルだけが入っている場合は、glob() を使って簡易的に削除することもできます。
<?php
$dir = __DIR__ . '/sample_dir';
foreach (glob($dir . '/*') as $file) {
if (is_file($file)) {
unlink($file);
}
}
rmdir($dir);
?>
このコードでは、sample_dir の中にあるファイルを順番に削除し、最後に sample_dir を rmdir() で削除しています。
サブディレクトリや隠しファイルには注意する
glob() を使った方法はシンプルですが、万能ではありません。
例えば、以下のようなケースでは意図通りに削除できないことがあります。
sample_dir/
├── file.txt
├── .htaccess
└── old/
└── image.jpg
glob($dir . '/*') では、.htaccess のようなドットから始まる隠しファイルを取得できない場合があります。
また、サブディレクトリがある場合は、その中身まで自動では削除できません。
そのため、実務でディレクトリを中身ごと削除したい場合は、glob() よりも scandir() を使った再帰処理のほうが扱いやすいです。
exec('rm -rf')で削除する方法は避ける
シェルコマンドによる削除は危険
PHPからシェルコマンドを実行して、以下のようにディレクトリを削除する方法もあります。
exec('rm -rf ' . $dir);
しかし、この方法は基本的に避けたほうがよいです。
特に、ユーザー入力が $dir に入る場合は非常に危険です。
例えば、次のような値が入ると、想定外のコマンドが実行される可能性があります。
$dir = 'sample; rm -rf /';
また、変数の中身が空文字や / になってしまった場合、重大な事故につながるおそれがあります。
$dir = '/';
exec('rm -rf ' . $dir);
どうしても使う場合もパス制限は必須
どうしてもシェルコマンドを使う必要がある場合は、少なくとも escapeshellarg() を使うべきです。
exec('rm -rf ' . escapeshellarg($dir));
ただし、escapeshellarg() を使えば完全に安全になるわけではありません。
コマンドインジェクションのリスクを下げることはできますが、削除対象のパスが正しいかどうかは別問題です。
そのため、シェルコマンドを使う場合でも、次のような対策は必須です。
- 削除できるディレクトリを限定する
- 空文字や
/を削除対象にしない realpath()で実際のパスを確認する- ベースディレクトリ配下か確認する
- ユーザー入力をそのまま使わない
基本的には、PHPの unlink() と rmdir() を使って削除処理を書くほうが安全です。
WordPressでディレクトリを削除する方法
WP_Filesystemを使う
WordPressでディレクトリを削除する場合は、PHP標準の unlink() や rmdir() を直接使う方法もありますが、WordPressらしく実装するなら WP_Filesystem を使う方法があります。
<?php
global $wp_filesystem;
require_once ABSPATH . 'wp-admin/includes/file.php';
if (!WP_Filesystem()) {
wp_die('ファイルシステムにアクセスできません。');
}
$targetDir = WP_CONTENT_DIR . '/uploads/sample_dir';
if ($wp_filesystem->is_dir($targetDir)) {
$deleted = $wp_filesystem->delete($targetDir, true);
if (!$deleted) {
wp_die('ディレクトリを削除できませんでした。');
}
}
?>
$wp_filesystem->delete() の第2引数に true を指定すると、ディレクトリを再帰的に削除できます。
$wp_filesystem->delete($targetDir, true);
WordPressのプラグインやテーマ内で、アップロードフォルダ配下の一時ディレクトリを削除したい場合などに使えます。
WordPressでも削除範囲の制限は必要
WordPressで削除処理を書く場合も、削除対象を安全な範囲に限定することが重要です。
例えば、以下のような場所に限定するとよいです。
$baseDir = WP_CONTENT_DIR . '/uploads/my-plugin';
プラグインが管理しているディレクトリ配下だけを削除対象にすれば、WordPress本体や他のプラグインのファイルを誤って削除するリスクを下げられます。
Laravelでディレクトリを削除する方法
Storage::deleteDirectory()を使う
Laravelでは、Storage ファサードを使ってディレクトリを削除できます。
use Illuminate\Support\Facades\Storage;
Storage::deleteDirectory('sample_dir');
この方法は、Laravelの設定されたディスク上のディレクトリを削除する場合に便利です。
例えば、storage/app/sample_dir を削除したい場合などに使えます。
また、S3などの外部ストレージを使っている場合も、Laravelのファイルシステム設定に応じて処理できます。
File::deleteDirectory()を使う
サーバー上の実パスを指定してディレクトリを削除したい場合は、File ファサードを使う方法もあります。
use Illuminate\Support\Facades\File;
File::deleteDirectory(storage_path('app/sample_dir'));
Storage::deleteDirectory() と File::deleteDirectory() の違いは、次のように考えると分かりやすいです。
| 方法 | 向いているケース |
|---|---|
Storage::deleteDirectory() | Laravelのディスク設定を使って削除したい場合 |
File::deleteDirectory() | サーバー上の実パスを直接指定して削除したい場合 |
Laravelで開発している場合は、保存先をLaravelのファイルシステムで管理しているなら Storage、実パスを扱うなら File を使うとよいでしょう。
ディレクトリを削除できないときの原因
ディレクトリが空ではない
rmdir() は、空のディレクトリしか削除できません。
そのため、以下のようなディレクトリはそのままでは削除できません。
sample_dir/
└── test.txt
この場合は、先に中のファイルを unlink() で削除してから、rmdir() を実行します。
パスが間違っている
相対パスを使っていると、想定と違う場所を参照していることがあります。
例えば、以下のような指定です。
$dir = 'uploads/sample';
この場合、PHPファイルの実行位置によって参照先が変わる可能性があります。
できれば、__DIR__ を使って絶対パスに近い形で指定すると分かりやすくなります。
$dir = __DIR__ . '/uploads/sample';
権限がない
PHPを実行しているユーザーに削除権限がない場合、unlink() や rmdir() は失敗します。
Webサーバーの実行ユーザーは、環境によって次のような名前になっていることがあります。
www-data
apache
nginx
削除対象のファイルやディレクトリの所有者・権限が合っていないと、PHPから削除できません。
また、ディレクトリ削除では、削除対象そのものだけでなく、親ディレクトリの書き込み権限も重要です。
例えば、次のように親ディレクトリの権限を確認できます。
$parentDir = dirname($dir);
if (!is_writable($parentDir)) {
echo '親ディレクトリに書き込み権限がありません。';
}
ファイルがロックされている
他のプロセスがファイルを使用中の場合、環境によっては削除に失敗することがあります。
特にWindows環境では、開いているファイルがあると削除できないケースがあります。
Linuxサーバーでは削除の挙動が異なる場合もありますが、いずれにしても削除に失敗する場合は、対象ファイルを使用している処理がないか確認しましょう。
シンボリックリンクがある
ディレクトリ内にシンボリックリンクがある場合も注意が必要です。
シンボリックリンク先を通常のディレクトリとして再帰処理してしまうと、想定外の場所まで削除対象になるおそれがあります。
再帰削除を行う場合は、次のように is_link() を先に確認するのが安全です。
if (is_link($path)) {
unlink($path);
}
PHPでディレクトリを削除するときの注意点
削除前に確認画面を挟む
管理画面からディレクトリを削除する場合、削除ボタンを押しただけですぐ削除される設計は避けたほうが安全です。
最低限、確認画面を挟むようにしましょう。
削除対象を選択
↓
確認画面
↓
POSTで削除実行
↓
完了画面
削除処理は取り消しが難しいため、ユーザーが意図した操作であることを確認する設計が重要です。
バックアップを用意する
本番サーバーでディレクトリ削除を行う場合は、事前にバックアップを用意しておくべきです。
特に、画像ファイル、アップロードファイル、キャッシュ以外の重要ファイルを削除する可能性がある場合は注意が必要です。
削除処理の実装ミスは、サイト全体のデータ損失につながる場合があります。
ログを残す
実務では、誰が、いつ、どのディレクトリを削除したのかをログに残すと安心です。
例えば、次のような情報を記録します。
| 項目 | 内容 |
|---|---|
| 実行日時 | 削除処理を行った日時 |
| ユーザーID | 削除を実行した管理者 |
| 削除対象 | 削除したディレクトリ |
| 結果 | 成功または失敗 |
| エラー内容 | 失敗した場合の原因 |
管理画面から削除機能を提供する場合は、削除ログを残しておくと、トラブル発生時に原因を追いやすくなります。
まとめ
PHPでサーバー上のディレクトリを削除する場合は、削除対象の状態によって使う関数が変わります。
| やりたいこと | 使用する関数・方法 |
|---|---|
| 空のディレクトリを削除する | rmdir() |
| ファイルを削除する | unlink() |
| ディレクトリ内の一覧を取得する | scandir() |
| 中身ごと削除する | 再帰処理を使う |
| 安全に削除する | realpath() で確認し、削除範囲を制限する |
空のディレクトリであれば、rmdir() だけで削除できます。
しかし、中にファイルやサブディレクトリがある場合は、先に中身を削除し、最後にディレクトリ本体を削除する必要があります。
また、実務で特に重要なのは、削除できる範囲を限定することです。
ユーザー入力をそのまま削除パスに使うと、../ などによって想定外のディレクトリを削除される危険があります。
そのため、PHPでディレクトリ削除機能を作る場合は、以下の対策を入れるようにしましょう。
- 削除対象を特定のベースディレクトリ配下に限定する
realpath()で実際のパスを確認する- ベースディレクトリ自体は削除できないようにする
- シンボリックリンク先へ再帰しない
- ユーザー入力の文字種を制限する
- GETではなくPOSTで削除する
- CSRF対策を行う
- 必要に応じてログやバックアップを用意する
単にディレクトリを削除するだけなら簡単ですが、サーバー上の削除処理は一歩間違えると大きな事故につながります。
そのため、実装時には「どう削除するか」だけでなく、どこまで削除を許可するか を必ず設計することが大切です。
以上、PHPでサーバー上のディレクトリを削除する方法についてでした。
最後までお読みいただき、ありがとうございました。









