由於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;
?>
請問我是哪邊寫錯了嗎?
參考一下 Chatgpt 說:
根據您提供的情況,問題主要出在自訂的 session handler 無法正確讀取或處理會話數據,導致 session_start() 失敗。以下是詳細的分析與解決方案:
PHP 8 中 SessionHandlerInterface 的要求:
SessionHandlerInterface::read 方法必須返回一個字符串。如果沒有讀取到會話數據,應返回空字符串 '',而不是 false。返回 false 會被視為讀取失敗,導致 session_start() 發出警告。您的代碼中 read 方法的實現:
read 方法在找不到會話數據時返回 false,這不符合 PHP 8 的要求。sess_read 方法也是類似地在找不到數據時返回 false。其他潛在問題:
db_session 表的結構是否符合預期。SET NAMES 'utf8'; 是否正確執行,確保字符集設定無誤。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
}
write 方法:
write 方法返回布林值 true 或 false,以表示寫入是否成功。gc 方法:
gc 方法應該返回布林值 true 或 false,而不是 int | false。db_session 表中有 sesskey, expiry, 和 value 這些欄位,並且 sesskey 是主鍵或有適當的索引。DB_TYPE, DB_HOST, DB_NAME, DB_USER, DB_PASSWD) 正確無誤。SESS_LIFE 常數已正確定義,並設置為適當的會話存活時間。以下是修正後的類基礎實現範例:
<?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();
?>
確保在測試檔案中正確引用並啟動會話:
<?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 的要求,應該可以解決您遇到的問題。
如果在進行上述修改後仍然遇到問題,建議檢查以下項目:
db_session 表的結構和數據是否正確。希望這些建議能夠幫助您順利完成網站的升級!