由于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;
?>

请问我是哪边写错了吗?

1 个回答

2

zivzhong

iT邦研究生 4 级 ‧ 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 表的结构和数据是否正确。

希望这些建议能够帮助您顺利完成网站的升级!


  • 1

迷路

iT邦研究生 5 级 ‧
2025-01-21 10:01:39

修改read后就可以用了,非常感谢

修改