由於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
表的結構和數據是否正確。希望這些建議能夠幫助您順利完成網站的升級!