[PHP]session_save_path()完全解説|セッション保存先の取得・変更方法と本番環境での安全な設定

PHP

はじめに

PHPのセッションデータはデフォルトでサーバーのファイルシステムに保存されます。その保存先ディレクトリを制御するのが session_save_path() です。

デフォルトのパス(多くの環境では /tmp/var/lib/php/sessions)のまま運用すると、複数アプリ間でのセッション混在・ディレクトリ権限の問題・共有ホスティング環境でのデータ漏洩リスクが生じる可能性があります。本番環境では必ずアプリ専用のパスを設定することが推奨されます。また Redis や Memcached などのキャッシュストアを使う場合も、接続先 DSN をこの関数で指定します。


関数の概要

項目内容
関数名session_save_path()
所属PHP セッション関数
導入バージョンPHP 4以降
PHP 8.x対応済み

構文

session_save_path(string $path = ?): string|false

パラメータ

パラメータ説明
$pathstring(省略可)設定したい保存先パス。省略すると現在の値を返すのみ。

戻り値

  • 引数なし(取得):現在の保存先パス(文字列)を返します。
  • 引数あり(変更):変更のパス(文字列)を返します。
  • 失敗時false を返します。

⚠️ 注意session_start() を呼び出したに変更しようとすると、警告が発生し変更されません。必ず session_start()に呼び出してください。


保存先パスの書式

ファイルシステム(デフォルト)

/var/lib/php/sessions/myapp

深さ(サブディレクトリ分散)を指定する書式も使えます:

N;/var/lib/php/sessions

N はサブディレクトリの深さ(12)で、セッションファイルが大量になる場合のディレクトリ分散に使います。

2;/var/lib/php/sessions
# → /var/lib/php/sessions/a/b/sess_ab1234...

Redis / Memcached の場合

tcp://127.0.0.1:6379           # Redis(認証なし)
tcp://127.0.0.1:6379?auth=pass # Redis(認証あり)
tcp://127.0.0.1:11211          # Memcached

基本的な使い方

<?php
// 保存先の取得
$currentPath = session_save_path();
echo "現在の保存先: " . $currentPath;
// 出力例: /var/lib/php/sessions

// 保存先の変更(session_start() より前に)
$oldPath = session_save_path('/var/lib/php/sessions/myapp');
echo "変更前: " . $oldPath;
echo "変更後: " . session_save_path();

session_start();

実践的なクラスベースの使用例

例1:アプリ専用セッションパス設定クラス

<?php
/**
 * アプリケーション専用のセッション保存ディレクトリを
 * 安全に作成・設定するクラス
 */
class AppSessionPathConfigurator
{
    public function __construct(
        private readonly string $basePath,
        private readonly string $appName
    ) {}

    /**
     * アプリ専用ディレクトリを作成し、セッション保存先に設定する
     */
    public function configure(): string
    {
        if (session_status() === PHP_SESSION_ACTIVE) {
            throw new \RuntimeException('セッション開始後はパスを変更できません');
        }

        $path = rtrim($this->basePath, '/') . '/' . $this->appName;

        $this->ensureDirectory($path);
        $this->validatePermissions($path);

        $previous = session_save_path($path);

        echo "セッション保存先を設定しました\n";
        echo "変更前: " . ($previous ?: '(php.ini デフォルト)') . "\n";
        echo "変更後: {$path}\n";

        return $path;
    }

    private function ensureDirectory(string $path): void
    {
        if (!is_dir($path)) {
            if (!mkdir($path, 0700, true)) {
                throw new \RuntimeException("ディレクトリ作成失敗: {$path}");
            }
            echo "ディレクトリを作成しました: {$path}\n";
        }
    }

    private function validatePermissions(string $path): void
    {
        if (!is_writable($path)) {
            throw new \RuntimeException("書き込み権限がありません: {$path}");
        }

        $perms = fileperms($path) & 0777;
        if ($perms & 0077) {
            // グループ・その他への権限が残っている場合に警告
            echo "⚠️ 警告: {$path} のパーミッションが緩すぎます("
                . decoct($perms) . ")。0700 を推奨します。\n";
        }
    }
}

$configurator = new AppSessionPathConfigurator('/var/lib/php/sessions', 'myshop');
// $configurator->configure();
// session_start();

例2:環境別セッションパス切り替えクラス

<?php
/**
 * 環境(本番・ステージング・開発)に応じて
 * セッション保存先を自動的に切り替えるクラス
 */
class EnvironmentSessionPathResolver
{
    private array $pathMap;

    public function __construct(array $pathMap = [])
    {
        $this->pathMap = $pathMap ?: [
            'production'  => '/var/lib/php/sessions/prod',
            'staging'     => '/var/lib/php/sessions/staging',
            'development' => '/tmp/php_sessions_dev',
            'testing'     => '/tmp/php_sessions_test',
        ];
    }

    public function apply(string $environment): string
    {
        if (session_status() === PHP_SESSION_ACTIVE) {
            throw new \RuntimeException('セッション開始後は変更できません');
        }

        if (!isset($this->pathMap[$environment])) {
            throw new \InvalidArgumentException("未定義の環境: {$environment}");
        }

        $path     = $this->pathMap[$environment];
        $previous = session_save_path($path);

        echo "環境: {$environment}\n";
        echo "セッション保存先: {$path}\n";
        echo "変更前: " . ($previous ?: '(デフォルト)') . "\n";

        return $path;
    }

    public function getCurrent(): string
    {
        return session_save_path() ?: '(php.ini デフォルト)';
    }
}

$resolver = new EnvironmentSessionPathResolver();
$resolver->apply('development');
// session_start();

/*
出力例:
環境: development
セッション保存先: /tmp/php_sessions_dev
変更前: (デフォルト)
*/

例3:Redis セッション保存先 DSN ビルダークラス

<?php
/**
 * Redis をセッションストレージとして使う場合の
 * 接続 DSN を構築して session_save_path() に適用するクラス
 */
class RedisSessionPathBuilder
{
    private array $config;

    public function __construct(array $config = [])
    {
        $this->config = array_merge([
            'host'     => '127.0.0.1',
            'port'     => 6379,
            'auth'     => null,
            'database' => 0,
            'prefix'   => 'sess_',
            'ttl'      => 1800,
            'timeout'  => 2.5,
        ], $config);
    }

    /**
     * DSN 文字列を生成する
     */
    public function buildDsn(): string
    {
        $dsn = "tcp://{$this->config['host']}:{$this->config['port']}";

        $params = [];
        if ($this->config['auth']) {
            $params[] = 'auth=' . urlencode($this->config['auth']);
        }
        if ($this->config['database'] !== 0) {
            $params[] = 'database=' . (int) $this->config['database'];
        }
        if ($this->config['prefix']) {
            $params[] = 'prefix=' . urlencode($this->config['prefix']);
        }
        $params[] = 'timeout=' . $this->config['timeout'];

        if (!empty($params)) {
            $dsn .= '?' . implode('&', $params);
        }

        return $dsn;
    }

    /**
     * セッションハンドラと保存先を一括設定して適用する
     */
    public function apply(): string
    {
        if (session_status() === PHP_SESSION_ACTIVE) {
            throw new \RuntimeException('セッション開始後は変更できません');
        }

        if (!extension_loaded('redis')) {
            throw new \RuntimeException('Redis 拡張がインストールされていません');
        }

        $dsn = $this->buildDsn();

        ini_set('session.save_handler', 'redis');
        $previous = session_save_path($dsn);

        // gc_maxlifetime を TTL に合わせる
        ini_set('session.gc_maxlifetime', (string) $this->config['ttl']);

        echo "Redis セッション設定完了\n";
        echo "DSN: {$dsn}\n";
        echo "TTL: {$this->config['ttl']} 秒\n";
        echo "変更前の保存先: " . ($previous ?: '(デフォルト)') . "\n";

        return $dsn;
    }
}

$builder = new RedisSessionPathBuilder([
    'host'     => 'redis.internal',
    'auth'     => 'secret_password',
    'database' => 1,
    'ttl'      => 3600,
]);

echo $builder->buildDsn() . "\n";
// tcp://redis.internal:6379?auth=secret_password&database=1&prefix=sess_&timeout=2.5

// $builder->apply();
// session_start();

例4:サブディレクトリ分散(深さ指定)設定クラス

<?php
/**
 * セッションファイルをサブディレクトリに分散させる設定クラス
 * 大量のセッションファイルによるディレクトリアクセス遅延を防ぐ
 *
 * session_save_path の "N;/path" 書式を扱う
 */
class SessionPathDepthConfigurator
{
    public function __construct(
        private readonly string $basePath,
        private readonly int    $depth = 2
    ) {
        if ($depth < 1 || $depth > 9) {
            throw new \InvalidArgumentException('depth は 1〜9 の範囲で指定してください');
        }
    }

    /**
     * 深さ付きパスを設定する
     * 例: "2;/var/lib/php/sessions"
     */
    public function apply(): string
    {
        if (session_status() === PHP_SESSION_ACTIVE) {
            throw new \RuntimeException('セッション開始後は変更できません');
        }

        $path     = $this->basePath;
        $fullPath = "{$this->depth};{$path}";

        // ベースディレクトリの存在確認
        if (!is_dir($path) || !is_writable($path)) {
            throw new \RuntimeException("ディレクトリが存在しないか書き込み不可: {$path}");
        }

        $previous = session_save_path($fullPath);

        echo "サブディレクトリ分散設定\n";
        echo "深さ: {$this->depth}\n";
        echo "パス: {$fullPath}\n";
        echo "変更前: " . ($previous ?: '(デフォルト)') . "\n";
        echo "期待されるファイルパス例: {$path}/a/b/sess_ab1c2d...\n";

        return $fullPath;
    }

    public function estimateSubdirs(): int
    {
        // 16^depth 個のサブディレクトリが作られる
        return (int) pow(16, $this->depth);
    }

    public function getInfo(): array
    {
        return [
            'base_path'       => $this->basePath,
            'depth'           => $this->depth,
            'full_path'       => "{$this->depth};{$this->basePath}",
            'subdir_count'    => $this->estimateSubdirs(),
            'recommendation'  => $this->depth === 1
                ? '数万セッションまで有効'
                : '数百万セッション規模に対応',
        ];
    }
}

$configurator = new SessionPathDepthConfigurator('/var/lib/php/sessions', depth: 2);
// $configurator->apply();

print_r($configurator->getInfo());

/*
出力例:
Array
(
    [base_path]      => /var/lib/php/sessions
    [depth]          => 2
    [full_path]      => 2;/var/lib/php/sessions
    [subdir_count]   => 256
    [recommendation] => 数百万セッション規模に対応
)
*/

例5:複数アプリのセッション保存先を分離する管理クラス

<?php
/**
 * 同一サーバーで複数アプリのセッションを
 * ディレクトリを分けて安全に管理するクラス
 * セッションの混在・漏洩を防ぐ
 */
class MultiAppSessionPathManager
{
    private string $rootPath;
    private array  $registeredApps = [];

    public function __construct(string $rootPath = '/var/lib/php/sessions')
    {
        $this->rootPath = rtrim($rootPath, '/');
    }

    /**
     * アプリを登録してパスを確保する
     */
    public function registerApp(string $appId, int $permissions = 0700): string
    {
        if (!preg_match('/^[a-z0-9_\-]+$/', $appId)) {
            throw new \InvalidArgumentException("appId は英小文字・数字・ハイフン・アンダースコアのみ: {$appId}");
        }

        $path = "{$this->rootPath}/{$appId}";

        if (!is_dir($path)) {
            mkdir($path, $permissions, true);
        }

        $this->registeredApps[$appId] = $path;
        echo "アプリ登録: {$appId} → {$path}\n";

        return $path;
    }

    /**
     * 指定アプリのセッション保存先を適用する
     */
    public function applyFor(string $appId): void
    {
        if (!isset($this->registeredApps[$appId])) {
            throw new \InvalidArgumentException("未登録のアプリ: {$appId}");
        }

        if (session_status() === PHP_SESSION_ACTIVE) {
            throw new \RuntimeException('セッション開始後は変更できません');
        }

        $path     = $this->registeredApps[$appId];
        $previous = session_save_path($path);

        echo "セッション保存先適用: {$appId}\n";
        echo "  パス: {$path}\n";
        echo "  変更前: " . ($previous ?: '(デフォルト)') . "\n";
    }

    /**
     * 登録済みアプリの一覧とセッションファイル数を表示する
     */
    public function reportStatus(): void
    {
        echo "=== セッションパス管理レポート ===\n";
        foreach ($this->registeredApps as $appId => $path) {
            $fileCount = is_dir($path)
                ? count(glob("{$path}/sess_*") ?: [])
                : 0;
            printf("%-20s | %s | セッション数: %d\n", $appId, $path, $fileCount);
        }
    }
}

$manager = new MultiAppSessionPathManager('/var/lib/php/sessions');
$manager->registerApp('shop');
$manager->registerApp('admin');
$manager->registerApp('api');

$manager->applyFor('shop');
// session_start();

$manager->reportStatus();

/*
出力例:
アプリ登録: shop → /var/lib/php/sessions/shop
アプリ登録: admin → /var/lib/php/sessions/admin
アプリ登録: api → /var/lib/php/sessions/api
セッション保存先適用: shop
  パス: /var/lib/php/sessions/shop
  変更前: (デフォルト)
=== セッションパス管理レポート ===
shop                 | /var/lib/php/sessions/shop  | セッション数: 0
admin                | /var/lib/php/sessions/admin | セッション数: 0
api                  | /var/lib/php/sessions/api   | セッション数: 0
*/

例6:セッション保存先の診断・ヘルスチェッククラス

<?php
/**
 * 現在のセッション保存先が正しく機能しているかを診断するクラス
 * デプロイ時のヘルスチェックや管理画面での状態確認に使用する
 */
class SessionPathHealthChecker
{
    public function check(): array
    {
        $path       = session_save_path();
        $results    = [];

        $results['raw_path']    = $path ?: '(php.ini デフォルト)';
        $results['type']        = $this->detectType($path);

        if ($results['type'] === 'filesystem') {
            // ファイルシステムの場合はディレクトリチェック
            $actualPath = $this->extractActualPath($path);
            $results['actual_path']  = $actualPath;
            $results['exists']       = is_dir($actualPath);
            $results['writable']     = is_writable($actualPath);
            $results['permissions']  = $results['exists']
                ? decoct(fileperms($actualPath) & 0777)
                : 'N/A';
            $results['session_count'] = $results['exists']
                ? count(glob("{$actualPath}/sess_*") ?: [])
                : 0;
            $results['disk_free_mb'] = $results['exists']
                ? round(disk_free_space($actualPath) / 1024 / 1024, 1)
                : 0;
            $results['status'] = $results['exists'] && $results['writable']
                ? '✅ 正常'
                : '❌ 異常';
        } else {
            // Redis / Memcached の場合は DSN 情報のみ
            $results['status'] = '⚠️ 外部ストレージ(接続テストは別途必要)';
        }

        return $results;
    }

    private function detectType(string $path): string
    {
        if (str_starts_with($path, 'tcp://') || str_starts_with($path, 'unix://')) {
            return 'network';
        }
        return 'filesystem';
    }

    private function extractActualPath(string $path): string
    {
        // "N;/actual/path" 形式から実パスを取り出す
        if (preg_match('/^\d+;(.+)$/', $path, $m)) {
            return $m[1];
        }
        return $path ?: sys_get_temp_dir();
    }

    public function printReport(): void
    {
        $results = $this->check();
        echo "=== セッション保存先 ヘルスチェック ===\n";
        foreach ($results as $key => $value) {
            printf("%-20s: %s\n", $key, $value);
        }
    }
}

$checker = new SessionPathHealthChecker();
$checker->printReport();

/*
出力例(ファイルシステム):
=== セッション保存先 ヘルスチェック ===
raw_path            : /var/lib/php/sessions
type                : filesystem
actual_path         : /var/lib/php/sessions
exists              : 1
writable            : 1
permissions         : 700
session_count       : 42
disk_free_mb        : 15420.3
status              : ✅ 正常
*/

よくある使用シーン

シーン設定例
アプリ専用ディレクトリ分離session_save_path('/var/lib/php/sessions/myapp')
大量セッションのディレクトリ分散session_save_path('2;/var/lib/php/sessions')
Redis によるセッション共有session_save_path('tcp://redis:6379?auth=pass')
Memcached によるセッション共有session_save_path('tcp://memcached:11211')
テスト環境の一時パスsession_save_path('/tmp/php_test_sessions')

関連関数との比較

関数役割
session_save_path()セッションデータの保存先パス(ディレクトリ or DSN)の取得・変更
session_module_name()保存モジュール(files / redis 等)の取得・変更
session_name()セッションIDを格納するCookie名の取得・変更
session_set_save_handler()カスタムセッションハンドラの登録
ini_set('session.save_path', ...)php.ini レベルで保存先を設定(session_save_path() と同等)

よくある落とし穴

<?php
// ❌ NG:session_start() 後に変更しても無視される
session_start();
session_save_path('/var/lib/php/sessions/myapp'); // 警告・無効

// ✅ OK:session_start() の前に変更する
session_save_path('/var/lib/php/sessions/myapp');
session_start();
<?php
// ❌ 注意:ディレクトリが存在しない・書き込み不可だとセッション開始が失敗する
session_save_path('/nonexistent/path');
session_start(); // Warning: Failed to read session data

// ✅ ディレクトリの存在と書き込み権限を事前に確認する
$path = '/var/lib/php/sessions/myapp';
if (!is_dir($path)) {
    mkdir($path, 0700, true);
}
session_save_path($path);
session_start();
<?php
// ❌ よくある誤解:Redis を使うときに session_save_path() だけ設定しても動かない
session_save_path('tcp://127.0.0.1:6379');
session_start(); // files モジュールのまま → エラー

// ✅ save_handler も合わせて設定する
ini_set('session.save_handler', 'redis');
session_save_path('tcp://127.0.0.1:6379');
session_start();

まとめ

項目内容
関数名session_save_path(?string $path): string|false
主な用途セッションデータの保存先ディレクトリまたは DSN の取得・変更
呼び出しタイミング必ず session_start() より前
戻り値現在(または変更前)のパス
ファイル分散"N;/path" 書式でサブディレクトリ深さを指定可能
Redis/Memcachedtcp://host:port 形式の DSN を指定
注意点ディレクトリ存在確認・書き込み権限確認・save_handler の合わせ設定が必要

session_save_path() は設定一行ながら、セキュリティ・パフォーマンス・スケーラビリティに直結する重要な関数です。本番環境ではアプリ専用パスの設定、大規模サービスではサブディレクトリ分散や Redis への切り替えをセットで検討しましょう。


参考リンク

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