[PHP]session_decode関数を完全解説!セッションデータをデコードする方法

PHP

こんにちは!今回は、PHPの標準関数であるsession_decode()について詳しく解説していきます。エンコードされたセッションデータを$_SESSION変数に復元できる、セッション操作の高度な関数です!

session_decode関数とは?

session_decode()関数は、エンコードされた文字列からセッションデータをデコードして$_SESSION変数に格納する関数です。

session_encode()の逆の操作を行い、セッションデータのバックアップ・復元、移行、カスタムストレージの実装などに使用されます!

基本的な構文

session_decode(string $data): bool
  • $data: エンコードされたセッションデータ文字列
  • 戻り値: 成功時はtrue、失敗時はfalse

session_encode()との関係

// エンコード: $_SESSION → 文字列
session_start();
$_SESSION['user'] = 'alice';
$_SESSION['role'] = 'admin';
$encoded = session_encode();
echo $encoded;  // 例: user|s:5:"alice";role|s:5:"admin";

// デコード: 文字列 → $_SESSION
session_start();
$data = 'user|s:5:"alice";role|s:5:"admin";';
session_decode($data);
echo $_SESSION['user'];  // alice
echo $_SESSION['role'];  // admin

セッションハンドラーの種類

// PHPのセッションシリアライザー

// 1. php (デフォルト)
// 形式: key|serialized_value
$encoded = 'name|s:4:"John";age|i:30;';

// 2. php_binary
// 形式: バイナリ形式(キーの長さをバイナリで表現)
// 読みにくいが高速

// 3. php_serialize
// 形式: 配列全体をserialize()
$encoded = 'a:2:{s:4:"name";s:4:"John";s:3:"age";i:30;}';

// ハンドラーを確認
echo ini_get('session.serialize_handler');  // 通常は 'php'

// ハンドラーを変更
ini_set('session.serialize_handler', 'php_serialize');

重要な注意点

// セキュリティ警告
// session_decode()は信頼できないデータに使用しない!
// 悪意のあるデータでオブジェクトインジェクション攻撃が可能

// 危険な例
$untrustedData = $_POST['session_data'];  // ユーザー入力
session_decode($untrustedData);  // 危険!

// session_start()後に使用
session_start();
session_decode($data);  // 正しい

// デコード前にsession_start()がないとエラー
// session_decode($data);  // エラー

// $_SESSION配列を上書き
session_start();
$_SESSION['old'] = 'value';
session_decode('new|s:5:"value";');
// $_SESSION['old']は消える(完全に置き換わる)

基本的な使用例

シンプルなエンコード・デコード

// セッション開始
session_start();

// データを設定
$_SESSION['username'] = 'alice';
$_SESSION['email'] = 'alice@example.com';
$_SESSION['login_time'] = time();

// エンコード
$encoded = session_encode();
echo "エンコード: {$encoded}\n";

// セッションをクリア
$_SESSION = [];

// デコード
$result = session_decode($encoded);
echo "デコード結果: " . ($result ? '成功' : '失敗') . "\n";

// データを確認
echo "ユーザー名: {$_SESSION['username']}\n";
echo "メール: {$_SESSION['email']}\n";

異なるハンドラー間の変換

session_start();
$_SESSION['data'] = 'test';

// phpハンドラーでエンコード
ini_set('session.serialize_handler', 'php');
$encodedPhp = session_encode();
echo "PHP形式: {$encodedPhp}\n";

// php_serializeハンドラーでエンコード
ini_set('session.serialize_handler', 'php_serialize');
$encodedPhpSerialize = session_encode();
echo "PHP Serialize形式: {$encodedPhpSerialize}\n";

// デコード(正しいハンドラーを使用)
$_SESSION = [];
ini_set('session.serialize_handler', 'php');
session_decode($encodedPhp);
echo "復元されたデータ: {$_SESSION['data']}\n";

セッションデータの上書き

session_start();

// 初期データ
$_SESSION['user_id'] = 123;
$_SESSION['username'] = 'alice';
echo "初期データ: " . count($_SESSION) . "件\n";

// 新しいデータでデコード(完全に置き換わる)
$newData = 'role|s:5:"admin";status|s:6:"active";';
session_decode($newData);

echo "デコード後: " . count($_SESSION) . "件\n";
echo "role: {$_SESSION['role']}\n";
echo "username存在: " . (isset($_SESSION['username']) ? 'Yes' : 'No') . "\n";
// usernameは消える

部分的なマージ(カスタム実装)

session_start();

// 現在のセッション
$_SESSION['existing'] = 'value1';
$_SESSION['keep'] = 'value2';

// 現在のデータを保存
$currentSession = $_SESSION;

// 新しいデータをデコード
$newData = 'new|s:6:"value3";update|s:6:"value4";';
session_decode($newData);

// マージ
$_SESSION = array_merge($currentSession, $_SESSION);

echo "マージ後:\n";
foreach ($_SESSION as $key => $value) {
    echo "  {$key}: {$value}\n";
}

実践的な使用例

例1: セッションバックアップ・復元システム

class SessionBackupManager {
    private $backupDir;
    
    /**
     * バックアップマネージャーを初期化
     */
    public function __construct($backupDir = '/tmp/session_backups') {
        $this->backupDir = $backupDir;
        
        if (!is_dir($backupDir)) {
            mkdir($backupDir, 0755, true);
        }
    }
    
    /**
     * 現在のセッションをバックアップ
     */
    public function backup($backupName = null) {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        // セッションデータをエンコード
        $encoded = session_encode();
        
        if ($encoded === false) {
            throw new Exception("Failed to encode session");
        }
        
        // バックアップ名を生成
        if ($backupName === null) {
            $backupName = 'backup_' . date('YmdHis') . '_' . uniqid();
        }
        
        // バックアップファイルに保存
        $backupFile = $this->backupDir . '/' . $backupName . '.dat';
        
        $backupData = [
            'timestamp' => time(),
            'session_id' => session_id(),
            'encoded_data' => $encoded,
            'handler' => ini_get('session.serialize_handler'),
            'data_count' => count($_SESSION)
        ];
        
        $result = file_put_contents($backupFile, serialize($backupData));
        
        if ($result === false) {
            throw new Exception("Failed to write backup file");
        }
        
        return [
            'backup_name' => $backupName,
            'file' => $backupFile,
            'size' => $result,
            'timestamp' => $backupData['timestamp'],
            'data_count' => $backupData['data_count']
        ];
    }
    
    /**
     * バックアップからセッションを復元
     */
    public function restore($backupName, $merge = false) {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $backupFile = $this->backupDir . '/' . $backupName . '.dat';
        
        if (!file_exists($backupFile)) {
            throw new Exception("Backup file not found: {$backupName}");
        }
        
        // バックアップデータを読み込み
        $backupData = unserialize(file_get_contents($backupFile));
        
        if ($backupData === false) {
            throw new Exception("Failed to unserialize backup data");
        }
        
        // 現在のセッションを保存(マージ用)
        $currentSession = $merge ? $_SESSION : [];
        
        // ハンドラーを一時的に変更
        $originalHandler = ini_get('session.serialize_handler');
        ini_set('session.serialize_handler', $backupData['handler']);
        
        // セッションデータをデコード
        $result = session_decode($backupData['encoded_data']);
        
        // ハンドラーを元に戻す
        ini_set('session.serialize_handler', $originalHandler);
        
        if (!$result) {
            throw new Exception("Failed to decode session data");
        }
        
        // マージが必要な場合
        if ($merge) {
            $_SESSION = array_merge($currentSession, $_SESSION);
        }
        
        return [
            'success' => true,
            'backup_name' => $backupName,
            'timestamp' => $backupData['timestamp'],
            'restored_count' => count($_SESSION),
            'merged' => $merge
        ];
    }
    
    /**
     * バックアップ一覧を取得
     */
    public function listBackups() {
        $backups = [];
        $files = glob($this->backupDir . '/*.dat');
        
        foreach ($files as $file) {
            $data = unserialize(file_get_contents($file));
            
            $backups[] = [
                'name' => basename($file, '.dat'),
                'file' => $file,
                'timestamp' => $data['timestamp'],
                'date' => date('Y-m-d H:i:s', $data['timestamp']),
                'size' => filesize($file),
                'data_count' => $data['data_count']
            ];
        }
        
        // タイムスタンプでソート(新しい順)
        usort($backups, function($a, $b) {
            return $b['timestamp'] - $a['timestamp'];
        });
        
        return $backups;
    }
    
    /**
     * バックアップを削除
     */
    public function deleteBackup($backupName) {
        $backupFile = $this->backupDir . '/' . $backupName . '.dat';
        
        if (!file_exists($backupFile)) {
            return false;
        }
        
        return unlink($backupFile);
    }
    
    /**
     * 古いバックアップをクリーンアップ
     */
    public function cleanupOldBackups($maxAge = 86400) {
        $deleted = 0;
        $backups = $this->listBackups();
        $cutoff = time() - $maxAge;
        
        foreach ($backups as $backup) {
            if ($backup['timestamp'] < $cutoff) {
                if ($this->deleteBackup($backup['name'])) {
                    $deleted++;
                }
            }
        }
        
        return [
            'deleted' => $deleted,
            'cutoff_date' => date('Y-m-d H:i:s', $cutoff)
        ];
    }
    
    /**
     * バックアップを比較
     */
    public function compareBackups($backupName1, $backupName2) {
        $file1 = $this->backupDir . '/' . $backupName1 . '.dat';
        $file2 = $this->backupDir . '/' . $backupName2 . '.dat';
        
        if (!file_exists($file1) || !file_exists($file2)) {
            throw new Exception("One or both backup files not found");
        }
        
        $data1 = unserialize(file_get_contents($file1));
        $data2 = unserialize(file_get_contents($file2));
        
        // 一時的にデコード
        $originalHandler = ini_get('session.serialize_handler');
        
        ini_set('session.serialize_handler', $data1['handler']);
        session_decode($data1['encoded_data']);
        $session1 = $_SESSION;
        
        ini_set('session.serialize_handler', $data2['handler']);
        session_decode($data2['encoded_data']);
        $session2 = $_SESSION;
        
        ini_set('session.serialize_handler', $originalHandler);
        
        // 差分を計算
        $diff = [
            'only_in_backup1' => array_diff_key($session1, $session2),
            'only_in_backup2' => array_diff_key($session2, $session1),
            'common' => array_intersect_key($session1, $session2),
            'changed' => []
        ];
        
        foreach ($diff['common'] as $key => $value) {
            if ($session1[$key] !== $session2[$key]) {
                $diff['changed'][$key] = [
                    'backup1' => $session1[$key],
                    'backup2' => $session2[$key]
                ];
            }
        }
        
        return $diff;
    }
}

// 使用例
echo "=== セッションバックアップ・復元 ===\n";

// セッション初期化
session_start();
$_SESSION['user_id'] = 123;
$_SESSION['username'] = 'alice';
$_SESSION['role'] = 'admin';
$_SESSION['login_time'] = time();
echo "初期セッション: " . count($_SESSION) . "件のデータ\n";

$manager = new SessionBackupManager('/tmp/session_backups_test');

// バックアップ作成
echo "\nバックアップ作成:\n";
$backup1 = $manager->backup('backup1');
echo "  バックアップ名: {$backup1['backup_name']}\n";
echo "  ファイルサイズ: {$backup1['size']} bytes\n";
echo "  データ数: {$backup1['data_count']}\n";

// セッションを変更
$_SESSION['status'] = 'active';
$_SESSION['last_activity'] = time();
unset($_SESSION['role']);
echo "\nセッション変更後: " . count($_SESSION) . "件のデータ\n";

// 2つ目のバックアップ
$backup2 = $manager->backup('backup2');
echo "2つ目のバックアップ作成: {$backup2['backup_name']}\n";

// セッションをクリア
$_SESSION = [];
echo "\nセッションクリア後: " . count($_SESSION) . "件\n";

// 復元
echo "\nバックアップから復元:\n";
$restored = $manager->restore('backup1');
echo "  復元成功: " . ($restored['success'] ? 'Yes' : 'No') . "\n";
echo "  復元データ数: {$restored['restored_count']}\n";
echo "  復元後のユーザー名: {$_SESSION['username']}\n";

// バックアップ一覧
echo "\n=== バックアップ一覧 ===\n";
$backups = $manager->listBackups();
foreach ($backups as $backup) {
    echo "{$backup['name']}:\n";
    echo "  日時: {$backup['date']}\n";
    echo "  データ数: {$backup['data_count']}\n";
}

// バックアップ比較
echo "\n=== バックアップ比較 ===\n";
$diff = $manager->compareBackups('backup1', 'backup2');
echo "backup1のみ: " . count($diff['only_in_backup1']) . "件\n";
echo "backup2のみ: " . count($diff['only_in_backup2']) . "件\n";
echo "変更: " . count($diff['changed']) . "件\n";

例2: セッションデータ移行ツール

class SessionMigrationTool {
    /**
     * セッションデータを別のハンドラー形式に変換
     */
    public function convertHandler($sessionData, $fromHandler, $toHandler) {
        // 元のハンドラーで設定
        $originalHandler = ini_get('session.serialize_handler');
        
        session_start();
        
        // 元の形式でデコード
        ini_set('session.serialize_handler', $fromHandler);
        $result = session_decode($sessionData);
        
        if (!$result) {
            throw new Exception("Failed to decode with handler: {$fromHandler}");
        }
        
        // 新しい形式でエンコード
        ini_set('session.serialize_handler', $toHandler);
        $converted = session_encode();
        
        // 元に戻す
        ini_set('session.serialize_handler', $originalHandler);
        
        return [
            'from_handler' => $fromHandler,
            'to_handler' => $toHandler,
            'original_data' => $sessionData,
            'converted_data' => $converted,
            'original_length' => strlen($sessionData),
            'converted_length' => strlen($converted)
        ];
    }
    
    /**
     * 旧バージョンのセッション形式を新バージョンに移行
     */
    public function migrateFromLegacy($legacyData) {
        session_start();
        
        // 旧形式をパース(カスタムパーサー)
        $parsed = $this->parseLegacyFormat($legacyData);
        
        // 現在の形式で再エンコード
        $_SESSION = $parsed;
        $newData = session_encode();
        
        return [
            'legacy_data' => $legacyData,
            'parsed_count' => count($parsed),
            'new_data' => $newData,
            'migrated_keys' => array_keys($parsed)
        ];
    }
    
    /**
     * 旧形式をパース(例:カスタム区切り文字形式)
     */
    private function parseLegacyFormat($data) {
        $result = [];
        
        // 例: key1:value1;key2:value2; 形式
        $pairs = explode(';', trim($data, ';'));
        
        foreach ($pairs as $pair) {
            if (empty($pair)) continue;
            
            list($key, $value) = explode(':', $pair, 2);
            $result[$key] = $value;
        }
        
        return $result;
    }
    
    /**
     * 複数のセッションファイルを一括移行
     */
    public function batchMigrate($sessionDir, $outputDir, $fromHandler, $toHandler) {
        if (!is_dir($outputDir)) {
            mkdir($outputDir, 0755, true);
        }
        
        $files = glob($sessionDir . '/sess_*');
        $migrated = 0;
        $failed = 0;
        $errors = [];
        
        foreach ($files as $file) {
            try {
                $sessionData = file_get_contents($file);
                $converted = $this->convertHandler($sessionData, $fromHandler, $toHandler);
                
                $outputFile = $outputDir . '/' . basename($file);
                file_put_contents($outputFile, $converted['converted_data']);
                
                $migrated++;
            } catch (Exception $e) {
                $failed++;
                $errors[] = [
                    'file' => basename($file),
                    'error' => $e->getMessage()
                ];
            }
        }
        
        return [
            'total' => count($files),
            'migrated' => $migrated,
            'failed' => $failed,
            'errors' => $errors
        ];
    }
    
    /**
     * セッションデータを検証
     */
    public function validateSessionData($sessionData, $handler = 'php') {
        $originalHandler = ini_get('session.serialize_handler');
        
        session_start();
        $_SESSION = [];
        
        ini_set('session.serialize_handler', $handler);
        $result = session_decode($sessionData);
        
        ini_set('session.serialize_handler', $originalHandler);
        
        return [
            'valid' => $result,
            'handler' => $handler,
            'data_count' => $result ? count($_SESSION) : 0,
            'keys' => $result ? array_keys($_SESSION) : []
        ];
    }
    
    /**
     * セッションデータの構造を分析
     */
    public function analyzeStructure($sessionData, $handler = 'php') {
        $validation = $this->validateSessionData($sessionData, $handler);
        
        if (!$validation['valid']) {
            return ['error' => 'Invalid session data'];
        }
        
        session_start();
        ini_set('session.serialize_handler', $handler);
        session_decode($sessionData);
        
        $analysis = [
            'total_keys' => count($_SESSION),
            'data_types' => [],
            'nested_arrays' => 0,
            'objects' => 0,
            'total_size' => strlen($sessionData)
        ];
        
        foreach ($_SESSION as $key => $value) {
            $type = gettype($value);
            
            if (!isset($analysis['data_types'][$type])) {
                $analysis['data_types'][$type] = 0;
            }
            $analysis['data_types'][$type]++;
            
            if (is_array($value) && count($value) > 0) {
                $analysis['nested_arrays']++;
            }
            
            if (is_object($value)) {
                $analysis['objects']++;
            }
        }
        
        return $analysis;
    }
}

// 使用例
echo "=== セッションデータ移行 ===\n";

$migrator = new SessionMigrationTool();

// セッションデータを作成
session_start();
$_SESSION = [
    'user_id' => 456,
    'username' => 'bob',
    'preferences' => ['theme' => 'dark', 'lang' => 'ja'],
    'login_count' => 5
];

// php形式でエンコード
ini_set('session.serialize_handler', 'php');
$phpData = session_encode();
echo "PHP形式: {$phpData}\n";

// php_serialize形式に変換
echo "\n形式変換:\n";
$converted = $migrator->convertHandler($phpData, 'php', 'php_serialize');
echo "  元の形式: {$converted['from_handler']}\n";
echo "  新しい形式: {$converted['to_handler']}\n";
echo "  元のサイズ: {$converted['original_length']} bytes\n";
echo "  新しいサイズ: {$converted['converted_length']} bytes\n";
echo "  変換後のデータ: {$converted['converted_data']}\n";

// データ検証
echo "\nデータ検証:\n";
$validation = $migrator->validateSessionData($phpData, 'php');
echo "  有効: " . ($validation['valid'] ? 'Yes' : 'No') . "\n";
echo "  キー数: {$validation['data_count']}\n";
echo "  キー: " . implode(', ', $validation['keys']) . "\n";

// 構造分析
echo "\n構造分析:\n";
$analysis = $migrator->analyzeStructure($phpData, 'php');
echo "  総キー数: {$analysis['total_keys']}\n";
echo "  総サイズ: {$analysis['total_size']} bytes\n";
echo "  データ型:\n";
foreach ($analysis['data_types'] as $type => $count) {
    echo "    {$type}: {$count}\n";
}

// 旧形式からの移行
echo "\n旧形式からの移行:\n";
$legacyData = 'user_id:789;username:charlie;status:active;';
$migrated = $migrator->migrateFromLegacy($legacyData);
echo "  旧データ: {$migrated['legacy_data']}\n";
echo "  パース数: {$migrated['parsed_count']}\n";
echo "  移行キー: " . implode(', ', $migrated['migrated_keys']) . "\n";
echo "  新データ: {$migrated['new_data']}\n";

例3: カスタムセッションストレージ

class CustomSessionStorage {
    private $storage = [];
    
    /**
     * セッションをカスタムストレージに保存
     */
    public function save($key) {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $encoded = session_encode();
        
        if ($encoded === false) {
            throw new Exception("Failed to encode session");
        }
        
        $this->storage[$key] = [
            'data' => $encoded,
            'timestamp' => time(),
            'session_id' => session_id(),
            'handler' => ini_get('session.serialize_handler')
        ];
        
        return true;
    }
    
    /**
     * カスタムストレージからセッションを読み込み
     */
    public function load($key, $merge = false) {
        if (!isset($this->storage[$key])) {
            throw new Exception("Storage key not found: {$key}");
        }
        
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $stored = $this->storage[$key];
        
        // 現在のセッションを保存(マージ用)
        $currentSession = $merge ? $_SESSION : [];
        
        // ハンドラーを設定
        $originalHandler = ini_get('session.serialize_handler');
        ini_set('session.serialize_handler', $stored['handler']);
        
        // デコード
        $result = session_decode($stored['data']);
        
        // ハンドラーを戻す
        ini_set('session.serialize_handler', $originalHandler);
        
        if (!$result) {
            throw new Exception("Failed to decode session data");
        }
        
        // マージ
        if ($merge) {
            $_SESSION = array_merge($currentSession, $_SESSION);
        }
        
        return [
            'key' => $key,
            'timestamp' => $stored['timestamp'],
            'age' => time() - $stored['timestamp'],
            'merged' => $merge
        ];
    }
    
    /**
     * ストレージ内のすべてのキーを取得
     */
    public function listKeys() {
        $keys = [];
        
        foreach ($this->storage as $key => $data) {
            $keys[] = [
                'key' => $key,
                'timestamp' => $data['timestamp'],
                'age' => time() - $data['timestamp'],
                'size' => strlen($data['data'])
            ];
        }
        
        return $keys;
    }
    
    /**
     * ストレージをクリア
     */
    public function clear($key = null) {
        if ($key === null) {
            $this->storage = [];
            return true;
        }
        
        if (isset($this->storage[$key])) {
            unset($this->storage[$key]);
            return true;
        }
        
        return false;
    }
    
    /**
     * セッションの差分を保存
     */
    public function saveDiff($key, $baseKey) {
        if (!isset($this->storage[$baseKey])) {
            throw new Exception("Base key not found: {$baseKey}");
        }
        
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        // ベースセッションをデコード
        $originalHandler = ini_get('session.serialize_handler');
        $baseData = $this->storage[$baseKey];
        
        ini_set('session.serialize_handler', $baseData['handler']);
        session_decode($baseData['data']);
        $baseSession = $_SESSION;
        
        // 現在のセッション
        session_start();
        $currentSession = $_SESSION;
        
        // 差分を計算
        $diff = [
            'added' => array_diff_key($currentSession, $baseSession),
            'removed' => array_diff_key($baseSession, $currentSession),
            'modified' => []
        ];
        
        foreach (array_intersect_key($currentSession, $baseSession) as $key => $value) {
            if ($currentSession[$key] !== $baseSession[$key]) {
                $diff['modified'][$key] = $currentSession[$key];
            }
        }
        
        ini_set('session.serialize_handler', $originalHandler);
        
        $this->storage[$key] = [
            'type' => 'diff',
            'base_key' => $baseKey,
            'diff' => $diff,
            'timestamp' => time()
        ];
        
        return $diff;
    }
    
    /**
     * 差分を適用
     */
    public function applyDiff($diffKey) {
        if (!isset($this->storage[$diffKey])) {
            throw new Exception("Diff key not found: {$diffKey}");
        }
        
        $diffData = $this->storage[$diffKey];
        
        if ($diffData['type'] !== 'diff') {
            throw new Exception("Not a diff storage: {$diffKey}");
        }
        
        // ベースをロード
        $this->load($diffData['base_key']);
        
        // 差分を適用
        $diff = $diffData['diff'];
        
        // 追加
        foreach ($diff['added'] as $key => $value) {
            $_SESSION[$key] = $value;
        }
        
        // 削除
        foreach ($diff['removed'] as $key => $value) {
            unset($_SESSION[$key]);
        }
        
        // 変更
        foreach ($diff['modified'] as $key => $value) {
            $_SESSION[$key] = $value;
        }
        
        return true;
    }
}

// 使用例
echo "=== カスタムセッションストレージ ===\n";

$storage = new CustomSessionStorage();

// セッションを保存
session_start();
$_SESSION = [
    'user_id' => 111,
    'username' => 'dave',
    'role' => 'user'
];

$storage->save('state1');
echo "状態1を保存\n";

// セッションを変更
$_SESSION['status'] = 'active';
$_SESSION['last_login'] = time();
unset($_SESSION['role']);

$storage->save('state2');
echo "状態2を保存\n";

// セッションをクリア
$_SESSION = [];
echo "セッションクリア\n";

// 状態1を復元
echo "\n状態1を復元:\n";
$loaded = $storage->load('state1');
echo "  キー: {$loaded['key']}\n";
echo "  経過時間: {$loaded['age']}秒\n";
echo "  復元データ: " . count($_SESSION) . "件\n";
echo "  ユーザー名: {$_SESSION['username']}\n";

// ストレージ一覧
echo "\nストレージキー一覧:\n";
foreach ($storage->listKeys() as $info) {
    echo "  {$info['key']}: {$info['size']} bytes, {$info['age']}秒前\n";
}

// 差分保存
session_start();
$_SESSION = ['base' => 'value', 'keep' => 'data'];
$storage->save('base');

$_SESSION['new'] = 'added';
$_SESSION['base'] = 'modified';
unset($_SESSION['keep']);

$diff = $storage->saveDiff('diff1', 'base');
echo "\n差分保存:\n";
echo "  追加: " . count($diff['added']) . "件\n";
echo "  削除: " . count($diff['removed']) . "件\n";
echo "  変更: " . count($diff['modified']) . "件\n";

例4: セッションデバッグツール

class SessionDebugTool {
    /**
     * セッションデータを可視化
     */
    public function visualize($sessionData = null, $handler = 'php') {
        if ($sessionData === null) {
            if (session_status() !== PHP_SESSION_ACTIVE) {
                session_start();
            }
            $sessionData = session_encode();
        }
        
        $originalHandler = ini_get('session.serialize_handler');
        
        session_start();
        $_SESSION = [];
        
        ini_set('session.serialize_handler', $handler);
        $result = session_decode($sessionData);
        
        ini_set('session.serialize_handler', $originalHandler);
        
        if (!$result) {
            return ['error' => 'Failed to decode session data'];
        }
        
        $visualization = [
            'raw_data' => $sessionData,
            'handler' => $handler,
            'total_keys' => count($_SESSION),
            'keys' => []
        ];
        
        foreach ($_SESSION as $key => $value) {
            $visualization['keys'][$key] = [
                'type' => gettype($value),
                'value' => $this->formatValue($value),
                'size' => strlen(serialize($value))
            ];
        }
        
        return $visualization;
    }
    
    /**
     * 値をフォーマット
     */
    private function formatValue($value, $maxLength = 50) {
        if (is_array($value)) {
            return '[' . count($value) . ' items]';
        } elseif (is_object($value)) {
            return get_class($value) . ' object';
        } elseif (is_string($value)) {
            if (strlen($value) > $maxLength) {
                return substr($value, 0, $maxLength) . '...';
            }
            return $value;
        } else {
            return var_export($value, true);
        }
    }
    
    /**
     * セッションデータを比較
     */
    public function compare($data1, $data2, $handler = 'php') {
        $originalHandler = ini_get('session.serialize_handler');
        
        // データ1をデコード
        session_start();
        $_SESSION = [];
        ini_set('session.serialize_handler', $handler);
        session_decode($data1);
        $session1 = $_SESSION;
        
        // データ2をデコード
        $_SESSION = [];
        session_decode($data2);
        $session2 = $_SESSION;
        
        ini_set('session.serialize_handler', $originalHandler);
        
        $comparison = [
            'only_in_data1' => array_diff_key($session1, $session2),
            'only_in_data2' => array_diff_key($session2, $session1),
            'common' => array_intersect_key($session1, $session2),
            'differences' => []
        ];
        
        foreach ($comparison['common'] as $key => $value) {
            if ($session1[$key] !== $session2[$key]) {
                $comparison['differences'][$key] = [
                    'data1' => $session1[$key],
                    'data2' => $session2[$key]
                ];
            }
        }
        
        return $comparison;
    }
    
    /**
     * セッションデータのヒストリーを追跡
     */
    public function trackHistory() {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        if (!isset($_SESSION['_debug_history'])) {
            $_SESSION['_debug_history'] = [];
        }
        
        $snapshot = [
            'timestamp' => microtime(true),
            'data' => session_encode(),
            'keys' => array_keys($_SESSION),
            'key_count' => count($_SESSION)
        ];
        
        $_SESSION['_debug_history'][] = $snapshot;
        
        // 最新10件のみ保持
        if (count($_SESSION['_debug_history']) > 10) {
            $_SESSION['_debug_history'] = array_slice($_SESSION['_debug_history'], -10);
        }
        
        return $snapshot;
    }
    
    /**
     * ヒストリーを取得
     */
    public function getHistory() {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        return $_SESSION['_debug_history'] ?? [];
    }
    
    /**
     * セッションデータの整合性をチェック
     */
    public function checkIntegrity($sessionData, $handler = 'php') {
        $issues = [];
        
        // デコードテスト
        $originalHandler = ini_get('session.serialize_handler');
        
        session_start();
        $_SESSION = [];
        
        ini_set('session.serialize_handler', $handler);
        $result = session_decode($sessionData);
        
        if (!$result) {
            $issues[] = [
                'type' => 'decode_failure',
                'severity' => 'critical',
                'message' => 'Failed to decode session data'
            ];
        }
        
        // サイズチェック
        $size = strlen($sessionData);
        if ($size > 10240) {  // 10KB
            $issues[] = [
                'type' => 'large_size',
                'severity' => 'warning',
                'message' => "Session data is large: {$size} bytes"
            ];
        }
        
        // キー数チェック
        $keyCount = count($_SESSION);
        if ($keyCount > 100) {
            $issues[] = [
                'type' => 'many_keys',
                'severity' => 'warning',
                'message' => "Many session keys: {$keyCount}"
            ];
        }
        
        // オブジェクトチェック
        foreach ($_SESSION as $key => $value) {
            if (is_object($value)) {
                $issues[] = [
                    'type' => 'contains_object',
                    'severity' => 'info',
                    'message' => "Key '{$key}' contains object: " . get_class($value)
                ];
            }
        }
        
        ini_set('session.serialize_handler', $originalHandler);
        
        return [
            'valid' => empty(array_filter($issues, function($i) {
                return $i['severity'] === 'critical';
            })),
            'issues' => $issues,
            'summary' => [
                'size' => $size,
                'key_count' => $keyCount,
                'total_issues' => count($issues)
            ]
        ];
    }
    
    /**
     * デバッグレポートを生成
     */
    public function generateReport() {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $sessionData = session_encode();
        
        $report = [
            'session_id' => session_id(),
            'session_name' => session_name(),
            'handler' => ini_get('session.serialize_handler'),
            'save_path' => session_save_path(),
            'visualization' => $this->visualize($sessionData),
            'integrity' => $this->checkIntegrity($sessionData),
            'history_count' => count($this->getHistory()),
            'generated_at' => date('Y-m-d H:i:s')
        ];
        
        return $report;
    }
}

// 使用例
echo "=== セッションデバッグツール ===\n";

$debugger = new SessionDebugTool();

// セッションデータを作成
session_start();
$_SESSION = [
    'user_id' => 999,
    'username' => 'debuguser',
    'data' => ['key1' => 'value1', 'key2' => 'value2'],
    'timestamp' => time()
];

// 可視化
echo "セッションデータ可視化:\n";
$viz = $debugger->visualize();
echo "  ハンドラー: {$viz['handler']}\n";
echo "  総キー数: {$viz['total_keys']}\n";
echo "  生データ: {$viz['raw_data']}\n";
echo "\nキー詳細:\n";
foreach ($viz['keys'] as $key => $info) {
    echo "  {$key}: {$info['type']} - {$info['value']} ({$info['size']} bytes)\n";
}

// ヒストリー追跡
echo "\nヒストリー追跡:\n";
$debugger->trackHistory();
$_SESSION['change1'] = 'test';
$debugger->trackHistory();
$_SESSION['change2'] = 'test2';
$debugger->trackHistory();

$history = $debugger->getHistory();
echo "  ヒストリー数: " . count($history) . "\n";

// 整合性チェック
echo "\n整合性チェック:\n";
$integrity = $debugger->checkIntegrity(session_encode());
echo "  有効: " . ($integrity['valid'] ? 'Yes' : 'No') . "\n";
echo "  問題数: {$integrity['summary']['total_issues']}\n";
if (!empty($integrity['issues'])) {
    foreach ($integrity['issues'] as $issue) {
        echo "  [{$issue['severity']}] {$issue['message']}\n";
    }
}

// デバッグレポート
echo "\n=== デバッグレポート ===\n";
$report = $debugger->generateReport();
echo "生成日時: {$report['generated_at']}\n";
echo "セッションID: {$report['session_id']}\n";
echo "ハンドラー: {$report['handler']}\n";
echo "総キー数: {$report['visualization']['total_keys']}\n";

例5: セッションマージシステム

class SessionMergeSystem {
    /**
     * 複数のセッションデータをマージ
     */
    public function merge($sessionDataArray, $handler = 'php', $strategy = 'last_wins') {
        $originalHandler = ini_get('session.serialize_handler');
        
        session_start();
        $_SESSION = [];
        
        ini_set('session.serialize_handler', $handler);
        
        $allSessions = [];
        
        // すべてのセッションをデコード
        foreach ($sessionDataArray as $index => $data) {
            session_decode($data);
            $allSessions[$index] = $_SESSION;
            $_SESSION = [];
        }
        
        // マージ戦略に応じて処理
        $merged = $this->applyMergeStrategy($allSessions, $strategy);
        
        $_SESSION = $merged;
        
        ini_set('session.serialize_handler', $originalHandler);
        
        return [
            'strategy' => $strategy,
            'input_count' => count($sessionDataArray),
            'output_keys' => count($merged),
            'merged_data' => $merged
        ];
    }
    
    /**
     * マージ戦略を適用
     */
    private function applyMergeStrategy($sessions, $strategy) {
        switch ($strategy) {
            case 'first_wins':
                // 最初の値を優先
                $result = [];
                foreach ($sessions as $session) {
                    foreach ($session as $key => $value) {
                        if (!isset($result[$key])) {
                            $result[$key] = $value;
                        }
                    }
                }
                return $result;
                
            case 'last_wins':
                // 最後の値を優先(デフォルト)
                $result = [];
                foreach ($sessions as $session) {
                    $result = array_merge($result, $session);
                }
                return $result;
                
            case 'array_merge':
                // 配列は結合、それ以外は上書き
                $result = [];
                foreach ($sessions as $session) {
                    foreach ($session as $key => $value) {
                        if (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
                            $result[$key] = array_merge($result[$key], $value);
                        } else {
                            $result[$key] = $value;
                        }
                    }
                }
                return $result;
                
            case 'keep_all':
                // 衝突するキーは配列に
                $result = [];
                foreach ($sessions as $session) {
                    foreach ($session as $key => $value) {
                        if (!isset($result[$key])) {
                            $result[$key] = $value;
                        } else {
                            if (!is_array($result[$key]) || !isset($result[$key][0])) {
                                $result[$key] = [$result[$key]];
                            }
                            $result[$key][] = $value;
                        }
                    }
                }
                return $result;
                
            default:
                return [];
        }
    }
    
    /**
     * 優先順位付きマージ
     */
    public function mergeWithPriority($sessionDataArray, $priorities, $handler = 'php') {
        $originalHandler = ini_get('session.serialize_handler');
        
        session_start();
        $_SESSION = [];
        
        ini_set('session.serialize_handler', $handler);
        
        // 優先順位でソート
        $sorted = [];
        foreach ($sessionDataArray as $index => $data) {
            $priority = $priorities[$index] ?? 0;
            $sorted[$priority][] = ['index' => $index, 'data' => $data];
        }
        ksort($sorted);
        
        // 優先順位の低い方から適用
        $result = [];
        foreach ($sorted as $priority => $items) {
            foreach ($items as $item) {
                session_decode($item['data']);
                $result = array_merge($result, $_SESSION);
                $_SESSION = [];
            }
        }
        
        $_SESSION = $result;
        
        ini_set('session.serialize_handler', $originalHandler);
        
        return [
            'merged_count' => count($sessionDataArray),
            'output_keys' => count($result),
            'merged_data' => $result
        ];
    }
    
    /**
     * 条件付きマージ
     */
    public function conditionalMerge($sessionDataArray, $condition, $handler = 'php') {
        $originalHandler = ini_get('session.serialize_handler');
        
        session_start();
        $_SESSION = [];
        
        ini_set('session.serialize_handler', $handler);
        
        $result = [];
        
        foreach ($sessionDataArray as $index => $data) {
            session_decode($data);
            
            foreach ($_SESSION as $key => $value) {
                if (call_user_func($condition, $key, $value, $index)) {
                    $result[$key] = $value;
                }
            }
            
            $_SESSION = [];
        }
        
        $_SESSION = $result;
        
        ini_set('session.serialize_handler', $originalHandler);
        
        return $result;
    }
    
    /**
     * セッション差分をマージ
     */
    public function mergeDiff($baseData, $diffData, $handler = 'php') {
        $originalHandler = ini_get('session.serialize_handler');
        
        session_start();
        $_SESSION = [];
        
        ini_set('session.serialize_handler', $handler);
        
        // ベースをデコード
        session_decode($baseData);
        $base = $_SESSION;
        
        // 差分をデコード
        $_SESSION = [];
        session_decode($diffData);
        $diff = $_SESSION;
        
        // マージ
        $merged = array_merge($base, $diff);
        
        $_SESSION = $merged;
        
        ini_set('session.serialize_handler', $originalHandler);
        
        return [
            'base_keys' => count($base),
            'diff_keys' => count($diff),
            'merged_keys' => count($merged),
            'added_keys' => array_diff_key($merged, $base)
        ];
    }
}

// 使用例
echo "=== セッションマージシステム ===\n";

$merger = new SessionMergeSystem();

// セッションデータを作成
session_start();

// セッション1
$_SESSION = ['user_id' => 1, 'name' => 'Alice', 'role' => 'user'];
$session1 = session_encode();

// セッション2
$_SESSION = ['user_id' => 1, 'email' => 'alice@example.com', 'role' => 'admin'];
$session2 = session_encode();

// セッション3
$_SESSION = ['user_id' => 1, 'status' => 'active', 'last_login' => time()];
$session3 = session_encode();

// マージ(last_wins戦略)
echo "last_wins戦略でマージ:\n";
$result1 = $merger->merge([$session1, $session2, $session3], 'php', 'last_wins');
echo "  入力数: {$result1['input_count']}\n";
echo "  出力キー数: {$result1['output_keys']}\n";
echo "  role: {$result1['merged_data']['role']}\n";  // 'admin'

// マージ(first_wins戦略)
echo "\nfirst_wins戦略でマージ:\n";
$result2 = $merger->merge([$session1, $session2, $session3], 'php', 'first_wins');
echo "  role: {$result2['merged_data']['role']}\n";  // 'user'

// マージ(keep_all戦略)
echo "\nkeep_all戦略でマージ:\n";
$result3 = $merger->merge([$session1, $session2, $session3], 'php', 'keep_all');
echo "  role: " . (is_array($result3['merged_data']['role']) ? 
    implode(', ', $result3['merged_data']['role']) : $result3['merged_data']['role']) . "\n";

// 優先順位付きマージ
echo "\n優先順位付きマージ:\n";
$priorities = [0 => 1, 1 => 3, 2 => 2];  // session2が最優先
$result4 = $merger->mergeWithPriority([$session1, $session2, $session3], $priorities);
echo "  role: {$result4['merged_data']['role']}\n";  // session2の'admin'

// 条件付きマージ
echo "\n条件付きマージ('user'で始まるキーのみ):\n";
$result5 = $merger->conditionalMerge(
    [$session1, $session2, $session3],
    function($key, $value, $index) {
        return strpos($key, 'user') === 0;
    }
);
echo "  マージされたキー: " . implode(', ', array_keys($result5)) . "\n";

例6: セキュアセッションデコーダー

class SecureSessionDecoder {
    private $trustedSources = [];
    private $maxSize = 10240;  // 10KB
    
    /**
     * 信頼できるソースを登録
     */
    public function addTrustedSource($identifier) {
        $this->trustedSources[] = $identifier;
    }
    
    /**
     * セキュアにデコード
     */
    public function secureDecode($sessionData, $sourceIdentifier = null, $handler = 'php') {
        // ソースの検証
        if ($sourceIdentifier !== null && !in_array($sourceIdentifier, $this->trustedSources)) {
            throw new Exception("Untrusted source: {$sourceIdentifier}");
        }
        
        // サイズチェック
        if (strlen($sessionData) > $this->maxSize) {
            throw new Exception("Session data too large: " . strlen($sessionData) . " bytes");
        }
        
        // フォーマット検証
        if (!$this->validateFormat($sessionData, $handler)) {
            throw new Exception("Invalid session data format");
        }
        
        // サンドボックス内でデコード
        $decoded = $this->decodeInSandbox($sessionData, $handler);
        
        // 内容検証
        $this->validateContent($decoded);
        
        // セッションに適用
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $_SESSION = $decoded;
        
        return [
            'success' => true,
            'source' => $sourceIdentifier,
            'keys' => array_keys($decoded),
            'size' => strlen($sessionData)
        ];
    }
    
    /**
     * フォーマットを検証
     */
    private function validateFormat($sessionData, $handler) {
        switch ($handler) {
            case 'php':
                // key|serialized_value の形式をチェック
                return preg_match('/^[a-zA-Z0-9_]+\|/', $sessionData) === 1;
                
            case 'php_serialize':
                // シリアライズ形式をチェック
                return strpos($sessionData, 'a:') === 0;
                
            default:
                return false;
        }
    }
    
    /**
     * サンドボックス内でデコード
     */
    private function decodeInSandbox($sessionData, $handler) {
        $originalHandler = ini_get('session.serialize_handler');
        
        // 一時セッションでデコード
        $tempSession = [];
        
        if (session_status() === PHP_SESSION_ACTIVE) {
            $tempSession = $_SESSION;
            $_SESSION = [];
        } else {
            session_start();
        }
        
        ini_set('session.serialize_handler', $handler);
        
        try {
            $result = session_decode($sessionData);
            
            if (!$result) {
                throw new Exception("Failed to decode session data");
            }
            
            $decoded = $_SESSION;
            $_SESSION = $tempSession;
            
            ini_set('session.serialize_handler', $originalHandler);
            
            return $decoded;
            
        } catch (Exception $e) {
            $_SESSION = $tempSession;
            ini_set('session.serialize_handler', $originalHandler);
            throw $e;
        }
    }
    
    /**
     * 内容を検証
     */
    private function validateContent($decoded) {
        // オブジェクトのチェック
        foreach ($decoded as $key => $value) {
            if (is_object($value)) {
                throw new Exception("Session contains object: " . get_class($value));
            }
            
            if (is_array($value)) {
                $this->validateContent($value);
            }
        }
        
        // キー数の制限
        if (count($decoded) > 100) {
            throw new Exception("Too many session keys: " . count($decoded));
        }
        
        return true;
    }
    
    /**
     * ホワイトリストベースのデコード
     */
    public function decodeWithWhitelist($sessionData, $allowedKeys, $handler = 'php') {
        $decoded = $this->decodeInSandbox($sessionData, $handler);
        
        // ホワイトリストにないキーを削除
        $filtered = [];
        foreach ($decoded as $key => $value) {
            if (in_array($key, $allowedKeys)) {
                $filtered[$key] = $value;
            }
        }
        
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $_SESSION = $filtered;
        
        return [
            'total_keys' => count($decoded),
            'filtered_keys' => count($filtered),
            'removed_keys' => count($decoded) - count($filtered)
        ];
    }
    
    /**
     * サニタイズしてデコード
     */
    public function sanitizedDecode($sessionData, $handler = 'php') {
        $decoded = $this->decodeInSandbox($sessionData, $handler);
        
        // 危険な値をサニタイズ
        $sanitized = $this->sanitizeValues($decoded);
        
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $_SESSION = $sanitized;
        
        return [
            'sanitized' => true,
            'keys' => count($sanitized)
        ];
    }
    
    /**
     * 値をサニタイズ
     */
    private function sanitizeValues($data) {
        $sanitized = [];
        
        foreach ($data as $key => $value) {
            if (is_string($value)) {
                // HTMLエスケープ
                $sanitized[$key] = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
            } elseif (is_array($value)) {
                $sanitized[$key] = $this->sanitizeValues($value);
            } elseif (is_int($value) || is_float($value) || is_bool($value)) {
                $sanitized[$key] = $value;
            } else {
                // その他の型は除外
                continue;
            }
        }
        
        return $sanitized;
    }
}

// 使用例
echo "=== セキュアセッションデコーダー ===\n";

$decoder = new SecureSessionDecoder();

// 信頼できるソースを登録
$decoder->addTrustedSource('internal_backup');
$decoder->addTrustedSource('migration_tool');

// セッションデータを作成
session_start();
$_SESSION = [
    'user_id' => 123,
    'username' => 'secureuser',
    'email' => 'user@example.com',
    'role' => 'admin',
    'untrusted' => '<script>alert("xss")</script>'
];
$sessionData = session_encode();

// セキュアデコード
echo "セキュアデコード:\n";
try {
    $result = $decoder->secureDecode($sessionData, 'internal_backup');
    echo "  成功\n";
    echo "  ソース: {$result['source']}\n";
    echo "  キー数: " . count($result['keys']) . "\n";
} catch (Exception $e) {
    echo "  エラー: " . $e->getMessage() . "\n";
}

// ホワイトリストベースデコード
echo "\nホワイトリストベースデコード:\n";
$allowedKeys = ['user_id', 'username', 'email'];
$result2 = $decoder->decodeWithWhitelist($sessionData, $allowedKeys);
echo "  総キー数: {$result2['total_keys']}\n";
echo "  フィルター後: {$result2['filtered_keys']}\n";
echo "  削除されたキー: {$result2['removed_keys']}\n";

// サニタイズデコード
echo "\nサニタイズデコード:\n";
$result3 = $decoder->sanitizedDecode($sessionData);
echo "  サニタイズ完了\n";
echo "  キー数: {$result3['keys']}\n";

session_start();
echo "  untrustedフィールド: {$_SESSION['untrusted']}\n";
// HTMLエスケープされている

セキュリティ上の注意

// 危険: 信頼できないデータのデコード
// オブジェクトインジェクション攻撃のリスク

// NG: ユーザー入力を直接デコード
$userInput = $_POST['session'];
session_decode($userInput);  // 危険!

// OK: 信頼できるソースからのみデコード
// - 自分でエンコードしたデータ
// - 署名付きデータ
// - 暗号化されたデータ

// ベストプラクティス
// 1. 内部バックアップのみに使用
// 2. ホワイトリストで検証
// 3. サイズ制限を設ける
// 4. オブジェクトを含まないことを確認

まとめ

session_decode()関数の特徴をまとめると:

できること:

  • エンコードされたセッションデータを$_SESSIONに復元
  • セッションデータのバックアップ・復元
  • カスタムセッションストレージの実装
  • セッションデータの移行

session_encode()との関係:

  • session_encode(): $_SESSION → 文字列
  • session_decode(): 文字列 → $_SESSION
  • 対になる関数

重要な注意点:

  • セキュリティリスク: 信頼できないデータには使用しない
  • session_start()後に使用
  • $_SESSION配列を完全に上書き
  • シリアライズハンドラーの指定が重要

推奨される使用場面:

  • セッションバックアップシステム
  • セッションデータの移行
  • カスタムセッションストレージ
  • デバッグツール
  • テスト環境でのセッション再現

ベストプラクティス:

// 1. 信頼できるソースのみ
$myBackup = session_encode();
session_decode($myBackup);  // OK

// 2. サイズチェック
if (strlen($data) > 10240) {
    throw new Exception("Too large");
}

// 3. フォーマット検証
if (!preg_match('/^[a-zA-Z0-9_]+\|/', $data)) {
    throw new Exception("Invalid format");
}

// 4. サンドボックス内で検証
$temp = $_SESSION;
session_decode($data);
// 検証...
$_SESSION = $temp;  // 問題があれば元に戻す

関連関数:

  • session_encode(): セッションをエンコード
  • session_start(): セッション開始
  • serialize(): データをシリアライズ
  • unserialize(): データをアンシリアライズ

session_decode()は、セッションデータを文字列から復元できる強力な関数ですが、セキュリティリスクが高いため、信頼できるデータソースでのみ使用してください!

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