背景
運用していると、DBからは参照されていない古い画像ファイルが storage/app/public/... に溜まりがちです。
本記事では、DBを基準に「未使用ファイルだけ」安全に削除できる Artisan コマンドを作ります。
▪️ 判定は 「ファイル名の最初の _ 以降が一致するか」 に限定(例:12345_home.webp → home.webp で照合)
▪️ --dry-run で削除せず候補だけ確認可
▪️ --keep で消したくないファイル名を追加除外可
▪️ デフォルトで github.png / noImage.jpg / qiita.png / webApp.png / YouTube.png を保護
例:DBに image_path = "home.webp" が入っていれば、実ファイル xxxxx_home.webp は 使用中 とみなされ削除しません。
前提
▪️ Laravel 9 以降を想定
▪️ 画像は public ディスク(= storage/app/public) に保存
▪️ 参照テーブル:collection_images、列:image_path(必要に応じて読み替えてください)
1. コマンドを作成
app/Console/Commands/PruneCollectionImages.php を作成(ファイル名は任意)
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\{DB, Storage};
use Illuminate\Support\Str;
class PruneCollectionImages extends Command
{
protected $signature = 'storage:prune-collection-images
{--dir=collection_images : storage/app/public 配下の対象ディレクトリ}
{--dry-run : 削除せず候補のみ表示}
{--keep=* : 削除対象から常に除外するファイル名(basename。複数指定可)}';
protected $description = 'DB未参照の画像を削除(アンダースコア以降の一致のみ)';
public function handle()
{
$disk = Storage::disk('public'); // storage/app/public
$dir = $this->option('dir') ?: 'collection_images';
if (!$disk->exists($dir)) {
$this->warn("📂 ディレクトリが見つかりません: storage/app/public/{$dir}");
return self::SUCCESS;
}
// 0) 常に残すファイル(固定)+ オプション --keep
$alwaysKeep = [
'github.png',
'noimage.jpg', // 大文字小文字差異を吸収するため小文字で扱う
'qiita.png',
'webapp.png',
'youtube.png',
];
$optKeep = array_map('strtolower', (array)$this->option('keep'));
$keepSet = collect(array_unique(array_merge($alwaysKeep, $optKeep)))->flip();
// 1) DB側:参照中パスを取得(null/空は除去)
$referenced = DB::table('collection_images')->pluck('image_path')->filter();
// 2) 「_ の後ろ」一致用セット(basename の最初の '_' 以降)
$suffixSet = $referenced
->map(function ($p) {
$base = basename(ltrim((string)$p, '/'));
return Str::contains($base, '_') ? Str::after($base, '_') : $base; // '_' が無ければ全体を使う
})
->filter()
->unique()
->flip();
// 3) 実ファイルを走査し、未参照だけ抽出(保護対象はスキップ)
$files = collect($disk->files($dir));
$cands = [];
foreach ($files as $path) {
$base = basename($path);
if (Str::startsWith($base, '.')) continue; // .DS_Store 等は無視
if ($keepSet->has(strtolower($base))) continue; // 保護対象
// 「_ の後ろ」一致で参照判定
$after = Str::contains($base, '_') ? Str::after($base, '_') : $base;
$matchAfter = $suffixSet->has($after);
if (!$matchAfter) {
$cands[] = $path; // 未参照(削除候補)
}
}
// 4) ドライラン
if ($this->option('dry-run')) {
$this->info('🧪 ドライラン: 削除候補一覧');
foreach ($cands as $p) $this->line(" - {$p}");
$this->info('合計: ' . count($cands) . ' ファイル');
return self::SUCCESS;
}
// 5) 削除実行
foreach ($cands as $p) {
$disk->delete($p);
}
$this->info('🗑️ 削除完了: ' . count($cands) . ' ファイル');
return self::SUCCESS;
}
}
コマンドの登録
▪️ Laravelのコマンド自動発見が有効なら、app/Console/Commands 配下に置くだけでOK。
▪️ もし手動登録しているプロジェクトなら app/Console/Kernel.php の $commands に追記してください。
protected $commands = [
\App\Console\Commands\PruneCollectionImages::class,
];
2. 使い方
まずは必ずドライラン!
php artisan storage:prune-collection-images --dry-run
よく使うオプション
オプション | 例 | 説明 |
---|---|---|
--dir |
--dir=collection_images |
storage/app/public/ 配下の対象ディレクトリ |
--dry-run |
--dry-run |
削除せず、候補だけ表示 |
--keep |
--keep=favicon.png --keep=logo.png |
常に残すファイル名(basename)。複数回OK |
実行例
# 候補確認
php artisan storage:prune-collection-images --dir=collection_images --dry-run --keep=logo.png
# 本削除
php artisan storage:prune-collection-images --dir=collection_images --keep=logo.png
3. 何をもって「使用中」とみなすか
▪️ DBの collection_images.image_path を basename に変換し、
▪️ 対象ファイルの 「最初の _ 以降」 と比較して一致したら 使用中 と判定
例:DBに home.webp があれば、17569907_home.webp は 使用中。
▪️ _ が含まれない場合は ファイル名全体 で比較します。
4. 他アプリに流用するには
最低限、この2行だけ読み替えればOKです。
// 参照テーブルとカラムを参照アプリに合わせる
$referenced = DB::table('collection_images')->pluck('image_path')->filter();
// 対象ディレクトリ(publicディスク配下)
php artisan storage:prune-collection-images --dir=uploads
5. スケジュール実行(任意)
protected function schedule(\Illuminate\Console\Scheduling\Schedule $schedule)
{
// 例)毎日 04:30 に実行、保護ファイルも指定
$schedule->command('storage:prune-collection-images --dir=collection_images --keep=logo.png')
->dailyAt('04:30')
->withoutOverlapping();
}
共有レンタルサーバ(例:Xserver)では OS の cron で
php artisan schedule:run を毎分実行に設定してください。
6. よくあるハマりどころ
▪️ php artisan storage:link
を忘れる
public/storage
が storage/app/public
にシンボリックリンクされているか確認。
▪️ 権限問題
webサーバーユーザーに書込権限があるか確認(storage
配下)。
▪️ 削除しちゃダメな共通画像
--keep
に追加、または $alwaysKeep
に固定登録。
▪️ 本番/開発で FILESYSTEM_DISK
が違う
コマンド内を 常に Storage::disk('public')
で固定しておくと安全。
# 初期値
FILESYSTEM_DISK=local
↓
FILESYSTEM_DISK=public