iT邦幫忙

0

升級PHP8.2後,將session儲存到mysql的問題

  • 分享至 

  • xImage

由於CPanel即將全面升級到PHP8以上的版本,並不再支援以下的版本
所以目前再對網站進行升級準備

網站利用 session_set_save_handler 將 session 儲存在 mysql 中
升級後出現以下錯誤訊息
PHP Warning: session_start(): Failed to read session data: user (path: {主機端路徑}) in {引用檔案路徑} on line 4
原始碼如下:

<?php
	require_once dirname(__FILE__)."/define.php";  //將資料庫參數設定為靜態值的檔案

	function getConnection(){
		$conn = new PDO(DB_TYPE.':host='.DB_HOST.';dbname='.DB_NAME, DB_USER, DB_PASSWD, array(PDO::MYSQL_ATTR_INIT_COMMAND=>"SET NAMES'utf8';"));
		$conn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
		$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES,false);
		$conn->query('SET NAMES UTF8');
		return $conn;
	}

	function sess_open($save_path, $session_name){
		$dbh = getConnection();

		try{
			$dbh->query("SELECT 1");
		}catch(PDOException $e){
			die("MySQL Error:".$e->getMessage());
		}
		return true;
	}

	function sess_close(){
		return true;
	}

	function sess_read($key){
		$dbh = getConnection();
		$sql = "SELECT `value` FROM `db_session` WHERE `sesskey`=:sesskey AND `expiry`>UNIX_TIMESTAMP()";
		$sth = $dbh->prepare($sql);
		$sth->bindParam(":sesskey", $key, PDO::PARAM_STR, 32);
		$sth->execute();
		$data = $sth->fetchAll();
		if(count($data) > 0) return $data[0]["value"];
		return false;
	}

	function sess_write($key, $val){
		$dbh = getConnection();
		$expiry = time() + SESS_LIFE;
		$sql = "INSERT INTO `db_session` (`sesskey`, `expiry`, `value`) VALUES (:sesskey, :expiry1, :value1) ON DUPLICATE KEY UPDATE `expiry`=:expiry2,`value`=:value2";
		$sth = $dbh->prepare($sql);
		$sth->bindParam(":sesskey", $key, PDO::PARAM_STR, 32);
		$sth->bindParam(":expiry1", $expiry, PDO::PARAM_INT);
		$sth->bindParam(":value1", $val, PDO::PARAM_STR);
		$sth->bindParam(":expiry2", $expiry, PDO::PARAM_INT);
		$sth->bindParam(":value2", $val, PDO::PARAM_STR);
		$sth->execute();
		return ($sth->errorCode() == "00000")?true:false;
	}

	function sess_destroy($key){
		$dbh = getConnection();
		$sql = "DELETE FROM `db_session` WHERE `sesskey`=:sesskey";
		$sth = $dbh->prepare($sql);
		$sth->bindParam(":sesskey", $key, PDO::PARAM_STR, 32);
		$sth->execute();
		return ($sth->errorCode() == "00000")?true:false;
	}

	function sess_gc($maxlifetime){
		$dbh = getConnection();
		$sql = "DELETE FROM `db_session` WHERE `expiry`<UNIX_TIMESTAMP()";
		$sth = $dbh->prepare($sql);
		$sth->execute();
		return ($sth->errorCode() == "00000")?true:false;
	}

	ini_set('session.gc_maxlifetime', SESS_LIFE);
	ini_set('session.gc_probability', '1');
	ini_set('session.gc_divisor', '1');

	session_set_save_handler("sess_open", "sess_close", "sess_read", "sess_write", "sess_destroy", "sess_gc");
?>

查了一下,PHP目前傾向 session_set_save_handler 不支援兩個以上的參數
有修改成下面的進行測試,依然不行

<?php
	require_once dirname(__FILE__)."/define.php";  //將資料庫參數設定為靜態值的檔案

	class SysSession implements SessionHandlerInterface
	{
		private $dbh;
		function __construct()
		{
			$conn = new PDO(DB_TYPE.':host='.DB_HOST.';dbname='.DB_NAME, DB_USER, DB_PASSWD, array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES'utf8';"));
			$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
			$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
			$conn->query('SET NAMES UTF8');
			$this->dbh = $conn;
		}
		public function open($savePath, $sessionName): bool
		{
			try {
				$this->dbh->query("SELECT 1");
			} catch (PDOException $e) {
				//die("MySQL Error:".$e->getMessage());
				return false;
			}
			return true;
		}
		public function close(): bool
		{
			$this->dbh = null;
			return true;
		}
		public function read($key): string | false
		{
			$sql = "SELECT `value` FROM `db_session` WHERE `sesskey`=:sesskey AND `expiry`>UNIX_TIMESTAMP()";
			$sth = $this->dbh->prepare($sql);
			$sth->bindParam(":sesskey", $key, PDO::PARAM_STR, 32);
			$sth->execute();
			$data = $sth->fetchAll();
			return count($data) ? $data[0]["value"] : false;
		}
		public function write($key, $val): bool
		{
			$expiry = time() + SESS_LIFE;
			$sql = "REPLACE INTO `db_session` (`sesskey`, `expiry`, `value`) VALUES (:sesskey, :expiry, :value)";
			$sth = $this->dbh->prepare($sql);
			$sth->bindParam(":sesskey", $key, PDO::PARAM_STR, 32);
			$sth->bindParam(":expiry", $expiry, PDO::PARAM_INT);
			$sth->bindParam(":value", $val, PDO::PARAM_STR);
			$sth->execute();
			return ($sth->errorCode() == "00000") ? true : false;
		}
		public function destroy($key): bool
		{
			$sql = "DELETE FROM `db_session` WHERE `sesskey`=:sesskey";
			$sth = $this->dbh->prepare($sql);
			$sth->bindParam(":sesskey", $key, PDO::PARAM_STR, 32);
			$sth->execute();
			return ($sth->errorCode() == "00000") ? true : false;
		}
		public function gc($maxlifetime): int | false
		{
			$sql = "DELETE FROM `db_session` WHERE `expiry`<UNIX_TIMESTAMP()";
			$sth = $this->dbh->prepare($sql);
			$sth->execute();
			return ($sth->errorCode() == "00000") ? true : false;
		}
	}

	ini_set('session.gc_maxlifetime', SESS_LIFE);
	ini_set('session.gc_probability', '1');
	ini_set('session.gc_divisor', '1');

	$handler = new SysSession();
	session_set_save_handler($handler, true);
?>

這邊是引用的測試檔

<?php
	require_once dirname(__FILE__)."/include/session_mysql.php";

	session_start();  //報錯的地方
	$a = isset($_SESSION["a"])?(int)$_SESSION["a"]:1;
	$_SESSION["a"] = $a + 1;
	echo $a;
?>

請問我是哪邊寫錯了嗎?

參考這邊的解說,你會知道你少了什麼動作,還有 PHP CONF 跟 MYSQL CONF 有可能也要看一下
https://stackoverflow.com/questions/36753513/how-do-i-save-php-session-data-to-a-database-instead-of-in-the-file-system
參考
https://www.google.com/search?q=php+conf+allow+session+save+to+mysql
https://www.google.com/search?q=php8+session+save+to+mysql
https://www.google.com/search?q=how+do+i+session+save+to+mysql
https://www.google.com/search?q=session+in+php+mysql
https://www.google.com/search?q=session+in+php+mysql+conf
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 個回答

2
zivzhong
iT邦研究生 5 級 ‧ 2025-01-20 17:53:55
最佳解答

參考一下 Chatgpt 說:

根據您提供的情況,問題主要出在自訂的 session handler 無法正確讀取或處理會話數據,導致 session_start() 失敗。以下是詳細的分析與解決方案:

問題分析

  1. PHP 8 中 SessionHandlerInterface 的要求

    • 在 PHP 8 中,SessionHandlerInterface::read 方法必須返回一個字符串。如果沒有讀取到會話數據,應返回空字符串 '',而不是 false。返回 false 會被視為讀取失敗,導致 session_start() 發出警告。
  2. 您的代碼中 read 方法的實現

    • 在類基礎的實現中,read 方法在找不到會話數據時返回 false,這不符合 PHP 8 的要求。
    • 在過程式的實現中,sess_read 方法也是類似地在找不到數據時返回 false
  3. 其他潛在問題

    • 確認資料庫連線是否正常,並且 db_session 表的結構是否符合預期。
    • 檢查 SET NAMES 'utf8'; 是否正確執行,確保字符集設定無誤。

解決方案

1. 修改 read 方法的返回值

read 方法在找不到會話數據時返回空字符串 '',而不是 false。這樣可以滿足 PHP 8 的要求,避免 session_start() 發出警告。

類基礎的實現修改:

public function read($key): string
{
    $sql = "SELECT `value` FROM `db_session` WHERE `sesskey`=:sesskey AND `expiry`>UNIX_TIMESTAMP()";
    $sth = $this->dbh->prepare($sql);
    $sth->bindParam(":sesskey", $key, PDO::PARAM_STR, 32);
    $sth->execute();
    $data = $sth->fetchAll(PDO::FETCH_ASSOC);
    return count($data) ? $data[0]["value"] : '';
}

過程式的實現修改:

function sess_read($key){
    $dbh = getConnection();
    $sql = "SELECT `value` FROM `db_session` WHERE `sesskey`=:sesskey AND `expiry`>UNIX_TIMESTAMP()";
    $sth = $dbh->prepare($sql);
    $sth->bindParam(":sesskey", $key, PDO::PARAM_STR, 32);
    $sth->execute();
    $data = $sth->fetchAll(PDO::FETCH_ASSOC);
    if(count($data) > 0) {
        return $data[0]["value"];
    }
    return ''; // 返回空字符串而不是 false
}

2. 確保其他方法的正確性

  • write 方法

    • 確保 write 方法返回布林值 truefalse,以表示寫入是否成功。
  • gc 方法

    • gc 方法應該返回布林值 truefalse,而不是 int | false

3. 確認資料庫連線與表結構

  • 確認 db_session 表中有 sesskey, expiry, 和 value 這些欄位,並且 sesskey 是主鍵或有適當的索引。
  • 確認資料庫連線參數 (DB_TYPE, DB_HOST, DB_NAME, DB_USER, DB_PASSWD) 正確無誤。
  • 確認 SESS_LIFE 常數已正確定義,並設置為適當的會話存活時間。

4. 完整範例修正後的類基礎實現

以下是修正後的類基礎實現範例:

<?php
    require_once dirname(__FILE__)."/define.php";  // 將資料庫參數設定為靜態值的檔案

    class SysSession implements SessionHandlerInterface
    {
        private $dbh;

        function __construct()
        {
            try {
                $this->dbh = new PDO(
                    DB_TYPE . ':host=' . DB_HOST . ';dbname=' . DB_NAME,
                    DB_USER,
                    DB_PASSWD,
                    array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8';")
                );
                $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                $this->dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
            } catch (PDOException $e) {
                die("Connection failed: " . $e->getMessage());
            }
        }

        public function open($savePath, $sessionName): bool
        {
            try {
                $this->dbh->query("SELECT 1");
            } catch (PDOException $e) {
                return false;
            }
            return true;
        }

        public function close(): bool
        {
            $this->dbh = null;
            return true;
        }

        public function read($key): string
        {
            $sql = "SELECT `value` FROM `db_session` WHERE `sesskey`=:sesskey AND `expiry`>UNIX_TIMESTAMP()";
            $sth = $this->dbh->prepare($sql);
            $sth->bindParam(":sesskey", $key, PDO::PARAM_STR, 32);
            $sth->execute();
            $data = $sth->fetchAll(PDO::FETCH_ASSOC);
            return count($data) ? $data[0]["value"] : '';
        }

        public function write($key, $val): bool
        {
            $expiry = time() + SESS_LIFE;
            $sql = "REPLACE INTO `db_session` (`sesskey`, `expiry`, `value`) VALUES (:sesskey, :expiry, :value)";
            $sth = $this->dbh->prepare($sql);
            $sth->bindParam(":sesskey", $key, PDO::PARAM_STR, 32);
            $sth->bindParam(":expiry", $expiry, PDO::PARAM_INT);
            $sth->bindParam(":value", $val, PDO::PARAM_STR);
            return $sth->execute();
        }

        public function destroy($key): bool
        {
            $sql = "DELETE FROM `db_session` WHERE `sesskey`=:sesskey";
            $sth = $this->dbh->prepare($sql);
            $sth->bindParam(":sesskey", $key, PDO::PARAM_STR, 32);
            return $sth->execute();
        }

        public function gc($maxlifetime): bool
        {
            $sql = "DELETE FROM `db_session` WHERE `expiry`<UNIX_TIMESTAMP()";
            $sth = $this->dbh->prepare($sql);
            return $sth->execute();
        }
    }

    ini_set('session.gc_maxlifetime', SESS_LIFE);
    ini_set('session.gc_probability', '1');
    ini_set('session.gc_divisor', '1');

    $handler = new SysSession();
    session_set_save_handler($handler, true);
    session_start();
?>

5. 測試檔案

確保在測試檔案中正確引用並啟動會話:

<?php
    require_once dirname(__FILE__)."/include/session_mysql.php";

    // 確保 session_start() 在設置 session handler 之後呼叫
    // 已在 session_mysql.php 中呼叫

    $a = isset($_SESSION["a"]) ? (int)$_SESSION["a"] : 1;
    $_SESSION["a"] = $a + 1;
    echo $a;
?>

總結

主要問題在於 read 方法返回了 false 而不是空字符串,這導致 PHP 8 對自訂 session handler 的要求不滿足。通過修改 read 方法的返回值,並確保其他方法的實現符合 PHP 8 的要求,應該可以解決您遇到的問題。

如果在進行上述修改後仍然遇到問題,建議檢查以下項目:

  • 確認 PHP 錯誤日誌中是否有更多詳細的錯誤資訊。
  • 確認資料庫連線和查詢是否正常。
  • 確認 db_session 表的結構和數據是否正確。

希望這些建議能夠幫助您順利完成網站的升級!

迷路 iT邦研究生 5 級 ‧ 2025-01-21 10:01:39 檢舉

修改read後就可以用了,非常感謝

我要發表回答

立即登入回答