[PHP]session_reset()完全解説|セッションデータの変更を取り消す「元に戻す」機能の使い方

PHP

はじめに

セッションを使った処理の中で、「途中まで変更したセッションデータを、まだ保存していないうちに元に戻したい」という場面はありませんか? たとえばウィザード形式のフォームで途中キャンセルした場合や、トランザクション的にセッションを扱いたい場合などです。

session_reset() は、現在のリクエスト中にセッション変数へ加えた変更をすべて取り消し、ストレージに保存されている元のデータに戻す関数です。session_write_close() でまだ書き込んでいない状態であれば、まるで変更がなかったかのようにリセットできます。データベースでいう「ROLLBACK」に相当するイメージです。


関数の概要

項目内容
関数名session_reset()
所属PHP セッション関数
導入バージョンPHP 5.6.0以降
PHP 8.x対応済み
戻り値true(成功) / false(失敗)

構文

session_reset(): bool

パラメータはありません。呼び出すだけで $_SESSION の内容がストレージから読み込んだ元の状態に戻ります。


session_reset() が行うこと・行わないこと

項目動作
$_SESSION の変更を取り消す✅ 行う
ストレージ(ファイル等)の内容は変更しない✅ 変更しない(読み直すだけ)
セッションIDは変更しない✅ 変更しない
セッションを破棄しない✅ 破棄しない
session_write_close() 後の変更には無効⚠️ 既に書き込まれた内容には戻せない

⚠️ 注意session_reset() が有効なのは、同一リクエスト内でまだ session_write_close()session_destroy() を呼んでいない間だけです。書き込み後は元のデータがすでにストレージへ反映されているため、呼んでも意味がありません。


session_reset() vs 関連関数の違い

関数セッションIDデータストレージ
session_reset()維持元の状態に戻す変更なし
session_unset()維持すべて削除(空に)変更なし(書き込み前)
session_destroy()削除削除ファイル削除
session_abort()維持変更を破棄(書き込みもしない)変更なし

基本的な使い方

<?php
session_start();

// ストレージに保存されている状態(例)
// $_SESSION = ['user_id' => 1, 'cart' => ['A', 'B']]

// リクエスト中に変更
$_SESSION['cart'][] = 'C';
$_SESSION['temp']   = 'working';

var_dump($_SESSION['cart']); // ['A', 'B', 'C']

// やっぱり元に戻したい
session_reset();

var_dump($_SESSION['cart']); // ['A', 'B'] ← 元に戻った
isset($_SESSION['temp']);    // false ← 削除された

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

例1:セッションのトランザクション管理クラス

<?php
/**
 * セッション操作をトランザクション的に扱うクラス
 * commit() で確定、rollback() で session_reset() による取り消しを行う
 */
class SessionTransaction
{
    private bool $inTransaction = false;
    private array $snapshot     = [];

    public function __construct()
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
    }

    /**
     * トランザクション開始:現在のセッションをスナップショット
     */
    public function begin(): void
    {
        if ($this->inTransaction) {
            throw new \RuntimeException('トランザクションは既に開始されています');
        }
        $this->snapshot      = $_SESSION;
        $this->inTransaction = true;
        echo "トランザクション開始\n";
    }

    /**
     * コミット:変更をそのまま確定(session_write_close で書き込まれる)
     */
    public function commit(): void
    {
        $this->assertInTransaction();
        $this->inTransaction = false;
        $this->snapshot      = [];
        echo "コミット完了\n";
    }

    /**
     * ロールバック:session_reset() で変更を取り消す
     */
    public function rollback(): void
    {
        $this->assertInTransaction();

        $result = session_reset();

        if ($result) {
            echo "ロールバック成功: セッションを元の状態に戻しました\n";
        } else {
            // session_reset() が失敗した場合はスナップショットで復元
            $_SESSION = $this->snapshot;
            echo "ロールバック(スナップショット復元)\n";
        }

        $this->inTransaction = false;
        $this->snapshot      = [];
    }

    private function assertInTransaction(): void
    {
        if (!$this->inTransaction) {
            throw new \RuntimeException('トランザクションが開始されていません');
        }
    }

    public function isInTransaction(): bool
    {
        return $this->inTransaction;
    }
}

// 利用例
$tx = new SessionTransaction();

// 初期状態(ストレージ)
$_SESSION['balance'] = 10000;
$_SESSION['history'] = [];

$tx->begin();

$_SESSION['balance'] -= 3000;
$_SESSION['history'][] = '送金 -3000円';
echo "変更後残高: {$_SESSION['balance']}円\n"; // 7000

// 何らかのエラーで取り消す
$tx->rollback();

echo "復元後残高: {$_SESSION['balance']}円\n"; // 10000

/*
出力例:
トランザクション開始
変更後残高: 7000円
ロールバック成功: セッションを元の状態に戻しました
復元後残高: 10000円
*/

例2:ウィザードフォームのキャンセル処理クラス

<?php
/**
 * 複数ステップのウィザードフォームで、
 * ユーザーがキャンセルしたときにセッションを元の状態に戻すクラス
 */
class WizardSessionManager
{
    private const SESSION_KEY = 'wizard_data';

    public function __construct(private readonly string $wizardId)
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
    }

    /**
     * ウィザードのステップデータを保存(まだコミットしない)
     */
    public function saveStep(int $step, array $data): void
    {
        $_SESSION[self::SESSION_KEY][$this->wizardId]['step' . $step] = $data;
        $_SESSION[self::SESSION_KEY][$this->wizardId]['current_step'] = $step;
        echo "ステップ{$step} 一時保存\n";
    }

    /**
     * 全ステップ完了:セッションを確定させる
     * (session_write_close は呼ばないが、このまま終了すれば書き込まれる)
     */
    public function complete(): array
    {
        $data = $_SESSION[self::SESSION_KEY][$this->wizardId] ?? [];
        $_SESSION[self::SESSION_KEY][$this->wizardId]['completed'] = true;
        echo "ウィザード完了\n";
        return $data;
    }

    /**
     * キャンセル:session_reset() でこのリクエスト中の変更を取り消す
     */
    public function cancel(): void
    {
        $result = session_reset();
        if ($result) {
            echo "ウィザードキャンセル: セッションをリクエスト前の状態に戻しました\n";
        } else {
            // 手動で対象キーを削除してフォールバック
            unset($_SESSION[self::SESSION_KEY][$this->wizardId]);
            echo "ウィザードキャンセル(手動削除)\n";
        }
    }

    public function getCurrentStep(): int
    {
        return $_SESSION[self::SESSION_KEY][$this->wizardId]['current_step'] ?? 0;
    }
}

// 利用例
$wizard = new WizardSessionManager('order_wizard');

// ステップ1・2を入力
$wizard->saveStep(1, ['name' => '山田太郎', 'email' => 'yamada@example.com']);
$wizard->saveStep(2, ['address' => '神戸市中央区', 'zip' => '650-0000']);

echo "現在のステップ: " . $wizard->getCurrentStep() . "\n"; // 2

// ユーザーがキャンセルボタンを押した
$wizard->cancel();

echo "キャンセル後のステップ: " . $wizard->getCurrentStep() . "\n"; // 0

/*
出力例:
ステップ1 一時保存
ステップ2 一時保存
現在のステップ: 2
ウィザードキャンセル: セッションをリクエスト前の状態に戻しました
キャンセル後のステップ: 0
*/

例3:セッションデータの楽観的ロッククラス

<?php
/**
 * セッションデータにバージョン番号を付与し、
 * 競合が発生した場合に session_reset() で変更を破棄する
 * 楽観的ロック(Optimistic Locking)パターンの実装
 */
class OptimisticSessionLock
{
    private int $readVersion;

    public function __construct()
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        // 読み込み時のバージョンを記録
        $this->readVersion = $_SESSION['_version'] ?? 0;
    }

    /**
     * バージョンが一致していれば変更を確定、不一致なら rollback
     */
    public function trySave(callable $mutation): bool
    {
        // 変更処理を適用
        $mutation();

        // バージョンの一致チェック(他リクエストが先に書き込んでいないか)
        $currentVersion = $_SESSION['_version'] ?? 0;

        if ($currentVersion !== $this->readVersion) {
            // 競合検出:変更を取り消す
            session_reset();
            echo "競合検出: 変更を取り消しました (read={$this->readVersion}, current={$currentVersion})\n";
            return false;
        }

        // バージョンをインクリメントして確定
        $_SESSION['_version'] = $currentVersion + 1;
        echo "保存成功: バージョン {$_SESSION['_version']}\n";
        return true;
    }

    public function getVersion(): int
    {
        return $_SESSION['_version'] ?? 0;
    }
}

// 初期状態
$_SESSION['_version'] = 3;
$_SESSION['stock']    = 5;

$lock = new OptimisticSessionLock();

$success = $lock->trySave(function () {
    $_SESSION['stock'] -= 1; // 在庫を1減らす
});

if ($success) {
    echo "在庫更新完了: {$_SESSION['stock']}個\n";
} else {
    echo "再試行してください\n";
}

/*
出力例(競合なし):
保存成功: バージョン 4
在庫更新完了: 4個
*/

例4:A/Bテスト用セッション分岐クラス

<?php
/**
 * A/Bテストで「テストグループへの仮割り当て」を試み、
 * 条件を満たさない場合は session_reset() で元に戻すクラス
 */
class AbTestSessionManager
{
    public function __construct()
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
    }

    /**
     * ユーザーをテストグループに仮割り当てし、
     * 定員を超えていれば session_reset() で取り消す
     */
    public function tryAssign(string $testName, string $variant, int $maxUsers): bool
    {
        // 現在の割り当て数を確認(実際はDBで管理するが、ここではセッションで模擬)
        $currentCount = $_SESSION['ab_counts'][$testName][$variant] ?? 0;

        if ($currentCount >= $maxUsers) {
            echo "定員超過: {$testName}/{$variant} ({$currentCount}/{$maxUsers})\n";
            return false;
        }

        // 仮割り当て
        $_SESSION['ab_test'][$testName]           = $variant;
        $_SESSION['ab_counts'][$testName][$variant] = $currentCount + 1;
        $_SESSION['ab_assigned_at'][$testName]    = time();

        // 追加条件チェック(例:既に別テストに参加中でないか)
        if ($this->hasConflict($testName)) {
            session_reset(); // 仮割り当てを取り消し
            echo "競合テストあり: {$testName} への割り当てを取り消しました\n";
            return false;
        }

        echo "割り当て成功: {$testName} → {$variant}\n";
        return true;
    }

    private function hasConflict(string $testName): bool
    {
        // 例:同時に3つ以上のテストには参加させない
        $activeTests = array_keys($_SESSION['ab_test'] ?? []);
        return count($activeTests) > 3;
    }

    public function getVariant(string $testName): ?string
    {
        return $_SESSION['ab_test'][$testName] ?? null;
    }
}

$abManager = new AbTestSessionManager();
$abManager->tryAssign('checkout_flow', 'variant_b', 100);
echo "割り当てバリアント: " . ($abManager->getVariant('checkout_flow') ?? 'なし') . "\n";

例5:セッション変更の差分検出クラス

<?php
/**
 * session_reset() を使ってリクエスト中のセッション変更差分を検出するクラス
 * デバッグ・監査ログ・変更通知などに活用する
 */
class SessionDiffDetector
{
    private array $before = [];

    public function __construct()
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        // 元の状態をスナップショット
        $this->before = $_SESSION;
    }

    /**
     * 現在の $_SESSION と元の状態を比較して差分を返す
     * session_reset() はせず差分情報のみ返す(呼び出し側が判断)
     */
    public function getDiff(): array
    {
        $after = $_SESSION;
        $diff  = [
            'added'   => [],
            'removed' => [],
            'changed' => [],
        ];

        foreach ($after as $key => $value) {
            if (!array_key_exists($key, $this->before)) {
                $diff['added'][$key] = $value;
            } elseif ($this->before[$key] !== $value) {
                $diff['changed'][$key] = [
                    'before' => $this->before[$key],
                    'after'  => $value,
                ];
            }
        }

        foreach ($this->before as $key => $value) {
            if (!array_key_exists($key, $after)) {
                $diff['removed'][$key] = $value;
            }
        }

        return $diff;
    }

    /**
     * 変更があれば差分を表示し、必要なら session_reset() で取り消す
     */
    public function reviewAndMaybeReset(bool $doReset = false): void
    {
        $diff = $this->getDiff();
        $hasChanges = !empty($diff['added']) || !empty($diff['removed']) || !empty($diff['changed']);

        if (!$hasChanges) {
            echo "セッションに変更はありません\n";
            return;
        }

        echo "=== セッション変更差分 ===\n";
        foreach ($diff['added'] as $key => $val) {
            echo "  追加: {$key} = " . json_encode($val, JSON_UNESCAPED_UNICODE) . "\n";
        }
        foreach ($diff['removed'] as $key => $val) {
            echo "  削除: {$key}\n";
        }
        foreach ($diff['changed'] as $key => $change) {
            echo "  変更: {$key}: "
                . json_encode($change['before'], JSON_UNESCAPED_UNICODE)
                . " → "
                . json_encode($change['after'], JSON_UNESCAPED_UNICODE) . "\n";
        }

        if ($doReset) {
            session_reset();
            echo "→ session_reset() により変更を取り消しました\n";
        }
    }
}

// 初期状態
$_SESSION['user_id']  = 1;
$_SESSION['cart']     = ['apple'];
$_SESSION['language'] = 'ja';

$detector = new SessionDiffDetector();

// リクエスト中に変更
$_SESSION['cart'][]   = 'banana';
$_SESSION['theme']    = 'dark';
unset($_SESSION['language']);

$detector->reviewAndMaybeReset(doReset: true);
echo "リセット後 cart: " . implode(', ', $_SESSION['cart']) . "\n";

/*
出力例:
=== セッション変更差分 ===
  追加: theme = "dark"
  削除: language
  変更: cart: ["apple"] → ["apple","banana"]
→ session_reset() により変更を取り消しました
リセット後 cart: apple
*/

例6:エラー発生時のセッション自動ロールバッククラス

<?php
/**
 * 処理中に例外が発生した場合、自動的に session_reset() を呼び出すクラス
 * try-catch のロールバック処理を自動化するラッパー
 */
class SessionSafeExecutor
{
    private array $log = [];

    public function __construct()
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
    }

    /**
     * コールバックを実行し、例外が発生したらセッションをロールバックする
     *
     * @param callable $action      実行する処理
     * @param callable|null $onError エラー時のコールバック
     * @return mixed 正常終了時の戻り値
     */
    public function execute(callable $action, ?callable $onError = null): mixed
    {
        $this->log[] = ['event' => 'execute_start', 'session_size' => count($_SESSION)];

        try {
            $result = $action();
            $this->log[] = ['event' => 'execute_success'];
            return $result;

        } catch (\Throwable $e) {
            // セッション変更をロールバック
            $resetResult = session_reset();

            $this->log[] = [
                'event'        => 'execute_error',
                'error'        => $e->getMessage(),
                'reset_result' => $resetResult,
            ];

            echo "エラー発生: " . $e->getMessage() . "\n";
            echo "セッション " . ($resetResult ? 'ロールバック成功' : 'ロールバック失敗') . "\n";

            if ($onError !== null) {
                return $onError($e);
            }

            throw $e;
        }
    }

    public function getLog(): array
    {
        return $this->log;
    }
}

// 初期状態
$_SESSION['points'] = 500;
$_SESSION['status'] = 'active';

$executor = new SessionSafeExecutor();

try {
    $executor->execute(function () {
        $_SESSION['points'] -= 200;
        $_SESSION['status']  = 'processing';
        echo "ポイント消費: {$_SESSION['points']}pt\n";

        // 何らかの例外が発生
        throw new \RuntimeException('決済サーバーエラー');
    });
} catch (\RuntimeException $e) {
    echo "最終エラー: " . $e->getMessage() . "\n";
}

echo "ロールバック後 points: {$_SESSION['points']}pt\n"; // 500
echo "ロールバック後 status: {$_SESSION['status']}\n";   // active

/*
出力例:
ポイント消費: 300pt
エラー発生: 決済サーバーエラー
セッション ロールバック成功
最終エラー: 決済サーバーエラー
ロールバック後 points: 500pt
ロールバック後 status: active
*/

関連関数との比較(再掲・詳細版)

関数データ操作ストレージIDユースケース
session_reset()元の状態に戻す変更なし維持変更のロールバック
session_abort()変更を破棄(書き込みなし)変更なし維持読み取り専用アクセス後のクリーンな終了
session_unset()全変数を削除(空に)変更なし(書き込み前)維持ログアウト前のデータ削除
session_destroy()全データ削除ファイル削除削除完全なログアウト
session_write_close()変更を確定・書き込み書き込む維持早期ロック解放

よくある落とし穴

<?php
// ❌ NG:session_write_close() 後に session_reset() を呼んでも無効
session_start();
$_SESSION['key'] = 'new_value';
session_write_close(); // ストレージに書き込み済み

session_start();       // 再オープン
session_reset();       // 「書き込み済みの new_value」が元の状態なので意味がない

// ✅ session_reset() は session_write_close() の前にのみ有効
session_start();
$_SESSION['key'] = 'new_value';
session_reset();       // ← 書き込み前なので元に戻る
<?php
// ❌ よくある誤解:session_reset() はセッションIDも変えると思っている
session_start();
$oldId = session_id();
$_SESSION['foo'] = 'bar';
session_reset();
$newId = session_id();

var_dump($oldId === $newId); // true ← IDは変わらない
// IDを変えたい場合は session_regenerate_id() を使う
<?php
// ❌ 注意:セッションが開始されていない状態での呼び出し
session_reset(); // Warning: Session is not active

// ✅ セッション開始後に呼ぶ
session_start();
session_reset();

まとめ

項目内容
関数名session_reset(): bool
主な用途リクエスト中のセッション変更をストレージの元の状態に戻す
導入バージョンPHP 5.6.0以降
呼び出しタイミングsession_start() 後・session_write_close()
セッションID変更しない
ストレージ変更しない
戻り値成功 true / 失敗 false
イメージDBの ROLLBACK に相当

session_reset() は「書き込む前なら何度でも元に戻せる」という安全網です。ウィザードフォームのキャンセル、エラー発生時の自動ロールバック、セッションの楽観的ロックなど、**「まだ確定していない変更を取り消す」**あらゆる場面で活躍します。session_abort() と合わせて覚えておくと、セッション操作の選択肢が大きく広がります。


参考リンク

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