[PHP]sys_get_temp_dir完全解説|環境ごとの一時ディレクトリを正しく取得する方法

PHP

はじめに

PHPでファイルアップロード処理・キャッシュ生成・一時ファイルの作成を行うとき、「一時ディレクトリのパスをどこにすべきか」は意外と悩ましい問題です。OSやサーバー環境によって /tmp だったり C:\Windows\Temp だったりするため、パスをハードコーディングするのは移植性に欠けます。

sys_get_temp_dir() は、現在の実行環境におけるシステムの一時ディレクトリを自動判定して返す関数です。OS・環境変数・PHP設定を考慮した優先順位で解決されるため、これを使えば環境に依存しない一時ファイル処理を実装できます。


関数の基本情報

項目内容
関数名sys_get_temp_dir()
利用可能バージョンPHP 5.2.1以降
所属ファイルシステム関数(Filesystem Functions)
戻り値string(一時ディレクトリのパス)
拡張機能不要(コア関数)

シグネチャ

sys_get_temp_dir(): string

パラメータ

なし。


ディレクトリ解決の優先順位

sys_get_temp_dir() は以下の優先順位でディレクトリを決定します。

1. 環境変数 TMPDIR (Unix系優先)
   ↓ 未設定なら
2. 環境変数 TMP / TEMP (Windows優先)
   ↓ 未設定なら
3. php.ini の sys_temp_dir 設定
   ↓ 未設定なら
4. OSのデフォルト一時ディレクトリ
   - Unix/Linux/macOS: /tmp
   - Windows: C:\Windows\Temp(通常)

注意: 優先順位はOSや実行環境(Webサーバー/CLI/CGIなど)によって細部が異なる場合があります。確実な値を得るには、本関数を使うのが最も信頼できる方法です。


OS別の典型的な戻り値

OS / 環境典型的な戻り値の例
Linux/tmp
macOS/var/folders/xx/xxxxxxxx/T/(プロセスごとに異なる場合あり)
WindowsC:\Windows\Temp または C:\Users\<user>\AppData\Local\Temp
Docker(Linux系)/tmp
共有ホスティング/tmp または契約者固有のパス(環境変数依存)

実践サンプル集(PHP 8.x対応)

サンプル1:基本的な取得と検証

<?php
declare(strict_types=1);

$tempDir = sys_get_temp_dir();

echo "一時ディレクトリ: {$tempDir}\n";
echo "存在する:        " . (is_dir($tempDir)    ? 'yes' : 'no') . "\n";
echo "書き込み可能:    " . (is_writable($tempDir) ? 'yes' : 'no') . "\n";
echo "末尾のスラッシュ: " . (str_ends_with($tempDir, DIRECTORY_SEPARATOR) ? 'あり' : 'なし') . "\n";

// 環境変数の確認(参考)
echo "\n--- 関連する環境変数 ---\n";
foreach (['TMPDIR', 'TMP', 'TEMP'] as $envName) {
    $value = getenv($envName);
    echo "{$envName}: " . ($value !== false ? $value : '(未設定)') . "\n";
}

実行結果(Linux環境の例):

一時ディレクトリ: /tmp
存在する:        yes
書き込み可能:    yes
末尾のスラッシュ: なし

--- 関連する環境変数 ---
TMPDIR: (未設定)
TMP: (未設定)
TEMP: (未設定)

解説: sys_get_temp_dir() の戻り値には末尾にスラッシュが付かないことが多いため、パスを連結する際は必ずディレクトリ区切り文字を明示的に挿入する必要があります。


サンプル2:安全なパス連結ユーティリティ

末尾スラッシュの有無に依存しない、安全なパス生成パターンです。

<?php
declare(strict_types=1);

/**
 * 一時ディレクトリ内のパスを安全に生成する
 */
function tempPath(string ...$segments): string
{
    $base = rtrim(sys_get_temp_dir(), '/\\');
    $rest = implode(DIRECTORY_SEPARATOR, array_map(
        fn($s) => trim($s, '/\\'),
        $segments
    ));
    return $base . DIRECTORY_SEPARATOR . $rest;
}

// --- 使用例 ---
echo tempPath('myapp', 'cache', 'data.json') . "\n";
// /tmp/myapp/cache/data.json

echo tempPath('uploads', 'image_123.jpg') . "\n";
// /tmp/uploads/image_123.jpg

// ネストしたディレクトリも作成するヘルパー
function ensureTempDir(string ...$segments): string
{
    $path = tempPath(...$segments);
    if (!is_dir($path)) {
        mkdir($path, 0700, true);
        echo "作成: {$path}\n";
    }
    return $path;
}

$cacheDir = ensureTempDir('myapp', 'cache');
echo "キャッシュディレクトリ: {$cacheDir}\n";
echo "存在確認: " . (is_dir($cacheDir) ? 'yes' : 'no') . "\n";

実行結果:

/tmp/myapp/cache/data.json
/tmp/uploads/image_123.jpg
作成: /tmp/myapp/cache
キャッシュディレクトリ: /tmp/myapp/cache
存在確認: yes

解説: rtrim() で末尾の区切り文字を除去してから連結することで、OSの違いやスラッシュの有無に関わらず安全にパスを構築できます。


サンプル3:一意な一時ファイルの作成

sys_get_temp_dir()tempnam() を組み合わせて衝突しない一時ファイルを生成するパターンです。

<?php
declare(strict_types=1);

class TempFileManager
{
    /** @var list<string> 生成した一時ファイルのパス一覧(クリーンアップ用) */
    private array $createdFiles = [];

    /**
     * 一意な一時ファイルを作成する
     */
    public function create(string $prefix = 'tmp_'): string
    {
        $path = tempnam(sys_get_temp_dir(), $prefix);
        if ($path === false) {
            throw new \RuntimeException('一時ファイルの作成に失敗しました');
        }

        $this->createdFiles[] = $path;
        return $path;
    }

    /**
     * 指定した拡張子付きの一時ファイルを作成
     * (tempnam は拡張子を付けられないため、リネームで対応)
     */
    public function createWithExtension(string $extension, string $prefix = 'tmp_'): string
    {
        $base = $this->create($prefix);
        $newPath = $base . '.' . ltrim($extension, '.');

        rename($base, $newPath);

        // 記録を更新
        $index = array_search($base, $this->createdFiles, true);
        if ($index !== false) {
            $this->createdFiles[$index] = $newPath;
        }

        return $newPath;
    }

    /**
     * すべての一時ファイルを削除する
     */
    public function cleanup(): void
    {
        foreach ($this->createdFiles as $file) {
            if (file_exists($file)) {
                unlink($file);
                echo "削除: {$file}\n";
            }
        }
        $this->createdFiles = [];
    }

    public function __destruct()
    {
        $this->cleanup(); // フェイルセーフ
    }
}

// --- 使用例 ---
$manager = new TempFileManager();

$file1 = $manager->create('upload_');
file_put_contents($file1, 'アップロードされたデータ');
echo "作成: {$file1}\n";
echo "内容: " . file_get_contents($file1) . "\n";

$file2 = $manager->createWithExtension('csv', 'export_');
file_put_contents($file2, "id,name\n1,Alice\n2,Bob");
echo "\n作成(拡張子付き): {$file2}\n";

$manager->cleanup();
echo "\n削除後の存在確認: " . (file_exists($file1) ? 'あり' : 'なし') . "\n";

実行結果:

作成: /tmp/upload_A1B2C3
内容: アップロードされたデータ

作成(拡張子付き): /tmp/export_D4E5F6.csv
削除: /tmp/upload_A1B2C3
削除: /tmp/export_D4E5F6.csv

削除後の存在確認: なし

解説: tempnam() は競合しない一意なファイル名を生成しますが拡張子を付けられないため、必要な場合は生成後にリネームします。__destruct() でのフェイルセーフ削除により、例外発生時もファイルが残りにくい設計になっています。


サンプル4:ファイルアップロード処理での活用

アップロードされたファイルを一時的に処理してから本番ディレクトリへ移動するパターンです。

<?php
declare(strict_types=1);

class UploadProcessor
{
    private readonly string $tempDir;

    public function __construct(?string $customTempDir = null)
    {
        // カスタムディレクトリ指定があれば使用、なければシステムの一時ディレクトリ
        $this->tempDir = $customTempDir ?? sys_get_temp_dir();

        if (!is_dir($this->tempDir) || !is_writable($this->tempDir)) {
            throw new \RuntimeException("一時ディレクトリが利用できません: {$this->tempDir}");
        }
    }

    /**
     * アップロードファイルを一時的に処理する(検証・変換など)
     *
     * @param string $sourceContent シミュレーション用の元データ
     */
    public function process(string $sourceContent, string $originalName): array
    {
        // ① 一時ファイルに保存
        $tempPath = $this->tempDir . DIRECTORY_SEPARATOR
            . 'upload_' . bin2hex(random_bytes(8)) . '_' . basename($originalName);

        file_put_contents($tempPath, $sourceContent);
        echo "[1] 一時保存: {$tempPath}\n";

        try {
            // ② 検証(サイズ・拡張子など)
            $size = filesize($tempPath);
            $ext  = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));

            echo "[2] 検証: サイズ={$size}bytes, 拡張子={$ext}\n";

            if (!in_array($ext, ['jpg', 'png', 'csv', 'txt'], true)) {
                throw new \InvalidArgumentException("許可されていない拡張子: {$ext}");
            }

            // ③ 何らかの処理(リサイズ・スキャンなど、ここではハッシュ計算で代用)
            $hash = hash_file('sha256', $tempPath);
            echo "[3] 処理: SHA256={$hash}\n";

            return [
                'success'  => true,
                'temp_path' => $tempPath,
                'size'     => $size,
                'hash'     => $hash,
            ];

        } catch (\Throwable $e) {
            // ④ エラー時は一時ファイルを削除
            if (file_exists($tempPath)) {
                unlink($tempPath);
                echo "[!] エラーにより一時ファイル削除: {$tempPath}\n";
            }
            throw $e;
        }
    }

    /**
     * 一時ファイルを最終的な保存先へ移動
     */
    public function moveToFinal(string $tempPath, string $finalDir): string
    {
        if (!is_dir($finalDir)) {
            mkdir($finalDir, 0755, true);
        }

        $finalPath = $finalDir . DIRECTORY_SEPARATOR . basename($tempPath);
        rename($tempPath, $finalPath);
        echo "[4] 移動: {$tempPath} → {$finalPath}\n";

        return $finalPath;
    }
}

// --- 使用例 ---
$processor = new UploadProcessor();

$result = $processor->process(
    "id,name,score\n1,Alice,95\n2,Bob,87",
    'scores.csv'
);

echo "\n処理結果:\n";
print_r($result);

// 最終的な保存先へ移動
$finalDir  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'final_storage_demo';
$finalPath = $processor->moveToFinal($result['temp_path'], $finalDir);

echo "\n最終パス: {$finalPath}\n";
echo "存在確認: " . (file_exists($finalPath) ? 'yes' : 'no') . "\n";

// クリーンアップ(デモ用)
unlink($finalPath);
rmdir($finalDir);

実行結果:

[1] 一時保存: /tmp/upload_a1b2c3d4e5f6g7h8_scores.csv
[2] 検証: サイズ=35bytes, 拡張子=csv
[3] 処理: SHA256=...

処理結果:
Array
(
    [success] => 1
    [temp_path] => /tmp/upload_a1b2c3d4e5f6g7h8_scores.csv
    [size] => 35
    [hash] => ...
)
[4] 移動: /tmp/upload_a1b2c3d4e5f6g7h8_scores.csv → /tmp/final_storage_demo/upload_a1b2c3d4e5f6g7h8_scores.csv

最終パス: /tmp/final_storage_demo/upload_a1b2c3d4e5f6g7h8_scores.csv
"yes"

解説: 「一時ディレクトリで検証・加工 → 成功したら本番ディレクトリへ移動」というアップロード処理の典型パターンです。rename() は同一ファイルシステム内であればアトミックに実行されるため、処理中の不整合状態を避けられます。


サンプル5:環境に応じたフォールバック付き取得

sys_get_temp_dir() が想定外の値を返す場合に備えたフォールバック実装です。

<?php
declare(strict_types=1);

class TempDirectoryResolver
{
    /** @var list<string> フォールバック候補(優先順) */
    private array $fallbacks;

    public function __construct(array $customFallbacks = [])
    {
        $this->fallbacks = array_merge($customFallbacks, [
            sys_get_temp_dir(),
            getenv('TMPDIR') ?: '',
            getenv('TMP') ?: '',
            getenv('TEMP') ?: '',
            '/tmp',
            '/var/tmp',
            ini_get('upload_tmp_dir') ?: '',
        ]);
    }

    /**
     * 書き込み可能な一時ディレクトリを解決する
     */
    public function resolve(): string
    {
        foreach ($this->fallbacks as $candidate) {
            if ($candidate === '') {
                continue;
            }

            if ($this->isUsable($candidate)) {
                return rtrim($candidate, '/\\');
            }
        }

        throw new \RuntimeException(
            '書き込み可能な一時ディレクトリが見つかりません。候補: '
            . implode(', ', array_filter($this->fallbacks))
        );
    }

    private function isUsable(string $dir): bool
    {
        return is_dir($dir) && is_writable($dir);
    }

    public function diagnose(): void
    {
        echo "=== 一時ディレクトリ診断 ===\n";
        foreach (array_unique(array_filter($this->fallbacks)) as $candidate) {
            $exists    = is_dir($candidate)      ? '✅' : '❌';
            $writable  = is_writable($candidate) ? '✅' : '❌';
            printf("  存在:%s 書込:%s  %s\n", $exists, $writable, $candidate);
        }
    }
}

// --- 使用例 ---
$resolver = new TempDirectoryResolver([
    '/var/www/app/storage/tmp', // アプリ独自の優先候補
]);

$resolver->diagnose();

try {
    $tempDir = $resolver->resolve();
    echo "\n解決された一時ディレクトリ: {$tempDir}\n";
} catch (\RuntimeException $e) {
    echo "\nエラー: " . $e->getMessage() . "\n";
}

実行結果:

=== 一時ディレクトリ診断 ===
  存在:❌ 書込:❌  /var/www/app/storage/tmp
  存在:✅ 書込:✅  /tmp
  存在:✅ 書込:✅  /var/tmp

解決された一時ディレクトリ: /tmp

解説: 一部の制限された共有ホスティング環境やコンテナでは sys_get_temp_dir() が予期しない値(書き込み不可)を返すことがあります。複数の候補を順に検証するフォールバック戦略により、より堅牢なアプリケーションを実現できます。


サンプル6:プロセス・リクエスト単位の専用一時ディレクトリ

複数リクエストやプロセスが同じ一時ディレクトリを共有する際の名前衝突を避けるパターンです。

<?php
declare(strict_types=1);

class ScopedTempDirectory
{
    private readonly string $path;
    private bool $cleanedUp = false;

    public function __construct(string $namespace = 'app')
    {
        $unique = bin2hex(random_bytes(8));
        $this->path = rtrim(sys_get_temp_dir(), '/\\')
            . DIRECTORY_SEPARATOR . $namespace
            . DIRECTORY_SEPARATOR . $unique;

        if (!mkdir($this->path, 0700, true) && !is_dir($this->path)) {
            throw new \RuntimeException("一時ディレクトリの作成に失敗: {$this->path}");
        }

        echo "[ScopedTemp] 作成: {$this->path}\n";
    }

    public function path(string ...$segments): string
    {
        if (empty($segments)) {
            return $this->path;
        }
        return $this->path . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $segments);
    }

    public function writeFile(string $relativePath, string $content): string
    {
        $fullPath = $this->path(...explode('/', $relativePath));
        $dir = dirname($fullPath);
        if (!is_dir($dir)) {
            mkdir($dir, 0700, true);
        }
        file_put_contents($fullPath, $content);
        return $fullPath;
    }

    /**
     * ディレクトリとその中身を再帰的に削除
     */
    public function cleanup(): void
    {
        if ($this->cleanedUp || !is_dir($this->path)) {
            return;
        }

        $this->removeRecursive($this->path);
        $this->cleanedUp = true;
        echo "[ScopedTemp] 削除: {$this->path}\n";
    }

    private function removeRecursive(string $dir): void
    {
        $items = scandir($dir);
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            $full = $dir . DIRECTORY_SEPARATOR . $item;
            if (is_dir($full)) {
                $this->removeRecursive($full);
            } else {
                unlink($full);
            }
        }
        rmdir($dir);
    }

    public function __destruct()
    {
        $this->cleanup();
    }
}

// --- 使用例:複数の「リクエスト」をシミュレート ---
function simulateRequest(int $requestId): void
{
    $scoped = new ScopedTempDirectory('myapp_request');

    $scoped->writeFile('input/data.json', json_encode(['request_id' => $requestId]));
    $scoped->writeFile('output/result.txt', "処理結果: リクエスト{$requestId}");

    echo "リクエスト{$requestId}用ディレクトリ: " . $scoped->path() . "\n";
    echo "  - " . $scoped->path('input', 'data.json') . "\n";
    echo "  - " . $scoped->path('output', 'result.txt') . "\n";

    // スコープを抜けると __destruct() で自動クリーンアップ
}

simulateRequest(1);
echo "\n";
simulateRequest(2);

実行結果:

[ScopedTemp] 作成: /tmp/myapp_request/a1b2c3d4e5f6a7b8
リクエスト1用ディレクトリ: /tmp/myapp_request/a1b2c3d4e5f6a7b8
  - /tmp/myapp_request/a1b2c3d4e5f6a7b8/input/data.json
  - /tmp/myapp_request/a1b2c3d4e5f6a7b8/output/result.txt
[ScopedTemp] 削除: /tmp/myapp_request/a1b2c3d4e5f6a7b8

[ScopedTemp] 作成: /tmp/myapp_request/f9e8d7c6b5a4f3e2
リクエスト2用ディレクトリ: /tmp/myapp_request/f9e8d7c6b5a4f3e2
v  - /tmp/myapp_request/f9e8d7c6b5a4f3e2/input/data.json
  - /tmp/myapp_request/f9e8d7c6b5a4f3e2/output/result.txt
[ScopedTemp] 削除: /tmp/myapp_request/f9e8d7c6b5a4f3e2

解説: random_bytes() でユニークなサブディレクトリ名を生成することで、並行リクエスト間でのファイル名衝突を防げます。__destruct() での再帰削除により、スコープを抜けると自動的にクリーンアップされます。


よくある落とし穴

① 末尾のスラッシュの有無を仮定しない

// ❌ 末尾スラッシュがあると仮定して連結
$path = sys_get_temp_dir() . 'myfile.txt'; // "/tmpmyfile.txt" になる可能性

// ✅ DIRECTORY_SEPARATORで明示的に連結
$path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'myfile.txt';

② 書き込み可能性を確認しない

// ❌ 常に書き込めると仮定する
file_put_contents(sys_get_temp_dir() . '/data.txt', $content); // 失敗する場合あり

// ✅ 事前に確認する
$dir = sys_get_temp_dir();
if (!is_writable($dir)) {
    throw new RuntimeException("一時ディレクトリに書き込めません: {$dir}");
}

③ 共有ディレクトリでの予測可能なファイル名

// ❌ 予測可能な名前は他プロセスとの衝突やセキュリティリスクになる
$file = sys_get_temp_dir() . '/cache.txt'; // 誰でも上書きできる可能性

// ✅ tempnam() やランダムな名前を使う
$file = tempnam(sys_get_temp_dir(), 'cache_');
// または
$file = sys_get_temp_dir() . '/' . bin2hex(random_bytes(16)) . '.tmp';

④ クリーンアップ忘れによるディスク圧迫

// ❌ 一時ファイルを作りっぱなしにする
$tmp = tempnam(sys_get_temp_dir(), 'big_');
file_put_contents($tmp, $largeData);
// ... 削除を忘れる

// ✅ 確実に削除する(try-finallyまたはデストラクタ)
$tmp = tempnam(sys_get_temp_dir(), 'big_');
try {
    file_put_contents($tmp, $largeData);
    // 処理...
} finally {
    unlink($tmp);
}

⑤ OS間でのパス区切り文字の違い

// ❌ ハードコードされたスラッシュはWindowsで問題になる場合がある
$path = sys_get_temp_dir() . '/sub/dir/file.txt';

// ✅ DIRECTORY_SEPARATOR を使う(クロスプラットフォーム対応)
$path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'sub' . DIRECTORY_SEPARATOR . 'dir' . DIRECTORY_SEPARATOR . 'file.txt';

まとめ

ポイント内容
主な用途環境に依存しない一時ディレクトリパスの取得
解決順序TMPDIR/TMP/TEMP環境変数 → php.ini → OSデフォルト
注意点末尾スラッシュの有無は保証されない
安全な連結rtrim() + DIRECTORY_SEPARATOR
ファイル名tempnam() で衝突しない一意な名前を生成
クリーンアップtry-finally やデストラクタで確実に削除
堅牢性向上書き込み可能性チェック・フォールバック候補の用意

sys_get_temp_dir() はシンプルながら、環境を問わず移植性の高いコードを書くための基本ツールです。tempnam()DIRECTORY_SEPARATOR と組み合わせ、書き込み可能性のチェックとクリーンアップ処理を徹底することで、安全で信頼性の高い一時ファイル処理が実現できます。


PHP 8.x / 執筆時点の最新安定版にて動作確認済み

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