[PHP]session_register_shutdown()完全解説|セッションの安全なシャットダウン処理と自動書き込みの仕組み

PHP

はじめに

PHPのセッションは通常、スクリプト終了時に自動で書き込み・クローズされます。しかしフレームワークやカスタムハンドラを使う場合、セッションの終了タイミングを明示的にコントロールしたい場面が出てきます。

session_register_shutdown() は、PHPスクリプトのシャットダウン時に session_write_close() を自動で呼び出すよう登録する関数です。register_shutdown_function()session_write_close を手動登録するのと同等ですが、より安全でPHP組み込みの方法として提供されています。地味ながら、セッションの書き込み漏れや二重クローズを防ぐ上で重要な役割を担います。


関数の概要

項目内容
関数名session_register_shutdown()
所属PHP セッション関数
導入バージョンPHP 5.4.0以降
PHP 8.x対応済み
戻り値なし(void

構文

session_register_shutdown(): void

パラメータはありません。呼び出すだけでシャットダウン時の session_write_close() が登録されます。


なぜこの関数が必要なのか

PHPはデフォルトで session_start() を呼び出した際に自動的にシャットダウンハンドラを登録します。しかし以下のケースでは明示的な登録が必要になります。

ケース説明
カスタムセッションハンドラ使用時session_set_save_handler() でオブジェクトを登録した場合
フレームワークの独自セッション管理標準の自動登録が無効化されている場合
session_write_close() を途中で呼んだ後再開後の終了保証が必要な場合
シャットダウン順序の制御他の register_shutdown_function() との実行順を意識する場合

session_register_shutdown() vs 手動登録の違い

// 方法①:session_register_shutdown()(推奨)
session_start();
session_register_shutdown();

// 方法②:register_shutdown_function() で手動登録(同等だが非推奨)
session_start();
register_shutdown_function('session_write_close');

session_register_shutdown() はPHP内部で重複登録を防ぐ仕組みを持っており、より安全です。手動登録では複数回呼んだ場合に session_write_close() が重複実行されることがあります。


基本的な使い方

<?php
session_start();
session_register_shutdown(); // シャットダウン時に session_write_close() を自動呼び出し

$_SESSION['last_access'] = time();

// スクリプト終了時に自動的にセッションが書き込まれる
echo "処理完了\n";

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

例1:カスタムセッションハンドラとのセットアップクラス

<?php
/**
 * カスタムセッションハンドラと session_register_shutdown() を
 * 安全に組み合わせる基底クラス
 */
class CustomSessionBase implements \SessionHandlerInterface
{
    protected bool $isRegistered = false;

    /**
     * セッションを初期化し、シャットダウンハンドラを登録する
     */
    public function initSession(string $sessionName = 'AppSession'): void
    {
        if (session_status() === PHP_SESSION_ACTIVE) {
            return;
        }

        // ハンドラを登録(第2引数 false でシャットダウン自動登録を抑制)
        session_set_save_handler($this, false);

        session_name($sessionName);
        session_start();

        // 明示的にシャットダウンハンドラを登録
        if (!$this->isRegistered) {
            session_register_shutdown();
            $this->isRegistered = true;
            echo "シャットダウンハンドラ登録済み\n";
        }
    }

    // --- SessionHandlerInterface の実装 ---

    public function open(string $path, string $name): bool
    {
        echo "セッション OPEN: {$name}\n";
        return true;
    }

    public function close(): bool
    {
        echo "セッション CLOSE\n";
        return true;
    }

    public function read(string $id): string|false
    {
        echo "セッション READ: " . substr($id, 0, 8) . "...\n";
        return '';
    }

    public function write(string $id, string $data): bool
    {
        echo "セッション WRITE: " . strlen($data) . " bytes\n";
        return true;
    }

    public function destroy(string $id): bool
    {
        echo "セッション DESTROY\n";
        return true;
    }

    public function gc(int $max_lifetime): int|false
    {
        return 0;
    }
}

$handler = new CustomSessionBase();
$handler->initSession();
$_SESSION['foo'] = 'bar';
// スクリプト終了時に自動で write → close が呼ばれる

例2:セッション書き込み保証クラス(ロック管理付き)

<?php
/**
 * セッションの書き込みとロック解放を確実に行うクラス
 * 長時間処理でセッションロックが続くことを防ぎ、
 * 終了時には必ずシャットダウンハンドラで後処理する
 */
class SessionWriteGuard
{
    private bool $isClosed = false;
    private float $startTime;

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

        session_register_shutdown(); // 終了時の書き込みを保証
        $this->startTime = microtime(true);
    }

    /**
     * セッションへのデータ書き込みが完了したらロックを早期解放する
     * これにより同一ユーザーからの並行リクエストのブロックを防ぐ
     */
    public function releaseEarly(): void
    {
        if (!$this->isClosed) {
            session_write_close();
            $this->isClosed = true;
            $elapsed = round((microtime(true) - $this->startTime) * 1000, 1);
            echo "セッションロック解放({$elapsed}ms 経過時点)\n";
        }
    }

    /**
     * セッションを再オープンして書き込む
     */
    public function reopen(): void
    {
        if ($this->isClosed) {
            session_start();
            $this->isClosed = false;
            session_register_shutdown(); // 再登録
            echo "セッション再オープン: " . session_id() . "\n";
        }
    }

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

$guard = new SessionWriteGuard();
$_SESSION['cart'] = ['item_id' => 101, 'qty' => 2];

// セッションへの書き込みが終わったら早期解放
$guard->releaseEarly();

// 以降の重い処理はセッションロックなしで実行
// sleep(5); // 重いAPI呼び出しなど

echo "処理完了(セッションロックは既に解放済み)\n";
// スクリプト終了時、session_register_shutdown() による後処理が走る

例3:非同期ジョブキュー処理でのセッション分離クラス

<?php
/**
 * バックグラウンドジョブ処理など、長時間実行スクリプトで
 * セッション処理を安全に分離・管理するクラス
 */
class LongRunningSessionManager
{
    private array $sessionSnapshots = [];

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

    /**
     * 処理開始前にセッションのスナップショットを取り、
     * ロックを解放してからジョブを実行する
     */
    public function runWithoutLock(callable $job): mixed
    {
        // 現在のセッションデータをスナップショット
        $snapshot = $_SESSION;
        $sessionId = session_id();

        // セッションロックを解放(他リクエストが待たないようにする)
        session_write_close();
        echo "セッションロック解放: " . substr($sessionId, 0, 8) . "...\n";

        // ジョブを実行(この間セッションはロックされない)
        $result = $job();

        // ジョブ完了後にセッションを再開して結果を書き込む
        session_start();
        session_register_shutdown(); // 再オープン後に再登録

        $_SESSION['last_job_result'] = $result;
        $_SESSION['last_job_at']     = time();

        echo "ジョブ結果をセッションに保存\n";

        return $result;
    }

    /**
     * セッションIDを保持したままデータをリセットする
     */
    public function softReset(array $preserve = []): void
    {
        $preserved = [];
        foreach ($preserve as $key) {
            if (isset($_SESSION[$key])) {
                $preserved[$key] = $_SESSION[$key];
            }
        }

        session_unset();

        foreach ($preserved as $key => $value) {
            $_SESSION[$key] = $value;
        }

        echo "セッションデータをリセット(保持キー: " . implode(', ', $preserve) . ")\n";
    }
}

$manager = new LongRunningSessionManager();

$result = $manager->runWithoutLock(function () {
    // 時間のかかる処理(API呼び出し・バッチ処理など)
    echo "重い処理を実行中...\n";
    // sleep(3);
    return ['status' => 'ok', 'processed' => 150];
});

print_r($result);

例4:セッション終了フックの管理クラス

<?php
/**
 * セッションシャットダウン時に複数の後処理フックを順番に実行するクラス
 * session_register_shutdown() と組み合わせてクリーンアップ処理を一元管理する
 */
class SessionShutdownHookManager
{
    private array $hooks = [];
    private static bool $registered = false;

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

        if (!self::$registered) {
            session_register_shutdown();

            // セッションの書き込み後に実行するカスタムフックを登録
            register_shutdown_function(function () {
                $this->runHooks();
            });

            self::$registered = true;
        }
    }

    /**
     * シャットダウン時に実行するフックを追加する
     */
    public function addHook(string $name, callable $hook, int $priority = 10): void
    {
        $this->hooks[] = [
            'name'     => $name,
            'callback' => $hook,
            'priority' => $priority,
        ];

        // 優先度順にソート
        usort($this->hooks, fn($a, $b) => $a['priority'] <=> $b['priority']);
        echo "フック登録: {$name} (priority={$priority})\n";
    }

    private function runHooks(): void
    {
        echo "\n--- シャットダウンフック実行 ---\n";
        foreach ($this->hooks as $hook) {
            try {
                ($hook['callback'])();
                echo "✅ {$hook['name']} 完了\n";
            } catch (\Throwable $e) {
                echo "❌ {$hook['name']} 失敗: " . $e->getMessage() . "\n";
            }
        }
    }
}

$hookManager = new SessionShutdownHookManager();

$hookManager->addHook('アクセスログ記録', function () {
    // $_SESSION にアクセス情報を記録する処理
    $_SESSION['last_page'] = $_SERVER['REQUEST_URI'] ?? '/';
}, priority: 5);

$hookManager->addHook('カート金額再計算', function () {
    // セッション内のカートデータを整合性チェック
    if (isset($_SESSION['cart'])) {
        $_SESSION['cart_total'] = array_sum(
            array_column($_SESSION['cart'], 'price')
        );
    }
}, priority: 10);

$hookManager->addHook('一時データ削除', function () {
    unset($_SESSION['_flash'], $_SESSION['_temp']);
}, priority: 20);

$_SESSION['cart'] = [
    ['item' => 'Book A', 'price' => 1200],
    ['item' => 'Book B', 'price' => 800],
];

echo "メイン処理完了\n";
// スクリプト終了時にフックが順番に実行される

/*
出力例(終了時):
--- シャットダウンフック実行 ---
✅ アクセスログ記録 完了
✅ カート金額再計算 完了
✅ 一時データ削除 完了
*/

例5:テスト用セッションライフサイクル検証クラス

<?php
/**
 * session_register_shutdown() の動作を検証するテストユーティリティ
 * ユニットテスト環境でセッションのライフサイクルをトレースする
 */
class SessionLifecycleTracer
{
    private array $events = [];

    public function __construct()
    {
        $this->trace('constructor');

        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
            $this->trace('session_start');
        }

        session_register_shutdown();
        $this->trace('session_register_shutdown');

        // デバッグ用:終了時のイベントもトレース
        register_shutdown_function(function () {
            $this->trace('shutdown_begin');
            // この時点では session_write_close() はまだ呼ばれていない
            // (session_register_shutdown が後で呼ぶ)
        });
    }

    private function trace(string $event): void
    {
        $this->events[] = [
            'event'      => $event,
            'session_id' => session_id() ? substr(session_id(), 0, 8) . '...' : 'none',
            'status'     => $this->statusLabel(session_status()),
            'time_ms'    => round(microtime(true) * 1000),
        ];
    }

    private function statusLabel(int $s): string
    {
        return match ($s) {
            PHP_SESSION_DISABLED => 'DISABLED',
            PHP_SESSION_NONE     => 'NONE',
            PHP_SESSION_ACTIVE   => 'ACTIVE',
            default              => 'UNKNOWN',
        };
    }

    public function printTrace(): void
    {
        echo "=== セッションライフサイクル トレース ===\n";
        foreach ($this->events as $i => $e) {
            printf(
                "#%d %-30s | ID: %-12s | Status: %s\n",
                $i + 1,
                $e['event'],
                $e['session_id'],
                $e['status']
            );
        }
    }
}

$tracer = new SessionLifecycleTracer();
$_SESSION['traced'] = true;
$tracer->printTrace();

/*
出力例:
=== セッションライフサイクル トレース ===
#1 constructor                  | ID: none         | Status: NONE
#2 session_start                | ID: a3f8bc12...  | Status: ACTIVE
#3 session_register_shutdown    | ID: a3f8bc12...  | Status: ACTIVE
*/

例6:フレームワーク統合用セッションブートストラッパー

<?php
/**
 * フレームワークのブートストラップ処理でセッションを安全に初期化するクラス
 * session_register_shutdown() を中心に据えた一元管理の実装例
 */
class SessionBootstrapper
{
    private static bool $booted = false;
    private array $config;

    public function __construct(array $config = [])
    {
        $this->config = array_merge([
            'name'            => 'FwSession',
            'gc_maxlifetime'  => 1800,
            'cookie_httponly' => true,
            'cookie_secure'   => false,
            'cookie_samesite' => 'Lax',
            'auto_shutdown'   => true,  // session_register_shutdown を使うか
        ], $config);
    }

    public function boot(): void
    {
        if (self::$booted) {
            echo "既にブート済みです\n";
            return;
        }

        if (session_status() === PHP_SESSION_ACTIVE) {
            throw new \RuntimeException('セッションが既に開始されています');
        }

        $this->applyIniSettings();
        session_name($this->config['name']);
        session_start();

        if ($this->config['auto_shutdown']) {
            session_register_shutdown();
            echo "✅ session_register_shutdown() 登録済み\n";
        }

        self::$booted = true;
        echo "✅ セッションブート完了: " . session_name() . " / " . substr(session_id(), 0, 8) . "...\n";
    }

    private function applyIniSettings(): void
    {
        ini_set('session.gc_maxlifetime',  (string) $this->config['gc_maxlifetime']);
        ini_set('session.cookie_httponly', $this->config['cookie_httponly'] ? '1' : '0');
        ini_set('session.cookie_secure',   $this->config['cookie_secure']   ? '1' : '0');
        ini_set('session.cookie_samesite', $this->config['cookie_samesite']);
        ini_set('session.use_strict_mode', '1');
    }

    public static function reset(): void
    {
        self::$booted = false;
    }

    public static function isBooted(): bool
    {
        return self::$booted;
    }
}

$bootstrapper = new SessionBootstrapper([
    'name'           => 'MyFwSession',
    'gc_maxlifetime' => 3600,
    'auto_shutdown'  => true,
]);

$bootstrapper->boot();
echo "ブート済み: " . (SessionBootstrapper::isBooted() ? 'YES' : 'NO') . "\n";

/*
出力例:
✅ session_register_shutdown() 登録済み
✅ セッションブート完了: MyFwSession / d92e44ab...
ブート済み: YES
*/

session_set_save_handler() 第2引数との関係

<?php
// session_set_save_handler() の第2引数 $register_shutdown を理解する

$handler = new MySessionHandler();

// 第2引数 true(デフォルト):内部で session_register_shutdown() を自動呼び出す
session_set_save_handler($handler, true);

// 第2引数 false:自動登録しない → 明示的に呼ぶ必要がある
session_set_save_handler($handler, false);
session_start();
session_register_shutdown(); // ← 自分で登録する

カスタムハンドラを使う場合は false にして session_register_shutdown() を明示的に呼ぶほうが、処理の流れが明確になります。


関連関数との比較

関数役割
session_register_shutdown()シャットダウン時に session_write_close() を自動実行するよう登録
session_write_close()セッションデータを即時書き込んでロックを解放する
session_abort()セッションの変更を破棄してロックを解放する(書き込みなし)
session_set_save_handler()カスタムセッションハンドラを登録(第2引数でシャットダウン登録を制御)
register_shutdown_function()汎用シャットダウン関数登録(session_write_close の手動登録に使われていた)

よくある落とし穴

<?php
// ❌ NG:session_start() の前に呼んでも意味がない(セッションが未開始)
session_register_shutdown();
session_start(); // 順序が逆

// ✅ OK:session_start() の後に登録する
session_start();
session_register_shutdown();
<?php
// ❌ 注意:session_write_close() 後に再び session_start() した場合、
//          新しいシャットダウン登録を忘れがち
session_start();
session_register_shutdown();
session_write_close(); // 一時的にロック解放

// ... 何らかの処理 ...

session_start(); // 再オープン
session_register_shutdown(); // ← 再登録が必要
<?php
// ❌ アンチパターン:手動登録を複数回すると session_write_close() が重複実行される
session_start();
register_shutdown_function('session_write_close');
register_shutdown_function('session_write_close'); // 2回登録 → 2回実行される

// ✅ session_register_shutdown() は重複登録を内部で防ぐ
session_start();
session_register_shutdown();
session_register_shutdown(); // 2回呼んでも安全(内部で1回のみ登録)

まとめ

項目内容
関数名session_register_shutdown(): void
主な用途スクリプト終了時に session_write_close() を自動実行するよう登録
呼び出しタイミングsession_start() の後
戻り値なし(void
特徴重複登録しても安全・PHP組み込みで推奨
よく組み合わせる関数session_set_save_handler() / session_write_close()

session_register_shutdown() は「セッションを確実に閉じる」ことを保証する安全網です。特にカスタムハンドラを使う場合や、session_write_close() で早期解放した後に再オープンする場合は、明示的に登録する習慣をつけることでセッションデータの消失やロック漏れを防げます。


参考リンク

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